Skip to content

Commit 9eb718e

Browse files
phurynclaude
andcommitted
Merge DEV: v1.2.6 — non-destructive rescan, Fable/Mythos+Opus-4.8 pricing, June 2026 dates, CI action bumps
Release boundary for v1.2.6. See CHANGELOG.md for the full set of changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents e2be890 + 70b56d4 commit 9eb718e

11 files changed

Lines changed: 123 additions & 23 deletions

File tree

.github/workflows/extension-ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ jobs:
2020
working-directory: vscode-extension
2121

2222
steps:
23-
- uses: actions/checkout@v4
23+
- uses: actions/checkout@v5
2424

25-
- uses: actions/setup-node@v4
25+
- uses: actions/setup-node@v5
2626
with:
2727
node-version: "20"
2828
cache: "npm"
@@ -40,7 +40,7 @@ jobs:
4040
run: npm run package
4141

4242
- name: Upload .vsix artifact
43-
uses: actions/upload-artifact@v4
43+
uses: actions/upload-artifact@v5
4444
with:
4545
name: claude-usage-extension-vsix
4646
path: vscode-extension/*.vsix

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
runs-on: ubuntu-latest
2828

2929
steps:
30-
- uses: actions/checkout@v4
30+
- uses: actions/checkout@v5
3131
with:
3232
# Need enough history to diff the whole push range (`before..after`)
3333
# not just the tip commit. fetch-depth: 0 = full clone; small repo,
@@ -103,7 +103,7 @@ jobs:
103103

104104
- name: Set up Node (to build the .vsix)
105105
if: steps.detect.outputs.version != ''
106-
uses: actions/setup-node@v4
106+
uses: actions/setup-node@v5
107107
with:
108108
node-version: "20"
109109
cache: "npm"

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
python-version: ["3.9", "3.11", "3.12"]
1515

1616
steps:
17-
- uses: actions/checkout@v4
17+
- uses: actions/checkout@v5
1818

1919
- name: Set up Python ${{ matrix.python-version }}
20-
uses: actions/setup-python@v5
20+
uses: actions/setup-python@v6
2121
with:
2222
python-version: ${{ matrix.python-version }}
2323

CHANGELOG.md

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

3+
## v1.2.6 — 2026-06-15
4+
5+
### Dashboard
6+
7+
- Added an explicit `claude-opus-4-8` entry to both pricing tables and pointed the generic `opus` fallback at it (was 4.7). 4.8 already costed correctly via the substring fallback — this guards against silent mis-costing if 4.8 is ever repriced, and keeps the catch-all on the newest Opus (#133, #134, thanks @Ninhache).
8+
- Added the `claude-fable-5`, `claude-mythos-5`, and `claude-opus-4-8` rows to the README cost table (they were already in the live CLI/dashboard tables) and listed `fable` / `mythos` in the README's "included models" note, so the docs match the code.
9+
- Unified the pricing "as of" date to **June 2026** everywhere — the dashboard footer, the in-chart cost sublabel, the pricing code comment, and the README were inconsistently labelled April/May.
10+
- Made the Rescan button non-destructive: `/api/rescan` now runs an incremental scan instead of deleting `usage.db` and rebuilding from scratch. `usage.db` is the only durable record of usage once Claude Code prunes old transcripts (`cleanupPeriodDays`), so the old wipe-and-rebuild could permanently lose history that was no longer on disk. The button stays (it's the only in-session way to ingest new turns); its tooltip now reflects the additive behaviour (#138, thanks @OtoGodfrey).
11+
12+
### Project / docs
13+
14+
- Bumped GitHub Actions to their Node 24-era major versions across all workflows (`actions/checkout@v5`, `actions/setup-node@v5`, `actions/setup-python@v6`, `actions/upload-artifact@v5`), ahead of GitHub forcing Node 24 on the runners (Node 20 actions are deprecated from 2026-06-16).
15+
316
## v1.2.5 — 2026-06-15
417

518
### Scanner / CLI

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,15 @@ Claude Code writes one JSONL file per session to `~/.claude/projects/`. Each lin
113113

114114
## Cost estimates
115115

116-
Costs are calculated using **Anthropic API pricing as of April 2026** ([claude.com/pricing#api](https://claude.com/pricing#api)).
116+
Costs are calculated using **Anthropic API pricing as of June 2026** ([claude.com/pricing#api](https://claude.com/pricing#api)).
117117

118-
**Only models whose name contains `opus`, `sonnet`, or `haiku` are included in cost calculations.** Local models, unknown models, and any other model names are excluded (shown as `n/a`).
118+
**Only models whose name contains `fable`, `mythos`, `opus`, `sonnet`, or `haiku` are included in cost calculations.** Local models, unknown models, and any other model names are excluded (shown as `n/a`).
119119

120120
| Model | Input | Output | Cache Write | Cache Read |
121121
|-------|-------|--------|------------|-----------|
122+
| claude-fable-5 | $10.00/MTok | $50.00/MTok | $12.50/MTok | $1.00/MTok |
123+
| claude-mythos-5 | $10.00/MTok | $50.00/MTok | $12.50/MTok | $1.00/MTok |
124+
| claude-opus-4-8 | $5.00/MTok | $25.00/MTok | $6.25/MTok | $0.50/MTok |
122125
| claude-opus-4-7 | $5.00/MTok | $25.00/MTok | $6.25/MTok | $0.50/MTok |
123126
| claude-opus-4-6 | $5.00/MTok | $25.00/MTok | $6.25/MTok | $0.50/MTok |
124127
| claude-sonnet-4-6 | $3.00/MTok | $15.00/MTok | $3.75/MTok | $0.30/MTok |

cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# (Mythos 5 shares Fable 5's pricing; Project-Glasswing access only.)
2222
"claude-fable-5": {"input": 10.00, "output": 50.00, "cache_read": 1.00, "cache_write": 12.50},
2323
"claude-mythos-5": {"input": 10.00, "output": 50.00, "cache_read": 1.00, "cache_write": 12.50},
24+
"claude-opus-4-8": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
2425
"claude-opus-4-7": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
2526
"claude-opus-4-6": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
2627
"claude-opus-4-5": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
@@ -45,7 +46,7 @@ def get_pricing(model):
4546
if "fable" in m or "mythos" in m:
4647
return PRICING["claude-fable-5"]
4748
if "opus" in m:
48-
return PRICING["claude-opus-4-7"]
49+
return PRICING["claude-opus-4-8"]
4950
if "sonnet" in m:
5051
return PRICING["claude-sonnet-4-6"]
5152
if "haiku" in m:

dashboard.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ def get_dashboard_data(db_path=DB_PATH):
269269
<h1>Claude Code Usage</h1>
270270
</div>
271271
<div class="meta" id="meta">Loading...</div>
272-
<button id="rescan-btn" onclick="triggerRescan()" title="Rebuild the database from scratch by re-scanning all JSONL files. Use if data looks stale or costs seem wrong.">&#x21bb; Rescan</button>
272+
<button id="rescan-btn" onclick="triggerRescan()" title="Scan for new usage since the last update. Adds new turns without affecting existing history.">&#x21bb; Rescan</button>
273273
</header>
274274
275275
<div id="filter-bar">
@@ -390,7 +390,7 @@ def get_dashboard_data(db_path=DB_PATH):
390390
391391
<footer>
392392
<div class="footer-content">
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>
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 June 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>
394394
<p>
395395
GitHub: <a href="https://github.com/phuryn/claude-usage" target="_blank">https://github.com/phuryn/claude-usage</a>
396396
&nbsp;&middot;&nbsp;
@@ -487,12 +487,13 @@ def get_dashboard_data(db_path=DB_PATH):
487487
}
488488
}
489489
490-
// ── Pricing (Anthropic API, April 2026) ────────────────────────────────────
490+
// ── Pricing (Anthropic API, June 2026) ────────────────────────────────────
491491
const PRICING = {
492492
// Fable / Mythos — Anthropic's most capable class, priced at 2x Opus.
493493
// (Mythos 5 shares Fable 5's pricing; Project-Glasswing access only.)
494494
'claude-fable-5': { input: 10.00, output: 50.00, cache_write: 12.50, cache_read: 1.00 },
495495
'claude-mythos-5': { input: 10.00, output: 50.00, cache_write: 12.50, cache_read: 1.00 },
496+
'claude-opus-4-8': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
496497
'claude-opus-4-7': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
497498
'claude-opus-4-6': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
498499
'claude-opus-4-5': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
@@ -519,7 +520,7 @@ def get_dashboard_data(db_path=DB_PATH):
519520
}
520521
const m = model.toLowerCase();
521522
if (m.includes('fable') || m.includes('mythos')) return PRICING['claude-fable-5'];
522-
if (m.includes('opus')) return PRICING['claude-opus-4-7'];
523+
if (m.includes('opus')) return PRICING['claude-opus-4-8'];
523524
if (m.includes('sonnet')) return PRICING['claude-sonnet-4-6'];
524525
if (m.includes('haiku')) return PRICING['claude-haiku-4-5'];
525526
return null;
@@ -920,7 +921,7 @@ def get_dashboard_data(db_path=DB_PATH):
920921
{ label: 'Output Tokens', value: fmt(t.output), sub: rangeLabel },
921922
{ label: 'Cache Read', value: fmt(t.cache_read), sub: 'from prompt cache' },
922923
{ label: 'Cache Creation', value: fmt(t.cache_creation), sub: 'writes to prompt cache' },
923-
{ label: 'Est. Cost', value: fmtCostBig(t.cost), sub: 'API pricing, May 2026', color: C.green },
924+
{ label: 'Est. Cost', value: fmtCostBig(t.cost), sub: 'API pricing, June 2026', color: C.green },
924925
];
925926
document.getElementById('stats-row').innerHTML = stats.map(s => `
926927
<div class="stat-card">
@@ -1544,14 +1545,15 @@ def do_GET(self):
15441545
def do_POST(self):
15451546
path = urlparse(self.path).path
15461547
if path == "/api/rescan":
1547-
# Full rebuild: delete DB and rescan from scratch.
1548+
# Incremental scan: ingest new/changed JSONL without touching
1549+
# existing rows. The DB is append-only and the only durable store
1550+
# of history once Claude Code prunes old transcripts, so we must
1551+
# never delete it here — scan() dedupes via the message_id index.
15481552
# Pass DB_PATH / DEFAULT_PROJECTS_DIRS explicitly so tests that
15491553
# patch the module globals are honored (scan's defaults are
15501554
# frozen at def time and would otherwise target the real paths).
15511555
import scanner
15521556
db_path = DB_PATH
1553-
if db_path.exists():
1554-
db_path.unlink()
15551557
result = scanner.scan(
15561558
db_path=db_path,
15571559
projects_dirs=scanner.DEFAULT_PROJECTS_DIRS,

tests/test_cli.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,47 @@ def test_exact_model_match(self):
1515
self.assertEqual(p["output"], 25.00)
1616

1717
def test_all_known_models_have_pricing(self):
18-
for model in ("claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5",
18+
for model in ("claude-fable-5", "claude-mythos-5",
19+
"claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-opus-4-5",
1920
"claude-sonnet-4-7", "claude-sonnet-4-6", "claude-sonnet-4-5",
2021
"claude-haiku-4-7", "claude-haiku-4-6", "claude-haiku-4-5"):
2122
p = get_pricing(model)
2223
self.assertGreater(p["input"], 0, f"Missing input price for {model}")
2324
self.assertGreater(p["output"], 0, f"Missing output price for {model}")
2425

26+
def test_fable_and_mythos_have_explicit_entries(self):
27+
"""Regression guard for #136/#137 — Fable 5 and Mythos 5 must be priced
28+
explicitly at 2x Opus, not fall through to $0/n/a or an Opus rate."""
29+
for model in ("claude-fable-5", "claude-mythos-5"):
30+
self.assertIn(model, PRICING)
31+
p = get_pricing(model)
32+
self.assertEqual(p["input"], 10.00, f"{model} input price wrong")
33+
self.assertEqual(p["output"], 50.00, f"{model} output price wrong")
34+
self.assertEqual(p["cache_read"], 1.00, f"{model} cache_read wrong")
35+
self.assertEqual(p["cache_write"], 12.50, f"{model} cache_write wrong")
36+
37+
def test_fable_date_suffix_matches(self):
38+
"""JSONL model strings may carry a date suffix."""
39+
p = get_pricing("claude-fable-5-20260601")
40+
self.assertEqual(p["input"], 10.00)
41+
self.assertEqual(p["output"], 50.00)
42+
43+
def test_substring_match_fable_and_mythos(self):
44+
"""Unknown future fable/mythos variants resolve to Fable pricing,
45+
not the generic opus/sonnet/haiku rates or n/a."""
46+
for model in ("some-fable-variant", "internal-mythos-test"):
47+
p = get_pricing(model)
48+
self.assertEqual(p["input"], 10.00, f"{model} should map to Fable pricing")
49+
self.assertEqual(p["output"], 50.00, f"{model} should map to Fable pricing")
50+
51+
def test_opus_4_8_has_explicit_entry(self):
52+
"""Regression guard for issue #133 — Opus 4.8 must be present, not just
53+
resolved via the generic 'opus' substring fallback."""
54+
self.assertIn("claude-opus-4-8", PRICING)
55+
p = get_pricing("claude-opus-4-8")
56+
self.assertEqual(p["input"], 5.00)
57+
self.assertEqual(p["output"], 25.00)
58+
2559
def test_opus_4_7_has_explicit_entry(self):
2660
"""Regression guard for issue #61 — Opus 4.7 must be present."""
2761
p = get_pricing("claude-opus-4-7")

tests/test_dashboard.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ class TestDashboardHTTP(unittest.TestCase):
255255
@classmethod
256256
def setUpClass(cls):
257257
# Redirect DB_PATH + projects dirs to a tempdir so /api/rescan
258-
# doesn't unlink the user's real ~/.claude/usage.db or scan their
259-
# real transcript directory during tests.
258+
# writes to a throwaway DB and scans a throwaway transcript dir
259+
# instead of the user's real ~/.claude/usage.db and transcripts.
260260
import dashboard as _d
261261
import scanner as _s
262262
cls._tmpdir = tempfile.TemporaryDirectory()
@@ -328,6 +328,53 @@ def test_api_rescan_returns_json(self):
328328
self.assertIn("updated", data)
329329
self.assertIn("skipped", data)
330330

331+
def test_api_rescan_is_non_destructive(self):
332+
# Regression (#138): /api/rescan must NOT wipe the DB. usage.db is the
333+
# only durable store of history once Claude Code prunes old transcripts
334+
# (cleanupPeriodDays), so a rescan with nothing left on disk must keep
335+
# the existing rows. Seed history that has no corresponding JSONL file
336+
# (the projects dir is empty), rescan, and assert it survives.
337+
import dashboard as _d
338+
db_path = _d.DB_PATH
339+
conn = get_db(db_path)
340+
init_db(conn)
341+
upsert_sessions(conn, [{
342+
"session_id": "pruned-sess", "project_name": "user/oldproject",
343+
"first_timestamp": "2026-01-01T09:00:00Z",
344+
"last_timestamp": "2026-01-01T10:00:00Z",
345+
"git_branch": "main", "model": "claude-opus-4-8",
346+
"total_input_tokens": 1000, "total_output_tokens": 400,
347+
"total_cache_read": 0, "total_cache_creation": 0,
348+
"turn_count": 1,
349+
}])
350+
insert_turns(conn, [{
351+
"session_id": "pruned-sess", "timestamp": "2026-01-01T09:30:00Z",
352+
"model": "claude-opus-4-8", "input_tokens": 1000,
353+
"output_tokens": 400, "cache_read_tokens": 0,
354+
"cache_creation_tokens": 0, "tool_name": None, "cwd": "/tmp",
355+
"message_id": "msg-pruned-1",
356+
}])
357+
conn.commit()
358+
conn.close()
359+
360+
url = f"http://127.0.0.1:{self.port}/api/rescan"
361+
req = urllib.request.Request(url, method="POST")
362+
with urllib.request.urlopen(req) as resp:
363+
self.assertEqual(resp.status, 200)
364+
365+
conn = sqlite3.connect(db_path)
366+
try:
367+
turn_count = conn.execute(
368+
"SELECT COUNT(*) FROM turns WHERE session_id = 'pruned-sess'"
369+
).fetchone()[0]
370+
sess_count = conn.execute(
371+
"SELECT COUNT(*) FROM sessions WHERE session_id = 'pruned-sess'"
372+
).fetchone()[0]
373+
finally:
374+
conn.close()
375+
self.assertEqual(turn_count, 1, "rescan must not delete existing turns")
376+
self.assertEqual(sess_count, 1, "rescan must not delete existing sessions")
377+
331378
def test_404_for_unknown_path(self):
332379
url = f"http://127.0.0.1:{self.port}/nonexistent"
333380
try:

vscode-extension/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`):
5454
| Command | What it does |
5555
|---|---|
5656
| **Claude Usage: Open Dashboard** | Reveal the sidebar and start the server (also fires automatically when you click the activity-bar icon) |
57-
| **Claude Usage: Rescan Transcripts** | Refresh the iframe; the dashboard's own Rescan button triggers a full DB rebuild |
57+
| **Claude Usage: Rescan Transcripts** | Refresh the iframe; the dashboard's own Rescan button runs an incremental scan that adds new usage without touching existing history |
5858
| **Claude Usage: Restart Server** | Kill and respawn the Python process (use after changing settings) |
5959
| **Claude Usage: Show Logs** | Open the extension's output channel — useful when something doesn't work |
6060

0 commit comments

Comments
 (0)