Skip to content

Commit e2be890

Browse files
phurynclaude
andcommitted
Release v1.2.5
Merge DEV: serve-first dashboard, Fable/Mythos pricing, automated GitHub Releases (tag-on-merge now builds and attaches the .vsix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents 9b3c206 + 5c4bfad commit e2be890

7 files changed

Lines changed: 168 additions & 27 deletions

File tree

.github/workflows/tag-on-merge.yml

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
name: Auto-tag releases from CHANGELOG
1+
name: Tag and release from CHANGELOG
22

33
# Runs after each push to main. If CHANGELOG.md gained a new ## vX.Y.Z heading
4-
# anywhere in this push's commit range (compared to the push's `before` SHA),
5-
# create a lightweight tag with that version name pointing at the pushed
6-
# commit. CHANGELOG is the source of truth; tags are a deterministic
7-
# projection of it.
4+
# anywhere in this push's commit range (compared to the push's `before` SHA):
5+
# 1. create a lightweight tag with that version name at the pushed commit, and
6+
# 2. build the VS Code extension .vsix and publish a GitHub Release for that
7+
# tag — the matching CHANGELOG section as the notes, the .vsix attached.
88
#
9-
# No action when CHANGELOG wasn't touched, no action when an existing version
10-
# heading was edited (not added), no action when the resulting tag already
11-
# exists. So this is safe to re-run on force-pushes and amends.
9+
# CHANGELOG is the source of truth; the tag and the Release are deterministic
10+
# projections of it. This is the automated twin of the manual procedure in
11+
# c:\GitHub\grok-build-vscode (scripts/release.*): tag + `gh release create`
12+
# with the built .vsix as a release asset.
13+
#
14+
# No action when CHANGELOG wasn't touched, when an existing version heading was
15+
# edited (not added), or when the tag/release already exists. Safe to re-run on
16+
# force-pushes and amends.
1217

1318
on:
1419
push:
@@ -91,3 +96,79 @@ jobs:
9196
git tag "$VERSION"
9297
git push origin "$VERSION"
9398
echo "Tagged $VERSION at $(git rev-parse HEAD)."
99+
100+
# ── Release: build the .vsix and publish a GitHub Release ───────────────
101+
# Everything below is gated on a new version being detected, so ordinary
102+
# pushes to main (docs, typo fixes) incur no Node setup or build cost.
103+
104+
- name: Set up Node (to build the .vsix)
105+
if: steps.detect.outputs.version != ''
106+
uses: actions/setup-node@v4
107+
with:
108+
node-version: "20"
109+
cache: "npm"
110+
cache-dependency-path: vscode-extension/package-lock.json
111+
112+
- name: Build the .vsix
113+
if: steps.detect.outputs.version != ''
114+
working-directory: vscode-extension
115+
env:
116+
VERSION: ${{ steps.detect.outputs.version }}
117+
run: |
118+
set -euo pipefail
119+
120+
# The .vsix filename embeds vscode-extension/package.json's version, so
121+
# it must match the CHANGELOG heading or the asset name won't match the
122+
# tag. Bumping package.json is a release-time step (see AGENTS.md); fail
123+
# loudly if it was forgotten rather than ship a mislabelled asset.
124+
want="${VERSION#v}"
125+
pkg_version=$(node -p "require('./package.json').version")
126+
if [ "$pkg_version" != "$want" ]; then
127+
echo "::error::vscode-extension/package.json is $pkg_version but CHANGELOG released $VERSION. Bump package.json to $want before the release merge."
128+
exit 1
129+
fi
130+
131+
npm ci
132+
npm run package
133+
134+
name=$(node -p "require('./package.json').name")
135+
vsix="${name}-${want}.vsix"
136+
if [ ! -f "$vsix" ]; then
137+
echo "::error::Expected $vsix but it wasn't produced."
138+
exit 1
139+
fi
140+
echo "VSIX=vscode-extension/$vsix" >> "$GITHUB_ENV"
141+
echo "Built $vsix."
142+
143+
- name: Create GitHub Release with the .vsix attached
144+
if: steps.detect.outputs.version != ''
145+
env:
146+
VERSION: ${{ steps.detect.outputs.version }}
147+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
148+
run: |
149+
set -euo pipefail
150+
151+
# Idempotent: a re-push of the same release commit shouldn't error.
152+
if gh release view "$VERSION" >/dev/null 2>&1; then
153+
echo "Release $VERSION already exists; nothing to do."
154+
exit 0
155+
fi
156+
157+
# Extract this version's CHANGELOG section (heading through the line
158+
# before the next `## vX` heading) as the release notes. $2 is the
159+
# version token: `## v1.2.5 — 2026-06-15` → $2 == "v1.2.5".
160+
notes="$(mktemp)"
161+
awk -v ver="$VERSION" '
162+
/^## v[0-9]/ { if (started) exit; if ($2 == ver) started=1 }
163+
started { print }
164+
' CHANGELOG.md > "$notes"
165+
if [ ! -s "$notes" ]; then
166+
echo "::error::No '## $VERSION' section found in CHANGELOG.md."
167+
exit 1
168+
fi
169+
170+
gh release create "$VERSION" \
171+
--title "$VERSION" \
172+
--notes-file "$notes" \
173+
"$VSIX"
174+
echo "Released $VERSION with $VSIX attached."

AGENTS.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,16 @@ This applies to all agents working on this repo, not just Claude Code.
107107

108108
The release flow:
109109
1. While work accumulates on `DEV`, the `## vX.Y.Z — TBD` heading at the top of `CHANGELOG.md` collects bullets. (For automated triage runs, see the routine note below.)
110-
2. When the maintainer is ready to release, they finalize the heading (`TBD` → today's date), merge `DEV → main` with `merge --no-ff` (so the release boundary is visible in `git log main`), and push `main`.
111-
3. [`.github/workflows/tag-on-merge.yml`](.github/workflows/tag-on-merge.yml) fires on the push, sees the new `## vX.Y.Z` heading in the CHANGELOG diff, and creates a lightweight tag at the merge commit. **No `git tag` step for the maintainer.**
110+
2. When the maintainer is ready to release, they finalize the heading (`TBD` → today's date), **bump `vscode-extension/package.json`'s `version` to match the CHANGELOG version** (the two ship in lockstep — the extension bundles the Python sources), merge `DEV → main` with `merge --no-ff` (so the release boundary is visible in `git log main`), and push `main`.
111+
3. [`.github/workflows/tag-on-merge.yml`](.github/workflows/tag-on-merge.yml) fires on the push, sees the new `## vX.Y.Z` heading in the CHANGELOG diff, and:
112+
- creates a lightweight tag at the merge commit (**no `git tag` step for the maintainer**), then
113+
- builds the VS Code extension `.vsix` and publishes a **GitHub Release** for that tag — the matching CHANGELOG section as the release notes, the built `.vsix` attached as a release asset.
112114

113-
No formal `gh release create` at any cadence — the CHANGELOG entry IS the release notes, and the tag IS the release marker. If a particular release ever warrants a formal GitHub Release page (e.g. a major with breaking changes), it can be promoted retroactively from the existing tag.
115+
So every release is both a tag *and* a GitHub Release with the installable `.vsix` downloadable from it. This mirrors the manual procedure in the sibling `grok-build-vscode` repo (`scripts/release.*`: tag + `gh release create` with the `.vsix` attached), adapted to this repo's CHANGELOG-driven, merge-to-`main` model — so it's automated rather than a local script. Marketplace publish (`vsce publish`) stays separate and explicit, exactly as there.
114116

115-
The workflow is idempotent: if the tag already exists (someone tagged manually before the workflow caught up), it's a no-op. It also no-ops on pushes that don't add a new version heading (typo fixes, docs-only edits, etc.).
117+
**The release step asserts `vscode-extension/package.json`'s version equals the CHANGELOG version and fails loudly if not** — the `.vsix` filename embeds the package version, so a mismatch would mislabel the asset. If you forget the bump, the tag is still created but the Release step fails; bump `package.json` and create the Release by hand (`gh release create vX.Y.Z --notes-file <section> vscode-extension/<name>-X.Y.Z.vsix`), since a same-commit re-push won't re-add the heading to re-trigger the workflow.
118+
119+
The workflow is idempotent: if the tag already exists (someone tagged manually before the workflow caught up) the tag step is a no-op, and if the Release already exists the release step is a no-op. It also no-ops entirely on pushes that don't add a new version heading (typo fixes, docs-only edits, etc.).
116120

117121
Existing tags `v1.0.0`, `v1.1.0`, `v1.1.1` are lightweight and were created by hand before the workflow existed. `v1.1.2` was the first tag created by the workflow. The workflow only *adds* missing tags; it never reconciles existing ones. Don't bother re-tagging the legacy ones.
118122

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## v1.2.5 — 2026-06-15
4+
5+
### Scanner / CLI
6+
7+
- `cli.py dashboard` now binds and serves the port *first*, then runs the scan in a background thread, instead of scanning before starting the server. A cold scan over a large `~/.claude/projects` backlog can take over a minute, and the VS Code extension kills the dashboard process if it doesn't answer `/api/data` within ~10s — so the server was being killed long before it ever bound. The dashboard now comes up immediately and fills in data as the background scan commits.
8+
9+
### Dashboard
10+
11+
- Added Fable and Mythos to the pricing tables (both CLI and dashboard), priced at 2× Opus (input $10 / output $50 / MTok; cache-read $1.00, cache-write $12.50). `claude-mythos-5` shares `claude-fable-5`'s pricing. They're now billable, sort above Opus in the model filter, and resolve via the keyword fallback (`fable` / `mythos`).
12+
- The "no data yet" path no longer wipes the page — on a fresh start the server serves before the initial scan creates the DB, so `/api/data` can briefly return an error. The dashboard now shows a non-destructive "retrying…" notice and re-polls until the background scan produces data.
13+
- Added `PRAGMA busy_timeout = 5000` to the dashboard's read connection so reads wait briefly for the background scan's write locks instead of raising "database is locked".
14+
15+
### Packaging
16+
17+
- The auto-tag workflow ([.github/workflows/tag-on-merge.yml](.github/workflows/tag-on-merge.yml)) now also publishes a **GitHub Release** for each new version: it builds the VS Code extension `.vsix` and attaches it to the release, using the matching CHANGELOG section as the release notes. Tags and releases are deterministic projections of the CHANGELOG. The release step asserts `vscode-extension/package.json`'s version matches the CHANGELOG heading and fails loudly otherwise, so the `.vsix` asset is always correctly labelled. Bumped the extension to 1.2.5.
18+
319
## v1.2.4 — 2026-05-30
420

521
### Dashboard

cli.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
DB_PATH = Path.home() / ".claude" / "usage.db"
1818

1919
PRICING = {
20+
# Fable / Mythos — Anthropic's most capable class, priced at 2x Opus.
21+
# (Mythos 5 shares Fable 5's pricing; Project-Glasswing access only.)
22+
"claude-fable-5": {"input": 10.00, "output": 50.00, "cache_read": 1.00, "cache_write": 12.50},
23+
"claude-mythos-5": {"input": 10.00, "output": 50.00, "cache_read": 1.00, "cache_write": 12.50},
2024
"claude-opus-4-7": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
2125
"claude-opus-4-6": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
2226
"claude-opus-4-5": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
@@ -38,6 +42,8 @@ def get_pricing(model):
3842
return PRICING[key]
3943
# Substring fallback: match model family by keyword
4044
m = model.lower()
45+
if "fable" in m or "mythos" in m:
46+
return PRICING["claude-fable-5"]
4147
if "opus" in m:
4248
return PRICING["claude-opus-4-7"]
4349
if "sonnet" in m:
@@ -358,21 +364,37 @@ def cmd_stats():
358364

359365

360366
def cmd_dashboard(projects_dir=None, host=None, port=None, no_browser=False):
361-
print("Running scan first...")
362-
cmd_scan(projects_dir=projects_dir)
367+
import threading
368+
import time
363369

364-
print("\nStarting dashboard server...")
365370
from dashboard import serve
366371

367372
host = host or os.environ.get("HOST", "localhost")
368373
port = int(port or os.environ.get("PORT", "8080"))
369374

375+
# Bind and serve the port *first*, then scan in the background. A cold scan
376+
# over a large ~/.claude/projects backlog can take well over a minute, and
377+
# the VS Code extension kills the process if it doesn't answer /api/data
378+
# within ~10s (see vscode-extension/src/server-manager.ts). Serving up front
379+
# means the port is live immediately; the dashboard shows whatever's already
380+
# in the DB and auto-refreshes as the background scan commits new data.
381+
#
382+
# Capture cmd_scan into a local so the background thread closes over the
383+
# current binding — keeps the test suite's mock.patch(cli.cmd_scan) effective
384+
# and prevents the thread from ever touching the real DB after a patch lifts.
385+
scan = cmd_scan
386+
387+
def background_scan():
388+
print("Scanning in the background...")
389+
scan(projects_dir=projects_dir)
390+
print("Background scan complete.")
391+
392+
threading.Thread(target=background_scan, daemon=True).start()
393+
370394
# Open a browser for users running this as a script (see README). The VS Code
371395
# extension passes --no-browser since it embeds the dashboard in a webview.
372396
if not no_browser:
373397
import webbrowser
374-
import threading
375-
import time
376398

377399
def open_browser():
378400
time.sleep(1.0)

dashboard.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def get_dashboard_data(db_path=DB_PATH):
1818
return {"error": "Database not found. Run: python cli.py scan"}
1919

2020
conn = sqlite3.connect(db_path)
21+
# The dashboard reads while a background scan may be committing (cmd_dashboard
22+
# serves first, scans in a background thread; /api/rescan scans in-process too).
23+
# Wait briefly for write locks instead of raising "database is locked".
24+
conn.execute("PRAGMA busy_timeout = 5000")
2125
conn.row_factory = sqlite3.Row
2226

2327
# ── All models (for filter UI) ────────────────────────────────────────────
@@ -386,7 +390,7 @@ def get_dashboard_data(db_path=DB_PATH):
386390
387391
<footer>
388392
<div class="footer-content">
389-
<p>Cost estimates based on Anthropic API pricing (<a href="https://claude.com/pricing#api" target="_blank">claude.com/pricing#api</a>) as of May 2026. Only models containing <em>opus</em>, <em>sonnet</em>, or <em>haiku</em> in the name are included in cost calculations. Actual costs for Max/Pro subscribers differ from API pricing.</p>
393+
<p>Cost estimates based on Anthropic API pricing (<a href="https://claude.com/pricing#api" target="_blank">claude.com/pricing#api</a>) as of May 2026. Only models containing <em>fable</em>, <em>mythos</em>, <em>opus</em>, <em>sonnet</em>, or <em>haiku</em> in the name are included in cost calculations. Actual costs for Max/Pro subscribers differ from API pricing.</p>
390394
<p>
391395
GitHub: <a href="https://github.com/phuryn/claude-usage" target="_blank">https://github.com/phuryn/claude-usage</a>
392396
&nbsp;&middot;&nbsp;
@@ -485,6 +489,10 @@ def get_dashboard_data(db_path=DB_PATH):
485489
486490
// ── Pricing (Anthropic API, April 2026) ────────────────────────────────────
487491
const PRICING = {
492+
// Fable / Mythos — Anthropic's most capable class, priced at 2x Opus.
493+
// (Mythos 5 shares Fable 5's pricing; Project-Glasswing access only.)
494+
'claude-fable-5': { input: 10.00, output: 50.00, cache_write: 12.50, cache_read: 1.00 },
495+
'claude-mythos-5': { input: 10.00, output: 50.00, cache_write: 12.50, cache_read: 1.00 },
488496
'claude-opus-4-7': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
489497
'claude-opus-4-6': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
490498
'claude-opus-4-5': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
@@ -499,7 +507,8 @@ def get_dashboard_data(db_path=DB_PATH):
499507
function isBillable(model) {
500508
if (!model) return false;
501509
const m = model.toLowerCase();
502-
return m.includes('opus') || m.includes('sonnet') || m.includes('haiku');
510+
return m.includes('fable') || m.includes('mythos') ||
511+
m.includes('opus') || m.includes('sonnet') || m.includes('haiku');
503512
}
504513
505514
function getPricing(model) {
@@ -509,6 +518,7 @@ def get_dashboard_data(db_path=DB_PATH):
509518
if (model.startsWith(key)) return PRICING[key];
510519
}
511520
const m = model.toLowerCase();
521+
if (m.includes('fable') || m.includes('mythos')) return PRICING['claude-fable-5'];
512522
if (m.includes('opus')) return PRICING['claude-opus-4-7'];
513523
if (m.includes('sonnet')) return PRICING['claude-sonnet-4-6'];
514524
if (m.includes('haiku')) return PRICING['claude-haiku-4-5'];
@@ -675,10 +685,11 @@ def get_dashboard_data(db_path=DB_PATH):
675685
// ── Model filter ───────────────────────────────────────────────────────────
676686
function modelPriority(m) {
677687
const ml = m.toLowerCase();
678-
if (ml.includes('opus')) return 0;
679-
if (ml.includes('sonnet')) return 1;
680-
if (ml.includes('haiku')) return 2;
681-
return 3;
688+
if (ml.includes('fable') || ml.includes('mythos')) return 0;
689+
if (ml.includes('opus')) return 1;
690+
if (ml.includes('sonnet')) return 2;
691+
if (ml.includes('haiku')) return 3;
692+
return 4;
682693
}
683694
684695
function readURLModels(allModels) {
@@ -1412,7 +1423,13 @@ def get_dashboard_data(db_path=DB_PATH):
14121423
const resp = await fetch('/api/data');
14131424
const d = await resp.json();
14141425
if (d.error) {
1415-
document.body.innerHTML = '<div style="padding:40px;color:#C74E39">' + esc(d.error) + '</div>';
1426+
// The server binds and serves before the initial scan finishes, so on a
1427+
// fresh start the DB may not exist yet. Show a non-destructive notice and
1428+
// retry instead of nuking the page — once the background scan creates the
1429+
// DB, the next poll renders normally.
1430+
const meta = document.getElementById('meta');
1431+
if (meta) meta.innerHTML = esc(d.error) + ' — retrying…';
1432+
if (rawData === null) setTimeout(loadData, 3000);
14161433
return;
14171434
}
14181435
const refreshNote = rangeIncludesToday(selectedRange) ? '<br>Auto-refresh in 30s' : '';

scanner.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
DB_PATH = Path.home() / ".claude" / "usage.db"
1515
DEFAULT_PROJECTS_DIRS = [PROJECTS_DIR, XCODE_PROJECTS_DIR]
1616

17-
# Higher number = higher priority when choosing a session's primary model
18-
MODEL_PRIORITY = {"opus": 3, "sonnet": 2, "haiku": 1}
17+
# Higher number = higher priority when choosing a session's primary model.
18+
# Fable / Mythos are Anthropic's most capable class, so they outrank Opus.
19+
MODEL_PRIORITY = {"fable": 5, "mythos": 5, "opus": 3, "sonnet": 2, "haiku": 1}
1920

2021

2122
def _model_priority(model):

vscode-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-usage-phuryn",
33
"displayName": "Claude Code Usage by Paweł Huryn",
44
"description": "Embed your Claude Code usage dashboard (token counts, costs, sessions, projects) directly inside VS Code. Reads local JSONL transcripts, no API calls.",
5-
"version": "1.2.4",
5+
"version": "1.2.5",
66
"publisher": "PawelHuryn",
77
"author": {
88
"name": "Paweł Huryn",

0 commit comments

Comments
 (0)