claude-token-saver 3.11.1 β 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/bin/cli.js +16 -0
- package/package.json +1 -1
- package/src/commands/harness.js +19 -1
- package/src/formatters/statusline.js +39 -10
- package/src/harness.js +29 -0
- package/src/model-alias.js +13 -1
- package/src/route-scan.js +17 -0
- package/src/savings-ledger.js +86 -0
package/README.en.md
CHANGED
|
@@ -77,7 +77,7 @@ The statusline appears at the bottom of Claude Code right away. If auto-registra
|
|
|
77
77
|
| `β¦ current` / `π
weekly` | 5-hour / 7-day rate-limit window usage + reset time |
|
|
78
78
|
| `π¦` | Context usage (e.g. `Ctx 68% of 1M`) β colored by fill. Current models default to 1M with no premium, but token volume itself drives per-turn cost and 5H/7D burn |
|
|
79
79
|
| `π°` | Cumulative savings from prompt caching |
|
|
80
|
-
| `π` | Cumulative savings from
|
|
80
|
+
| `π` | **Cumulative savings from model routing** β what work handled by a cheaper model instead of this one saved. A different number from prompt-cache savings (`π°`). Hidden until a delegation has actually been measured |
|
|
81
81
|
|
|
82
82
|
When something is wrong, a **warning chip leads the line**:
|
|
83
83
|
|
|
@@ -301,8 +301,13 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
|
|
|
301
301
|
|
|
302
302
|
## Release notes
|
|
303
303
|
|
|
304
|
+
### v3.12.0 (2026-08-22)
|
|
305
|
+
- **Routing saved now leads the statusline with weekly / monthly / lifetime totals** β `π Routing saved $1.3 wk Β· $2.0 mo Β· $9.8 all`. Each delegated subagent run's saving is recorded in a ledger (`delegation-ledger.json`) keyed by its transcript path, so overlapping re-scans upsert instead of double-counting; the statusline sums 7-day, 30-day, and lifetime windows on line 1 while the other chips move to line 2. An empty ledger keeps the legacy one-line layout, and `--single-line` forces it (some macOS Claude Code builds render only the first line). Totals start accruing from the release β no back-fill.
|
|
306
|
+
- **`harness check` reports context weight** β the approximate per-request token cost of CLAUDE.md (warns over the ~4k guideline) and whether a `.claudeignore` exists. Advisory only; the π
· N/5 score is unaffected.
|
|
307
|
+
- **Self-describing ARNs resolve without learning** β `foundation-model/anthropic.claude-β¦` and system cross-region profiles (`inference-profile/us.anthropic.claude-β¦`) embed the model id yet resolved to `unknown`, dropping delegation and cost aggregation entirely on gateway machines that hadn't accumulated learning samples. They now pass straight to the existing tier classifier; opaque application-profile ids keep the override/learning path.
|
|
308
|
+
|
|
304
309
|
### v3.11.0 (2026-08-21)
|
|
305
|
-
- **
|
|
310
|
+
- **Routing savings chip in the statusline** β `π Routing saved $3.2`. Cumulative savings from model routing, placed right after the model name so it reads early in the line. The existing `π° Cache saved` covers the prompt cache and is a different number. Zero or missing data hides the chip entirely, so nothing changes for direct-API users. The statusline only reads `model-rules.json` β it never triggers a scan (it re-renders every few seconds). Segment name: `delegated`.
|
|
306
311
|
- **Role-learning mis-classification fixed** β subagent records sometimes land in the parent transcript without an `isSidechain` flag, and that circumstantial evidence could outvote stated evidence, confirming a haiku profile as the session model (measured: 16 against 1, agreement exactly at the 80% line). Stated evidence (`Task(model:)` parameters, agent-definition frontmatter) and inferred evidence are now tallied separately, and an inference is not adopted when stated evidence contradicts it. Undecided ids stay `unknown` and leave the aggregate.
|
|
307
312
|
|
|
308
313
|
### v3.10.0 (2026-08-20)
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ npm i -g claude-token-saver
|
|
|
76
76
|
| `β¦ current` / `π
weekly` | 5μκ° / 7μΌ rate-limit μλ μ¬μ©λ₯ + 리μ
μκ° |
|
|
77
77
|
| `π¦` | 컨ν
μ€νΈ μ¬μ©λ₯ (μ: `Ctx 68% of 1M`) β μ¬μ©λ₯ κΈ°μ€ λ
Ή/ν©/μ . νμ¬ λͺ¨λΈμ 1Mμ΄ κΈ°λ³ΈΒ·ν리미μ μμμ΄μ§λ§, ν ν°λ μμ²΄κ° ν΄λΉ λΉμ©κ³Ό 5H/7D νλλ₯Ό νμλλ€ |
|
|
78
78
|
| `π°` | μΊμκ° μ μ½ν΄μ€ λμ κΈμ‘ |
|
|
79
|
-
| `π` |
|
|
79
|
+
| `π` | **λͺ¨λΈ μμμΌλ‘ μλ λμ λΉμ©** β κ°μ μΌμ λ μΌ λͺ¨λΈμ΄ λμ μ²λ¦¬ν΄μ μλ κΈμ‘μ΄λ©°, ν둬ννΈ μΊμ μ κ°(`π°`)κ³Όλ λ€λ₯Έ μμΉμ
λλ€. μ€μΈ‘ μμμ΄ μμΌλ©΄ νμλμ§ μμ΅λλ€ |
|
|
80
80
|
|
|
81
81
|
λ¬Έμ κ° κ°μ§λλ©΄ **κ²½κ³ μΉ©μ΄ λ§¨ μμ** λΆμ΅λλ€:
|
|
82
82
|
|
|
@@ -257,8 +257,13 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
|
257
257
|
|
|
258
258
|
## λ¦΄λ¦¬μ€ λ
ΈνΈ
|
|
259
259
|
|
|
260
|
+
### v3.12.0 (2026-08-22)
|
|
261
|
+
- **Routing savedκ° μ£Όκ°Β·μκ°Β·λμ μΌλ‘ statusline 1μ€μ§Έμ μ¬λΌμ΅λλ€** β `π Routing saved $1.3 wk Β· $2.0 mo Β· $9.8 all`. μμ μ κ° μ΄λ²€νΈλ₯Ό μλΈμμ΄μ νΈ run λ¨μλ‘ μμ₯(`delegation-ledger.json`)μ κΈ°λ‘νκ³ (νΈλμ€ν¬λ¦½νΈ κ²½λ‘ ν€λΌ μ¬μ€μΊμ΄ κ²Ήμ³λ μ€λ³΅ μ§κ³ μμ), 7μΌΒ·30μΌΒ·μ 체 ν©μ°μ νμν©λλ€. λλ¨Έμ§ μΉ©μ 2μ€μ§Έλ‘ λ΄λ €κ°λλ€. μμ₯μ΄ λΉμ΄ μμΌλ©΄ κΈ°μ‘΄ 1μ€ κ·Έλλ‘μ΄κ³ , μΌλΆ νκ²½(macOS ꡬλ²μ Claude Code)μμ 첫 μ€λ§ 보μ΄λ©΄ `--single-line`μΌλ‘ κΈ°μ‘΄ λ μ΄μμμ μ μ§ν μ μμ΅λλ€. μ§κ³λ μμ₯ λμ
μμ λΆν° μμν©λλ€(μκΈ μμ).
|
|
262
|
+
- **`harness check`κ° μ»¨ν
μ€νΈ 무κ²λ μλ €μ€λλ€** β CLAUDE.mdμ μμ²λΉ λλ΅ ν ν°(~4k κ°μ΄λλΌμΈ μ΄κ³Ό μ κ²½κ³ )κ³Ό `.claudeignore` μ 무λ₯Ό νμν©λλ€. μλ¬Έ μ λ³΄μΌ λΏ π
· N/5 μ μμλ λ°μνμ§ μμ΅λλ€.
|
|
263
|
+
- **λͺ¨λΈλͺ
μ΄ λ΄μ₯λ ARNμ νμ΅ μμ΄ μ¦μ ν΄μ** β `foundation-model/anthropic.claude-β¦`μ μμ€ν
κ΅μ°¨ 리μ νλ‘νμΌ(`inference-profile/us.anthropic.claude-β¦`)μ μμ IDκ° κ³§ λͺ¨λΈλͺ
μΈλ°λ `unknown`μΌλ‘ λ¨μ΄μ Έ, νμ΅ νλ³Έμ΄ μμ΄κΈ° μ μ κ²μ΄νΈμ¨μ΄ νκ²½μμ μμΒ·λΉμ© μ§κ³κ° ν΅μ§Έλ‘ λΉ μ‘μ΅λλ€. μ΄μ μ¦μ κΈ°μ‘΄ ν°μ΄ λΆλ₯λ‘ λκΉλλ€. λΆν¬λͺ
ν application-profile IDλ μ’
μ λλ‘ μ€λ²λΌμ΄λ/νμ΅ κ²½λ‘μ
λλ€.
|
|
264
|
+
|
|
260
265
|
### v3.11.0 (2026-08-21)
|
|
261
|
-
- **statuslineμ μμ μ κ° μΉ© μΆκ°** β `π
|
|
266
|
+
- **statuslineμ μμ μ κ° μΉ© μΆκ°** β `π Routing saved $3.2`. λͺ¨λΈ μμμΌλ‘ μλ λμ λΉμ©μ΄λ©°, λͺ¨λΈ μ΄λ¦ λ°λ‘ λ€μ λμ¬ μ€ μμͺ½μμ μ½νλλ€. κΈ°μ‘΄ `π° Cache saved`(ν둬ννΈ μΊμ μ κ°)μλ λ€λ₯Έ μμΉμ
λλ€. κ°μ΄ 0μ΄κ±°λ λ°μ΄ν°κ° μμΌλ©΄ μΉ©μ μμ κ·Έλ¦¬μ§ μμ μ§μ API μ¬μ©μμκ²λ μ무κ²λ λμ§ μμ΅λλ€. statuslineμ `model-rules.json`μ μ½κΈ°λ§ νλ©° μ€μΊμ λλ¦¬μ§ μμ΅λλ€(5μ΄λ§λ€ νΈμΆλλ μ리μ
λλ€). μΈκ·Έλ¨ΌνΈ μ΄λ¦μ `delegated`μ
λλ€.
|
|
262
267
|
- **μν νμ΅μ μ€λΆλ₯ μμ ** β μλΈμμ΄μ νΈ λ μ½λκ° λΆλͺ¨ νΈλμ€ν¬λ¦½νΈμ `isSidechain` μμ΄ μμ¬ λ€μ΄μ€λ κ²½μ°κ° μμ΄, κ·Έ μ ν© μ¦κ±°κ° λͺ
μ μ¦κ±°λ₯Ό μ΄κ²¨ haiku νλ‘νμΌμ΄ μΈμ
λͺ¨λΈλ‘ νμ λκ³€ νμ΅λλ€(μ€μΈ‘ 16건 λ 1건, ν©μμ¨ μ νν 80%). μ΄μ **λͺ
μ μ¦κ±°(`Task(model:)` νλΌλ―Έν°Β·μμ΄μ νΈ μ μ frontmatter)μ μ ν© μ¦κ±°(sidechain νλκ·Έ λΆμ¬)λ₯Ό λΆλ¦¬ν΄ μ§κ³**νκ³ , λͺ
μ μ¦κ±°κ° μ΄λ₯Ό λ°λ°νλ©΄ μ ν© μΆλ‘ μ μ±ννμ§ μμ΅λλ€. νμ μ΄ μ λλ©΄ `unknown`μΌλ‘ λ¨μ μ§κ³μμ λΉ μ§λλ€.
|
|
263
268
|
|
|
264
269
|
### v3.10.0 (2026-08-20)
|
package/bin/cli.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* npx claude-token-saver --statusline --no-color # strip ANSI colors
|
|
19
19
|
* npx claude-token-saver --statusline --icon # use π§ β³ π° icons
|
|
20
20
|
* npx claude-token-saver --statusline --no-timer # hide the TTL countdown
|
|
21
|
+
* npx claude-token-saver --statusline --single-line # legacy 1-line layout (no routing-totals headline)
|
|
21
22
|
* npx claude-token-saver --statusline --exclude-session <path>
|
|
22
23
|
* # exclude a JSONL path from lastActivity
|
|
23
24
|
* # (or set CACHE_MONITOR_EXCLUDE_SESSION env var)
|
|
@@ -231,6 +232,7 @@ async function main() {
|
|
|
231
232
|
timer: showTimer,
|
|
232
233
|
mode: isIcon ? 'icon' : 'text',
|
|
233
234
|
segments,
|
|
235
|
+
singleLine: hasFlag('--single-line'),
|
|
234
236
|
});
|
|
235
237
|
// For `cycle` mode, prefix with the scenario label so the screen recorder
|
|
236
238
|
// shows what the viewer is looking at (only when explicitly requested).
|
|
@@ -443,6 +445,15 @@ async function main() {
|
|
|
443
445
|
} catch (e) {
|
|
444
446
|
debug('model-rules:saved', e); // an unreadable registry just hides the chip
|
|
445
447
|
}
|
|
448
|
+
// Rolling week/month/lifetime totals from the delegation ledger β the
|
|
449
|
+
// statusline's headline line. Falls back to null (chip hidden) on any error.
|
|
450
|
+
let delegationTotals = null;
|
|
451
|
+
try {
|
|
452
|
+
const { delegationSavedTotals } = await import('../src/savings-ledger.js');
|
|
453
|
+
delegationTotals = delegationSavedTotals();
|
|
454
|
+
} catch (e) {
|
|
455
|
+
debug('savings-ledger:totals', e);
|
|
456
|
+
}
|
|
446
457
|
|
|
447
458
|
const data = {
|
|
448
459
|
summary: sum,
|
|
@@ -459,6 +470,7 @@ async function main() {
|
|
|
459
470
|
caps,
|
|
460
471
|
model,
|
|
461
472
|
delegationSaved,
|
|
473
|
+
delegationTotals,
|
|
462
474
|
};
|
|
463
475
|
|
|
464
476
|
let output;
|
|
@@ -505,6 +517,10 @@ async function main() {
|
|
|
505
517
|
timer: showTimer,
|
|
506
518
|
mode: isIcon ? 'icon' : 'text',
|
|
507
519
|
segments,
|
|
520
|
+
// macOS builds of Claude Code have rendered only the first line of a
|
|
521
|
+
// multi-line statusline in some versions (anthropics/claude-code#35176)
|
|
522
|
+
// β --single-line restores the legacy one-line layout in that case.
|
|
523
|
+
singleLine: hasFlag('--single-line'),
|
|
508
524
|
});
|
|
509
525
|
} else {
|
|
510
526
|
const { formatReport } = await import('../src/formatters/table.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet β post-hoc session analysis, no realtime router, no extra LLM calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/harness.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function run({ args, hasFlag }) {
|
|
|
27
27
|
}
|
|
28
28
|
return dflt;
|
|
29
29
|
};
|
|
30
|
-
const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessPull, harnessListRules, harnessRmRule, harnessPrune, ratchetSizeStatus, RATCHET_TOKEN_BUDGET, findProjectRoot } =
|
|
30
|
+
const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessPull, harnessListRules, harnessRmRule, harnessPrune, ratchetSizeStatus, RATCHET_TOKEN_BUDGET, contextWeightStatus, CLAUDE_MD_TOKEN_BUDGET, findProjectRoot } =
|
|
31
31
|
await import('../harness.js');
|
|
32
32
|
const { HARNESS_SECTIONS } = await import('../harness-templates.js');
|
|
33
33
|
const { loadConfig, saveConfig, userLanguage } = await import('../config.js');
|
|
@@ -77,6 +77,24 @@ export async function run({ args, hasFlag }) {
|
|
|
77
77
|
console.log(`${line}`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
// Advisory context-weight facts (never counted in π
· N/5): CLAUDE.md is
|
|
81
|
+
// charged on every request, and without a .claudeignore Claude Code can
|
|
82
|
+
// pull build artifacts and vendored code into context during searches.
|
|
83
|
+
try {
|
|
84
|
+
const w = contextWeightStatus({ root });
|
|
85
|
+
if (w.claudeMd) {
|
|
86
|
+
const line = `CLAUDE.md size: ~${w.claudeMd.tokens} tok/request (${w.claudeMd.path})`;
|
|
87
|
+
if (w.claudeMd.overBudget) {
|
|
88
|
+
console.log(`\nβ ${line} β over the ~${CLAUDE_MD_TOKEN_BUDGET} token guideline.`);
|
|
89
|
+
console.log(' Keep rules and file pointers here; move documentation into files it points to.');
|
|
90
|
+
} else {
|
|
91
|
+
console.log(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
console.log(`.claudeignore: ${w.hasClaudeIgnore ? 'present' : 'absent β consider adding one so searches skip build output, vendored code, and large data files'}`);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
debug('harness:context-weight', e);
|
|
97
|
+
}
|
|
80
98
|
return;
|
|
81
99
|
}
|
|
82
100
|
|
|
@@ -212,8 +212,9 @@ export function formatNoSession({ caps = null, model = null, windowLabel = '' }
|
|
|
212
212
|
* @param {boolean} [opts.timer=true] - show TTL countdown segment
|
|
213
213
|
* @param {'text'|'icon'} [opts.mode='text'] - label style. 'icon' uses π§ β³ π° instead of word labels.
|
|
214
214
|
* @param {string[]|null} [opts.segments] - whitelist of segments to render. Names: cap-warn, spike, harness, model, hit, ttl, saved, delegated, ctx, period, plus per-window keys (`five_hour`, `seven_day`, β¦). `5h`/`7d` are kept as aliases for back-compat. Null/undefined = all.
|
|
215
|
+
* @param {boolean} [opts.singleLine=false] - force the legacy one-line layout. By default, when the delegation ledger has lifetime savings, the routing totals lead on their own first line and everything else moves to line 2 (Claude Code renders multi-line statuslines; `--single-line` is the escape hatch for terminals that only show the first line).
|
|
215
216
|
*/
|
|
216
|
-
export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null } = {}) {
|
|
217
|
+
export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null, singleLine = false } = {}) {
|
|
217
218
|
const { summary, ttl, cost, options, lastActivity, contextWindow, ctxLive, spikeChip, caps, model } = data;
|
|
218
219
|
const { hitRate } = summary;
|
|
219
220
|
|
|
@@ -260,18 +261,37 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
260
261
|
// tier saved, summed from the rule registry route-scan maintains. Hidden
|
|
261
262
|
// when zero or absent: a permanent "$0" is noise for direct-API users and
|
|
262
263
|
// for anyone who has not delegated yet.
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
264
|
+
// "Routing saved" says what earned the money β work that ran on a cheaper
|
|
265
|
+
// model instead of this one. It leads the line rather than trailing it
|
|
266
|
+
// because it is the headline number of the whole tool, not a footnote.
|
|
267
|
+
// text: "Routing saved $3.2" | same in verbose
|
|
268
|
+
// icon: "π $3.2" | verbose: "π Routing saved $3.2"
|
|
267
269
|
const delegationSaved = Number(data.delegationSaved) || 0;
|
|
268
270
|
const delegateLabel = isIcon
|
|
269
|
-
? (verbose ? 'π
|
|
270
|
-
: '
|
|
271
|
+
? (verbose ? 'π Routing saved' : 'π')
|
|
272
|
+
: 'Routing saved';
|
|
271
273
|
const delegateSeg = delegationSaved > 0
|
|
272
274
|
? `${c(GREEN)}${delegateLabel}${c(RESET)} ${formatMoney(delegationSaved)}`
|
|
273
275
|
: null;
|
|
274
276
|
|
|
277
|
+
// Routing-totals headline line (multi-line layout). Week/month/lifetime
|
|
278
|
+
// sums from the delegation ledger β the number the whole tool exists to
|
|
279
|
+
// grow, so it gets line 1 to itself while the diagnostics move to line 2.
|
|
280
|
+
// icon: "π Routing saved $1.2 wk Β· $3.4 mo Β· $9.8 all"
|
|
281
|
+
// text: "Routing saved $1.2 wk Β· $3.4 mo Β· $9.8 all"
|
|
282
|
+
const totals = data.delegationTotals;
|
|
283
|
+
let totalsLine = null;
|
|
284
|
+
if (!singleLine && totals && Number(totals.total) > 0) {
|
|
285
|
+
const part = (usd, label) =>
|
|
286
|
+
`${formatMoney(Number(usd) || 0)} ${c(GRAY)}${label}${c(RESET)}`;
|
|
287
|
+
const head = isIcon ? 'π Routing saved' : 'Routing saved';
|
|
288
|
+
totalsLine =
|
|
289
|
+
`${c(GREEN)}${c(BOLD)}${head}${c(RESET)} ` +
|
|
290
|
+
`${part(totals.week, 'wk')} ${c(GRAY)}Β·${c(RESET)} ` +
|
|
291
|
+
`${part(totals.month, 'mo')} ${c(GRAY)}Β·${c(RESET)} ` +
|
|
292
|
+
`${part(totals.total, 'all')}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
275
295
|
// Period label honors hour-precision configs (`mode 6h` β "6h", `mode 1d` β "1d").
|
|
276
296
|
// Fall back to legacy `${days}d` when callers haven't supplied a label.
|
|
277
297
|
const periodLabel = options.windowLabel || `${options.days}d`;
|
|
@@ -489,6 +509,12 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
489
509
|
if (spikeSeg && want('spike')) segs.push(spikeSeg);
|
|
490
510
|
if (harnessSeg && want('harness')) segs.push(harnessSeg);
|
|
491
511
|
if (modelSeg && want('model')) segs.push(modelSeg);
|
|
512
|
+
// Delegation savings ride up front, next to the model that would otherwise
|
|
513
|
+
// have done the work. "Cache saved" stays at the tail: it is a lifetime brag
|
|
514
|
+
// stat, while this one is the point of the tool.
|
|
515
|
+
// When the totals headline owns line 1, the inline session chip would
|
|
516
|
+
// repeat the same story on line 2 β drop it there.
|
|
517
|
+
if (delegateSeg && want('delegated') && !totalsLine) segs.push(delegateSeg);
|
|
492
518
|
if (want('hit')) segs.push(hitSeg);
|
|
493
519
|
if (want('ttl')) segs.push(ttlSeg);
|
|
494
520
|
for (const { key, seg } of usageSegs) {
|
|
@@ -499,12 +525,15 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
499
525
|
// it sits near the tail. The period label closes the line as a quiet
|
|
500
526
|
// timeframe footer.
|
|
501
527
|
if (want('saved')) segs.push(saveSeg);
|
|
502
|
-
// Both savings numbers sit side by side, then the period label closes the line.
|
|
503
|
-
if (delegateSeg && want('delegated')) segs.push(delegateSeg);
|
|
504
528
|
if (want('period')) segs.push(periodSeg);
|
|
505
529
|
// Trailing erase-to-end-of-line so any leftover characters from a previous
|
|
506
530
|
// (longer) statusline render don't bleed into ours. \x1b[K is the standard
|
|
507
531
|
// "erase from cursor to EOL" CSI. Only emitted when color (i.e. ANSI) is
|
|
508
532
|
// allowed β --no-color/NO_COLOR consumers expect escape-free output.
|
|
509
|
-
|
|
533
|
+
const eol = color ? '\x1b[K' : '';
|
|
534
|
+
const rest = segs.join(' Β· ') + eol;
|
|
535
|
+
if (totalsLine && want('delegated')) {
|
|
536
|
+
return totalsLine + eol + '\n' + rest;
|
|
537
|
+
}
|
|
538
|
+
return rest;
|
|
510
539
|
}
|
package/src/harness.js
CHANGED
|
@@ -431,6 +431,35 @@ export function ratchetSizeStatus({ root = findProjectRoot(), scope = 'project'
|
|
|
431
431
|
return { path, count: rules.length, bytes, tokens, overBudget: tokens > RATCHET_TOKEN_BUDGET };
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
// CLAUDE.md itself is also charged on every request. The community guideline
|
|
435
|
+
// is "rules and file pointers, not documentation" β the harness block plus a
|
|
436
|
+
// modest project section fits well under this; docs pasted wholesale do not.
|
|
437
|
+
export const CLAUDE_MD_TOKEN_BUDGET = 4000;
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Advisory size/ignore facts for `harness check`:
|
|
441
|
+
* - approx token weight of the project (or global) CLAUDE.md
|
|
442
|
+
* - whether the project has a .claudeignore (limits what Claude Code will
|
|
443
|
+
* read into context during searches)
|
|
444
|
+
* Purely informational β never affects the π
· N/5 score.
|
|
445
|
+
*/
|
|
446
|
+
export function contextWeightStatus({ root = findProjectRoot() } = {}) {
|
|
447
|
+
const out = { claudeMd: null, hasClaudeIgnore: false };
|
|
448
|
+
try {
|
|
449
|
+
out.hasClaudeIgnore = existsSync(join(root, '.claudeignore'));
|
|
450
|
+
} catch { /* fs error β treat as absent */ }
|
|
451
|
+
for (const file of [join(root, 'CLAUDE.md'), join(homedir(), '.claude', 'CLAUDE.md')]) {
|
|
452
|
+
try {
|
|
453
|
+
if (!existsSync(file)) continue;
|
|
454
|
+
const bytes = statSync(file).size;
|
|
455
|
+
const tokens = Math.round(bytes / 4);
|
|
456
|
+
out.claudeMd = { path: file, bytes, tokens, overBudget: tokens > CLAUDE_MD_TOKEN_BUDGET };
|
|
457
|
+
break; // project file wins; global only as fallback
|
|
458
|
+
} catch { /* unreadable β skip */ }
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
|
|
434
463
|
/**
|
|
435
464
|
* harness prune β move rules out of ratchet.md into ratchet-archive.md next to
|
|
436
465
|
* it. Selection is by tag and/or age; nothing is deleted, so a pruned rule can
|
package/src/model-alias.js
CHANGED
|
@@ -44,8 +44,10 @@ export function profileMapPath() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// `converse/` (LiteLLM) or a bare ARN, foundation- or application-scoped.
|
|
47
|
+
// `foundation-model` ARNs carry the model id directly in the resource part
|
|
48
|
+
// (`foundation-model/anthropic.claude-haiku-β¦`), so they are captured too.
|
|
47
49
|
const ARN_RE =
|
|
48
|
-
/arn:aws:bedrock:[^:]*:[^:]*:(?:application-)?inference-profile\/([A-Za-z0-9._:-]+)/;
|
|
50
|
+
/arn:aws:bedrock:[^:]*:[^:]*:(?:foundation-model|(?:application-)?inference-profile)\/([A-Za-z0-9._:-]+)/;
|
|
49
51
|
|
|
50
52
|
/** True when the id came from a Bedrock gateway rather than the Claude API. */
|
|
51
53
|
export function isGatewayModelId(model) {
|
|
@@ -158,6 +160,16 @@ export function resolveModelAlias(rawModel, { env = process.env } = {}) {
|
|
|
158
160
|
const pid = profileIdFrom(model);
|
|
159
161
|
if (!pid) return UNKNOWN_MODEL;
|
|
160
162
|
|
|
163
|
+
// Self-describing resource ids need no learning: foundation-model ARNs and
|
|
164
|
+
// system cross-region inference profiles both embed the model id
|
|
165
|
+
// (`anthropic.claude-haiku-4-5-β¦` / `us.anthropic.claude-haiku-4-5-β¦`).
|
|
166
|
+
// detectPricingTier() already classifies those strings, region prefix
|
|
167
|
+
// included β only opaque application-profile ids (random hex) fall through
|
|
168
|
+
// to the override / learned paths. Without this, a fresh gateway machine
|
|
169
|
+
// reported every run as 'unknown' until enough sessions accumulated to
|
|
170
|
+
// learn a mapping that the string had spelled out all along.
|
|
171
|
+
if (/claude/i.test(pid)) return pid;
|
|
172
|
+
|
|
161
173
|
const entry = map.learned?.[pid];
|
|
162
174
|
if (entry?.role) {
|
|
163
175
|
const alias = aliasForRole(entry.role, env);
|
package/src/route-scan.js
CHANGED
|
@@ -451,6 +451,10 @@ export async function runRouteScan({ days = 14 } = {}) {
|
|
|
451
451
|
d.savedUsd += saved;
|
|
452
452
|
delegatedStats.set(key, d);
|
|
453
453
|
};
|
|
454
|
+
// Ledger events feed the statusline's weekly/monthly "Routing saved"
|
|
455
|
+
// totals. Keyed by run transcript path so overlapping re-scans upsert the
|
|
456
|
+
// same event instead of double-counting it.
|
|
457
|
+
const ledgerEvents = [];
|
|
454
458
|
for (const [sessionPath, index] of runIndexBySession) {
|
|
455
459
|
const used = new Set();
|
|
456
460
|
for (const { ep, projectDir, sessionPath: epSession } of all) {
|
|
@@ -474,9 +478,22 @@ export async function runRouteScan({ days = 14 } = {}) {
|
|
|
474
478
|
const saved = runSaving(run, mainModel);
|
|
475
479
|
bumpDelegated(`${runTier}|${cat.id}|${projectDir}`, run, saved);
|
|
476
480
|
bumpDelegated(`${runTier}|${cat.id}|*`, run, saved);
|
|
481
|
+
if (saved > 0) {
|
|
482
|
+
ledgerEvents.push({
|
|
483
|
+
key: run.path,
|
|
484
|
+
ts: run.endedAt ?? run.startedAt ?? Date.now(),
|
|
485
|
+
usd: saved,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
477
488
|
}
|
|
478
489
|
}
|
|
479
490
|
}
|
|
491
|
+
try {
|
|
492
|
+
const { recordDelegationEvents } = await import('./savings-ledger.js');
|
|
493
|
+
recordDelegationEvents(ledgerEvents);
|
|
494
|
+
} catch {
|
|
495
|
+
// ledger write failure only delays the statusline totals, never the scan
|
|
496
|
+
}
|
|
480
497
|
|
|
481
498
|
// Keep prior dismissed/promoted signatures across rescans.
|
|
482
499
|
const prev = readRouteScan();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* savings-ledger β per-run delegation saving events with timestamps.
|
|
3
|
+
*
|
|
4
|
+
* model-rules.json stores `savedUsd` as a scan-window snapshot per rule, which
|
|
5
|
+
* is right for rule-health but cannot answer "how much did routing save this
|
|
6
|
+
* week / this month / ever". This ledger keeps one event per subagent run,
|
|
7
|
+
* keyed by the run's transcript path so re-scans over overlapping windows
|
|
8
|
+
* upsert instead of double-counting.
|
|
9
|
+
*
|
|
10
|
+
* File: <userDataDir>/delegation-ledger.json
|
|
11
|
+
* { "events": { "<run path>": { "ts": <ms epoch>, "usd": <number> } } }
|
|
12
|
+
*
|
|
13
|
+
* Best-effort like every other state file here: an unreadable ledger reads as
|
|
14
|
+
* empty, and the statusline renders totals of 0 as "no chip".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { userDataDir } from './paths.js';
|
|
20
|
+
|
|
21
|
+
const WEEK_MS = 7 * 24 * 3600 * 1000;
|
|
22
|
+
const MONTH_MS = 30 * 24 * 3600 * 1000;
|
|
23
|
+
|
|
24
|
+
export function ledgerPath() {
|
|
25
|
+
return join(userDataDir(), 'delegation-ledger.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadLedger() {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(readFileSync(ledgerPath(), 'utf8'));
|
|
31
|
+
return data && typeof data.events === 'object' && data.events !== null
|
|
32
|
+
? data
|
|
33
|
+
: { events: {} };
|
|
34
|
+
} catch {
|
|
35
|
+
return { events: {} };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Upsert saving events. `events` is an array of { key, ts, usd } where `key`
|
|
41
|
+
* is the run's transcript path (unique per subagent run). Zero-saving runs
|
|
42
|
+
* are skipped β they carry no information the totals care about.
|
|
43
|
+
*/
|
|
44
|
+
export function recordDelegationEvents(events) {
|
|
45
|
+
if (!Array.isArray(events) || events.length === 0) return;
|
|
46
|
+
const data = loadLedger();
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const e of events) {
|
|
49
|
+
if (!e || !e.key || !(Number(e.usd) > 0) || !Number.isFinite(e.ts)) continue;
|
|
50
|
+
const prev = data.events[e.key];
|
|
51
|
+
const usd = Math.round(Number(e.usd) * 10000) / 10000;
|
|
52
|
+
if (prev && prev.ts === e.ts && prev.usd === usd) continue;
|
|
53
|
+
data.events[e.key] = { ts: e.ts, usd };
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
if (!changed) return;
|
|
57
|
+
try {
|
|
58
|
+
const dir = userDataDir();
|
|
59
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
60
|
+
writeFileSync(ledgerPath(), JSON.stringify(data) + '\n');
|
|
61
|
+
} catch {
|
|
62
|
+
// statusline totals just stay stale until the next successful scan
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Rolling totals: last 7 days, last 30 days, and lifetime. `now` is injectable
|
|
68
|
+
* for tests. Never throws β an unreadable ledger yields all-zero totals.
|
|
69
|
+
*/
|
|
70
|
+
export function delegationSavedTotals(now = Date.now()) {
|
|
71
|
+
const totals = { week: 0, month: 0, total: 0 };
|
|
72
|
+
try {
|
|
73
|
+
for (const e of Object.values(loadLedger().events)) {
|
|
74
|
+
const usd = Number(e.usd) || 0;
|
|
75
|
+
if (usd <= 0) continue;
|
|
76
|
+
totals.total += usd;
|
|
77
|
+
if (Number.isFinite(e.ts)) {
|
|
78
|
+
if (now - e.ts <= WEEK_MS) totals.week += usd;
|
|
79
|
+
if (now - e.ts <= MONTH_MS) totals.month += usd;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return { week: 0, month: 0, total: 0 };
|
|
84
|
+
}
|
|
85
|
+
return totals;
|
|
86
|
+
}
|