claude-token-saver 3.31.0 → 3.34.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 +72 -4
- package/README.md +72 -4
- package/bin/cli.js +12 -4
- package/package.json +1 -1
- package/presets/model-rules.json +115 -0
- package/presets/ratchet-rules.json +38 -0
- package/src/advice.js +8 -5
- package/src/commands/install.js +107 -2
- package/src/commands/route-scan.js +25 -2
- package/src/commands/seed.js +105 -0
- package/src/config.js +25 -0
- package/src/demo.js +8 -7
- package/src/formatters/statusline.js +3 -1
- package/src/formatters/table.js +7 -5
- package/src/harness.js +21 -8
- package/src/history.js +6 -3
- package/src/installer.js +5 -4
- package/src/korean-style.js +11 -3
- package/src/model-rules.js +34 -5
- package/src/route-scan.js +6 -7
- package/src/seed-rules.js +280 -0
- package/src/stats.js +17 -4
- package/presets/ratchet-rules.md +0 -16
|
@@ -137,9 +137,17 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
137
137
|
const health = r.status === 'review'
|
|
138
138
|
? (lang === 'ko' ? ' ⚠ 에러율 초과 — 재검토 필요' : ' ⚠ error rate over threshold — needs review')
|
|
139
139
|
: '';
|
|
140
|
+
// A seeded rule has no recurrence of its own until a scan measures one,
|
|
141
|
+
// so "반복 0회 / seen ×0" would misreport a curated preset as work that
|
|
142
|
+
// never happens.
|
|
143
|
+
const evidence = r.origin === 'preset' && !r.count
|
|
144
|
+
? (lang === 'ko' ? '동봉 프리셋 (로그 누적 전)' : 'bundled preset (no local history yet)')
|
|
145
|
+
: (lang === 'ko'
|
|
146
|
+
? `반복 ${r.count || 0}회 · 에러율 ${Math.round((r.errRate || 0) * 100)}%`
|
|
147
|
+
: `seen ×${r.count || 0} · err ${Math.round((r.errRate || 0) * 100)}%`);
|
|
140
148
|
const stat = lang === 'ko'
|
|
141
|
-
? `${r.tier} (${rs.tierLabel(r.tier)}) · ${rs.scopeLabel(r.scope)} ·
|
|
142
|
-
: `${r.tier} (${rs.tierLabel(r.tier, 'en')}) · ${rs.scopeLabel(r.scope, 'en')} ·
|
|
149
|
+
? `${r.tier} (${rs.tierLabel(r.tier)}) · ${rs.scopeLabel(r.scope)} · ${evidence}`
|
|
150
|
+
: `${r.tier} (${rs.tierLabel(r.tier, 'en')}) · ${rs.scopeLabel(r.scope, 'en')} · ${evidence}`;
|
|
143
151
|
console.log(` #${i + 1} ${stat}${health}`);
|
|
144
152
|
// Measured outcome of the rule actually firing, plus what it saved.
|
|
145
153
|
// A rule with no measured delegations shows "—", never "$0.00": the
|
|
@@ -254,6 +262,19 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
254
262
|
if (doc2mdHookRegistered()) doc2mdBlock = require('../doc2md.cjs').sessionNote(lang);
|
|
255
263
|
} catch (e) { debug('route-scan:doc2md-note', e); /* the note is optional */ }
|
|
256
264
|
|
|
265
|
+
// Starter rules that the user has not answered yet (bundled presets, both
|
|
266
|
+
// model-fitting and ratchet). A fresh install has an empty model ratchet
|
|
267
|
+
// and an empty ratchet.md, so without this the tool delegates nothing
|
|
268
|
+
// until the user's own history is long enough to propose a candidate.
|
|
269
|
+
// The asking happens here because a CLI prompt cannot reach a user whose
|
|
270
|
+
// only interface is the chat: the model gets the list and asks one rule
|
|
271
|
+
// at a time. Silent — and free — once everything has an answer.
|
|
272
|
+
let seedBlock = null;
|
|
273
|
+
try {
|
|
274
|
+
const { seedOfferBlock } = await import('../seed-rules.js');
|
|
275
|
+
seedBlock = seedOfferBlock({ lang });
|
|
276
|
+
} catch (e) { debug('route-scan:seed-offer', e); /* the offer is optional */ }
|
|
277
|
+
|
|
257
278
|
// Upgrade offer. A statusline cannot open a dialog, so session start is
|
|
258
279
|
// where the *asking* happens: the model gets one line telling it a newer
|
|
259
280
|
// version exists and to ask before installing anything. Cached read only
|
|
@@ -288,11 +309,13 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
288
309
|
|
|
289
310
|
if (open.length === 0 && reviewRules.length === 0) {
|
|
290
311
|
if (updateBlock) console.log(updateBlock);
|
|
312
|
+
if (seedBlock) console.log(seedBlock);
|
|
291
313
|
if (doc2mdBlock) console.log(doc2mdBlock);
|
|
292
314
|
if (koreanBlock) console.log(koreanBlock);
|
|
293
315
|
return; // nothing else to inject
|
|
294
316
|
}
|
|
295
317
|
if (updateBlock) console.log(updateBlock);
|
|
318
|
+
if (seedBlock) console.log(seedBlock);
|
|
296
319
|
if (doc2mdBlock) console.log(doc2mdBlock);
|
|
297
320
|
// This text is injected straight into the model's context, so it must
|
|
298
321
|
// follow the user's configured language — a Korean-only briefing in an
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subcommand: seed — register the bundled starter rules, one answer at a time.
|
|
3
|
+
*
|
|
4
|
+
* claude-token-saver seed # what is still pending
|
|
5
|
+
* claude-token-saver seed accept <id> --global # register one rule
|
|
6
|
+
* claude-token-saver seed accept <id> --project # ... into this project only
|
|
7
|
+
* claude-token-saver seed accept all --global # when the user says "all of them"
|
|
8
|
+
* claude-token-saver seed skip <id> | skip all # never offer it again
|
|
9
|
+
* claude-token-saver seed reset # make every preset pending again
|
|
10
|
+
*
|
|
11
|
+
* Scope is explicit on purpose, exactly as `harness promote` requires it: the
|
|
12
|
+
* hook environment is non-TTY, so the model has to ask the user and pass the
|
|
13
|
+
* flag rather than let a default decide where a rule lands.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export async function run({ args, hasFlag }) {
|
|
17
|
+
const sub = args[1];
|
|
18
|
+
const seed = await import('../seed-rules.js');
|
|
19
|
+
const { userLanguage } = await import('../config.js');
|
|
20
|
+
const { findProjectRoot } = await import('../harness.js');
|
|
21
|
+
const lang = userLanguage();
|
|
22
|
+
const ko = lang === 'ko';
|
|
23
|
+
const root = findProjectRoot();
|
|
24
|
+
|
|
25
|
+
const scopeFlag = () => (hasFlag('--global') ? 'global' : hasFlag('--project') ? 'project' : null);
|
|
26
|
+
|
|
27
|
+
if (sub === 'reset') {
|
|
28
|
+
const n = seed.resetSeeds();
|
|
29
|
+
console.log(ko
|
|
30
|
+
? `기록된 응답 ${n}건을 지웠습니다 — 프리셋 전체가 다시 제안 대상이 됩니다.`
|
|
31
|
+
: `Cleared ${n} recorded answer(s) — every preset is pending again.`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (sub === 'accept' || sub === 'skip') {
|
|
36
|
+
const ids = args.slice(2).filter((a) => !a.startsWith('-'));
|
|
37
|
+
if (ids.length === 0) {
|
|
38
|
+
console.error(ko
|
|
39
|
+
? 'id를 지정하십시오 (목록: claude-token-saver seed). 전체는 `all`.'
|
|
40
|
+
: 'Pass an id (list them with `claude-token-saver seed`), or `all`.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const pending = seed.pendingSeeds({ lang, root });
|
|
44
|
+
const targets = ids.includes('all') ? pending.map((s) => s.id) : ids;
|
|
45
|
+
|
|
46
|
+
if (sub === 'skip') {
|
|
47
|
+
for (const id of targets) {
|
|
48
|
+
const r = seed.skipSeed(id, { lang, root });
|
|
49
|
+
console.log(r
|
|
50
|
+
? (ko ? `건너뜀: ${id}` : `skipped: ${id}`)
|
|
51
|
+
: (ko ? `대기 중인 프리셋이 아닙니다: ${id}` : `not a pending preset: ${id}`));
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const scope = scopeFlag();
|
|
57
|
+
if (!scope) {
|
|
58
|
+
console.error(ko
|
|
59
|
+
? '적용 범위를 명시하십시오 (사용자에게 먼저 확인): --global 또는 --project'
|
|
60
|
+
: 'Scope required (confirm it with the user first): --global or --project');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
for (const id of targets) {
|
|
64
|
+
const r = await seed.acceptSeed(id, { scope, root, lang });
|
|
65
|
+
if (!r) {
|
|
66
|
+
console.log(ko ? `대기 중인 프리셋이 아닙니다: ${id}` : `not a pending preset: ${id}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
console.log(ko
|
|
70
|
+
? `✅ 등록: ${id} [${scope === 'global' ? '전체 프로젝트' : '이 프로젝트'}]`
|
|
71
|
+
: `✅ registered: ${id} [${scope}]`);
|
|
72
|
+
console.log(` ${r.rule}`);
|
|
73
|
+
for (const p of r.paths || (r.path ? [r.path] : [])) console.log(` → ${p}`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Default: status. Lists what is pending and what was already answered, so a
|
|
79
|
+
// user who said no can see that they did and reverse it with `reset`.
|
|
80
|
+
const pending = seed.pendingSeeds({ lang, root });
|
|
81
|
+
const { decided } = seed.loadSeedState();
|
|
82
|
+
const answered = Object.entries(decided);
|
|
83
|
+
console.log(ko
|
|
84
|
+
? `seed — 동봉 프리셋 중 대기 ${pending.length}건, 응답 완료 ${answered.length}건`
|
|
85
|
+
: `seed — ${pending.length} preset(s) pending, ${answered.length} already answered`);
|
|
86
|
+
for (const s of pending) {
|
|
87
|
+
const head = s.kind === 'model'
|
|
88
|
+
? `[${s.id}] ${s.tier} · "${s.label}" → ${s.agent}`
|
|
89
|
+
: `[${s.id}] ${ko ? '랫쳇 룰' : 'ratchet rule'}`;
|
|
90
|
+
console.log(` ${head}`);
|
|
91
|
+
console.log(` ${s.ruleText}`);
|
|
92
|
+
}
|
|
93
|
+
if (answered.length > 0) {
|
|
94
|
+
console.log(ko ? ' 응답 기록:' : ' answered:');
|
|
95
|
+
for (const [id, d] of answered) {
|
|
96
|
+
console.log(` ${id}: ${d.action}${d.scope ? ` (${d.scope})` : ''} — ${d.at}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (pending.length > 0) {
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(ko
|
|
102
|
+
? '등록: claude-token-saver seed accept <id> --global|--project · 거절: seed skip <id>'
|
|
103
|
+
: 'register: claude-token-saver seed accept <id> --global|--project · decline: seed skip <id>');
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/config.js
CHANGED
|
@@ -197,6 +197,31 @@ export function userLanguage() {
|
|
|
197
197
|
return v === 'ko' ? 'ko' : 'en';
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Whether the language is a recorded choice rather than the 'en' fallback.
|
|
202
|
+
*
|
|
203
|
+
* The install needs the distinction: a user who picked English must not be
|
|
204
|
+
* asked again on the next upgrade, and "nothing recorded" has to stay
|
|
205
|
+
* distinguishable from "chose en", which `userLanguage()` alone cannot say.
|
|
206
|
+
*/
|
|
207
|
+
export function languageDecided() {
|
|
208
|
+
const cfg = loadConfig();
|
|
209
|
+
const v = cfg.language || (cfg.statusline && cfg.statusline.language);
|
|
210
|
+
return v === 'ko' || v === 'en';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Record the output language ('ko' | 'en'). Anything else is ignored. */
|
|
214
|
+
export function setUserLanguage(v) {
|
|
215
|
+
const lang = v === 'ko' ? 'ko' : v === 'en' ? 'en' : null;
|
|
216
|
+
if (!lang) return null;
|
|
217
|
+
const cfg = loadConfig();
|
|
218
|
+
cfg.language = lang;
|
|
219
|
+
// The legacy slot would win on a later read for configs written by 2.9.x.
|
|
220
|
+
if (cfg.statusline) delete cfg.statusline.language;
|
|
221
|
+
saveConfig(cfg);
|
|
222
|
+
return lang;
|
|
223
|
+
}
|
|
224
|
+
|
|
200
225
|
/**
|
|
201
226
|
* Render hours as the most natural unit:
|
|
202
227
|
* 24h → "1d", 168h → "7d", 6h → "6h", 36h → "36h" (not whole days).
|
package/src/demo.js
CHANGED
|
@@ -99,15 +99,16 @@ const SCENARIOS = [
|
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: 'ctx-1m',
|
|
102
|
-
label: '⚠ Context past
|
|
102
|
+
label: '⚠ Context past 500k',
|
|
103
103
|
data: {
|
|
104
104
|
hitRate: 0.78,
|
|
105
105
|
pct1h: 0.92,
|
|
106
106
|
savings: 1340,
|
|
107
107
|
elapsedSec: 30,
|
|
108
108
|
contextSize: '1M',
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
ctxOverWarn: true,
|
|
110
|
+
ctxUsedPct: 62, // 62% of 1M ≈ 620k actually in context
|
|
111
|
+
spikeChip: '⚠ Ctx 500k+',
|
|
111
112
|
caps: HEALTHY_CAPS,
|
|
112
113
|
},
|
|
113
114
|
},
|
|
@@ -271,9 +272,9 @@ export function buildTableDemoData(options = {}) {
|
|
|
271
272
|
version: options.version ?? '',
|
|
272
273
|
},
|
|
273
274
|
spikeReport: { spikes, baseline: { p95: 940_000 } },
|
|
274
|
-
contextWindow: { size: '1M', maxContext:
|
|
275
|
+
contextWindow: { size: '1M', maxContext: 620_000, overWarn: true },
|
|
275
276
|
lastActivity: Date.now() - 60 * 1000,
|
|
276
|
-
spikeChip: '⚠ Ctx
|
|
277
|
+
spikeChip: '⚠ Ctx 500k+',
|
|
277
278
|
};
|
|
278
279
|
}
|
|
279
280
|
|
|
@@ -303,7 +304,7 @@ export function buildScenarioData(scenarioName, options) {
|
|
|
303
304
|
if (!scenario) return null;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
const { hitRate, pct1h, pct5m, savings, elapsedSec, contextSize, ctxUsedPct, spikeChip, caps } = scenario.data;
|
|
307
|
+
const { hitRate, pct1h, pct5m, savings, elapsedSec, contextSize, ctxOverWarn, ctxUsedPct, spikeChip, caps } = scenario.data;
|
|
307
308
|
return {
|
|
308
309
|
summary: { hitRate },
|
|
309
310
|
ttl: { pct1h, pct5m: pct5m ?? (1 - pct1h) },
|
|
@@ -315,7 +316,7 @@ export function buildScenarioData(scenarioName, options) {
|
|
|
315
316
|
version: options.version ?? '',
|
|
316
317
|
},
|
|
317
318
|
lastActivity: Date.now() - elapsedSec * 1000,
|
|
318
|
-
contextWindow: { size: contextSize },
|
|
319
|
+
contextWindow: { size: contextSize, overWarn: Boolean(ctxOverWarn) },
|
|
319
320
|
// Live fill level (`📦 68%`) — scenarios that set ctxUsedPct exercise the
|
|
320
321
|
// stdin-driven segment; the rest fall back to the size-based chip.
|
|
321
322
|
ctxLive: ctxUsedPct != null
|
|
@@ -545,7 +545,9 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
545
545
|
}
|
|
546
546
|
} else if (contextWindow && contextWindow.size && contextWindow.size !== 'unknown') {
|
|
547
547
|
const label = contextWindow.size === '1M' ? '1M' : '200k';
|
|
548
|
-
|
|
548
|
+
// Yellow is reserved for the 500k warn line. A 1M window by itself is the
|
|
549
|
+
// default on every current model, so colouring it yellow cried wolf.
|
|
550
|
+
const ctxColor = contextWindow.overWarn ? YELLOW : GREEN;
|
|
549
551
|
if (isIcon && verbose) {
|
|
550
552
|
ctxSeg = `${c(ctxColor)}📦 Ctx ${label}${c(RESET)}`;
|
|
551
553
|
} else if (isIcon) {
|
package/src/formatters/table.js
CHANGED
|
@@ -84,9 +84,9 @@ function renderSpikeSection(spikes, contextWindow) {
|
|
|
84
84
|
const lines = [];
|
|
85
85
|
lines.push(rbl(' ⚠ Token spike detected'));
|
|
86
86
|
lines.push(r(` ${'─'.repeat(50)}`));
|
|
87
|
-
if (contextWindow && contextWindow.
|
|
87
|
+
if (contextWindow && contextWindow.overWarn) {
|
|
88
88
|
lines.push(
|
|
89
|
-
r(` Context usage exceeded
|
|
89
|
+
r(` Context usage exceeded 500k (max recent single-request input ${formatContextSize(contextWindow.maxContext)} tokens)`),
|
|
90
90
|
);
|
|
91
91
|
lines.push('');
|
|
92
92
|
}
|
|
@@ -196,9 +196,11 @@ export function formatReport({ summary: sum, trend, ttl, anomalies, cost, option
|
|
|
196
196
|
// Context window chip for the normal case too
|
|
197
197
|
if (contextWindow && contextWindow.size !== 'unknown') {
|
|
198
198
|
const note =
|
|
199
|
-
contextWindow.
|
|
200
|
-
? '⚠ Context exceeded
|
|
201
|
-
:
|
|
199
|
+
contextWindow.overWarn
|
|
200
|
+
? '⚠ Context exceeded 500k in recent requests — big contexts re-bill every turn and drain the 5H/7D caps. Use /compact or /clear.'
|
|
201
|
+
: contextWindow.size === '1M'
|
|
202
|
+
? '✓ 1M window in use, under the 500k warn line'
|
|
203
|
+
: '✓ 200k context (standard)';
|
|
202
204
|
lines.push(` Context window: ${contextWindow.size} ${note}`);
|
|
203
205
|
lines.push(` (max recent single-request input ${formatContextSize(contextWindow.maxContext)} tokens)`);
|
|
204
206
|
lines.push('');
|
package/src/harness.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { routeWarningForStatusline } from './route-scan.js';
|
|
26
26
|
import { ruleHealthWarningForStatusline, modelRatchetPathFor, renderModelRatchet } from './model-rules.js';
|
|
27
27
|
import { compactWindowWarningForStatusline } from './compact-window.js';
|
|
28
|
+
import { userLanguage } from './config.js';
|
|
28
29
|
|
|
29
30
|
const require = createRequire(import.meta.url);
|
|
30
31
|
function readHarnessState() {
|
|
@@ -366,23 +367,35 @@ export function harnessPull({ root = findProjectRoot(), scope = 'global' } = {})
|
|
|
366
367
|
return result;
|
|
367
368
|
}
|
|
368
369
|
|
|
369
|
-
/**
|
|
370
|
-
|
|
370
|
+
/**
|
|
371
|
+
* The bundled preset rules, both languages per rule.
|
|
372
|
+
*
|
|
373
|
+
* These strings are injected into the model's context (by `seed`) and written
|
|
374
|
+
* into the user's ratchet (by `pull`), so a Korean-only set would pull an
|
|
375
|
+
* English session's responses into Korean — hence the pair. The `ko` text is
|
|
376
|
+
* canonical: it is what identifies a rule, so revising an English wording never
|
|
377
|
+
* turns a rule the user already answered into a new one.
|
|
378
|
+
*/
|
|
379
|
+
export function presetRuleEntries() {
|
|
371
380
|
try {
|
|
372
381
|
// fileURLToPath, not `.pathname`: the latter yields `/D:/repo/src` on
|
|
373
382
|
// Windows, where the read fails and the catch below turns a missing
|
|
374
383
|
// file into an empty rule set without ever saying so.
|
|
375
|
-
const path = join(dirname(fileURLToPath(import.meta.url)), '..', 'presets', 'ratchet-rules.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
.filter((
|
|
379
|
-
.map((
|
|
380
|
-
.filter(Boolean);
|
|
384
|
+
const path = join(dirname(fileURLToPath(import.meta.url)), '..', 'presets', 'ratchet-rules.json');
|
|
385
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
386
|
+
return (Array.isArray(data.rules) ? data.rules : [])
|
|
387
|
+
.filter((r) => r && typeof r.ko === 'string' && r.ko.trim())
|
|
388
|
+
.map((r) => ({ ko: r.ko.trim(), en: (r.en || r.ko).trim() }));
|
|
381
389
|
} catch {
|
|
382
390
|
return [];
|
|
383
391
|
}
|
|
384
392
|
}
|
|
385
393
|
|
|
394
|
+
/** Preset rule text in the user's configured language. */
|
|
395
|
+
export function presetRules(lang = userLanguage()) {
|
|
396
|
+
return presetRuleEntries().map((r) => (lang === 'ko' ? r.ko : r.en));
|
|
397
|
+
}
|
|
398
|
+
|
|
386
399
|
/**
|
|
387
400
|
* harness list — return numbered ratchet rules from .claude/ratchet.md.
|
|
388
401
|
* Numbering is 1-based and matches `harness rm <N>`.
|
package/src/history.js
CHANGED
|
@@ -69,7 +69,8 @@ function saveState(state) {
|
|
|
69
69
|
function chipKo(chip) {
|
|
70
70
|
if (!chip) return chip;
|
|
71
71
|
const map = {
|
|
72
|
-
'⚠ Ctx
|
|
72
|
+
'⚠ Ctx 500k+': '⚠ 컨텍스트 500k 초과',
|
|
73
|
+
'⚠ Ctx 200k+': '⚠ 컨텍스트 200k 초과', // legacy (pre-v3.32)
|
|
73
74
|
'⚠ 1M ON': '⚠ 1M 컨텍스트 활성', // legacy (pre-v2.18)
|
|
74
75
|
'⚠ Cache miss': '⚠ 캐시 미스',
|
|
75
76
|
'⚠ Rebuild churn': '⚠ 캐시 재빌드 빈발',
|
|
@@ -93,8 +94,10 @@ function chipKo(chip) {
|
|
|
93
94
|
*/
|
|
94
95
|
function detailKo(detail) {
|
|
95
96
|
if (!detail) return detail;
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
// The threshold moved from 200k to 500k in v3.32, so both spellings have to
|
|
98
|
+
// resolve: history files written by older versions still carry the old one.
|
|
99
|
+
const m0 = detail.match(/^Single-request context exceeded (200|500)k \(max (\d+)k tokens\)$/);
|
|
100
|
+
if (m0) return `단일 요청 컨텍스트 ${m0[1]}k 초과 (최대 ${m0[2]}k 토큰)`;
|
|
98
101
|
// legacy detail shape (pre-v2.18)
|
|
99
102
|
const m1 = detail.match(/^Context auto-promoted to 1M \(max single-request (\d+)k tokens\)$/);
|
|
100
103
|
if (m1) return `1M 컨텍스트 자동 활성 (단일 요청 최대 ${m1[1]}k 토큰)`;
|
package/src/installer.js
CHANGED
|
@@ -22,7 +22,7 @@ const STATUSLINE_REFRESH_INTERVAL = 5;
|
|
|
22
22
|
|
|
23
23
|
const SKILL_BODY = `---
|
|
24
24
|
name: claude-token-saver
|
|
25
|
-
description: Use when the user mentions Claude Code token usage, prompt cache hit rate, TTL/expiry, the 1M context window, cache misses, output spikes, rate-limit caps (5h/7d), or anything in the statusline produced by claude-token-saver (chips like "🚨 5H 94%", "🚨 7D 92%", "⚠
|
|
25
|
+
description: Use when the user mentions Claude Code token usage, prompt cache hit rate, TTL/expiry, the 1M context window, cache misses, output spikes, rate-limit caps (5h/7d), or anything in the statusline produced by claude-token-saver (chips like "🚨 5H 94%", "🚨 7D 92%", "⚠ Ctx 500k+", "⚠ Input spike", "⚠ Cache miss", "⚠ 5m TTL", "⚠ Rebuild churn", "⚠ Output heavy", "⚠ Call surge", "⏳ Cache expires", "💰 Cache saved", "🧠 Cache hit"). Also use when they ask to view token-usage history, want to understand a warning they just saw, or want to back up work before a session cap with \`claude-token-saver handoff\`.
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
# claude-token-saver — Claude Code Token Monitor
|
|
@@ -46,7 +46,7 @@ honor that for the rest of the turn without changing the saved setting.
|
|
|
46
46
|
## When this skill should activate
|
|
47
47
|
|
|
48
48
|
- The user references any chip wording: \`🚨 5H NN%\`, \`🚨 7D NN%\`,
|
|
49
|
-
\`⚠
|
|
49
|
+
\`⚠ Ctx 500k+\`, \`⚠ Input spike\`, \`⚠ Cache miss\`, \`⚠ 5m TTL\`,
|
|
50
50
|
\`⚠ Rebuild churn\`, \`⚠ Output heavy\`, \`⚠ Call surge\`.
|
|
51
51
|
- The user asks "why is my cache hit rate low", "what does this warning mean",
|
|
52
52
|
"when did this start happening", or similar.
|
|
@@ -80,7 +80,7 @@ honor that for the rest of the turn without changing the saved setting.
|
|
|
80
80
|
| ------------------ | ----------------------------------------------------- |
|
|
81
81
|
| \`🚨 5H NN%\` | 5-hour rate-limit window at NN% (>=90%). Cap is imminent. |
|
|
82
82
|
| \`🚨 7D NN%\` | 7-day rate-limit window at NN% (>=90%). Pace yourself. |
|
|
83
|
-
| \`⚠
|
|
83
|
+
| \`⚠ Ctx 500k+\` | A single recent request carried more than 500k input tokens. |
|
|
84
84
|
| \`⚠ Input spike\` | One request consumed >250k or >3× the recent p95. |
|
|
85
85
|
| \`⚠ Cache miss\` | Cache hit rate dropped below ~70%. |
|
|
86
86
|
| \`⚠ 5m TTL\` | Most cache writes are 5-min ephemeral (Pro plan default). |
|
|
@@ -96,7 +96,8 @@ honor that for the rest of the turn without changing the saved setting.
|
|
|
96
96
|
6. **Suggest the next action.** For \`🚨 5H/7D\` chips, recommend running
|
|
97
97
|
\`claude-token-saver handoff\` to back up the current work to a
|
|
98
98
|
\`HANDOFF-*.md\` file before the cap hits, then continue in a fresh
|
|
99
|
-
session. For
|
|
99
|
+
session. For \`⚠ Ctx 500k+\`, recommend \`/compact\` or \`/clear\` and
|
|
100
|
+
checking what is pinned into context. For
|
|
100
101
|
5m TTL, point at the Max plan's 1h bucket. For input spike, suggest
|
|
101
102
|
splitting the conversation or compacting context.
|
|
102
103
|
|
package/src/korean-style.js
CHANGED
|
@@ -67,13 +67,21 @@ export function koreanLocaleDetected({ env = process.env, platform = process.pla
|
|
|
67
67
|
try {
|
|
68
68
|
if (loadConfig().language === 'ko') return true;
|
|
69
69
|
} catch { /* unreadable config falls through to the env checks */ }
|
|
70
|
-
|
|
70
|
+
const posix = [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE].filter(
|
|
71
|
+
(v) => typeof v === 'string' && v.trim() && v !== 'C' && v !== 'POSIX',
|
|
72
|
+
);
|
|
73
|
+
for (const v of posix) {
|
|
71
74
|
// `ko` must be a whole subtag: `ko`, `ko_KR.UTF-8`, `ko-KR`, and the
|
|
72
75
|
// colon-separated `LANGUAGE=ko:en` all count, while `kok` (Konkani) and
|
|
73
76
|
// `tok` do not.
|
|
74
|
-
if (
|
|
77
|
+
if (/(^|[:._-])ko([:._-]|$)/i.test(v)) return true;
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
// A locale variable that is set and is NOT Korean is an answer, not a missing
|
|
80
|
+
// signal: `LANG=en_US.UTF-8` on a machine whose system locale is Korean means
|
|
81
|
+
// the user chose English for their shell. Only consult the system locale when
|
|
82
|
+
// the POSIX variables say nothing at all, which is the macOS GUI-shell case
|
|
83
|
+
// this fallback exists for.
|
|
84
|
+
if (platform === 'darwin' && posix.length === 0) {
|
|
77
85
|
try {
|
|
78
86
|
// `LANG` is commonly unset in macOS GUI-launched shells, so the system
|
|
79
87
|
// locale is the only reliable signal there.
|
package/src/model-rules.js
CHANGED
|
@@ -100,6 +100,27 @@ export function composeRuleText(baseText, rule, lang = userLanguage()) {
|
|
|
100
100
|
return `${baseText.replace(/\s*$/, '')}. ${clause}`;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The base rule sentence for one tier-delegation rule, without the budget
|
|
105
|
+
* clause (composeRuleText adds that). Lives here rather than in route-scan so
|
|
106
|
+
* that the scan, the seed presets and any later producer all emit byte-identical
|
|
107
|
+
* wording — two copies of this template would drift the moment one is reworded,
|
|
108
|
+
* and the drift only shows up in a file the model reads as instructions.
|
|
109
|
+
*
|
|
110
|
+
* `g` carries { tier, label, labelEn, example, exampleEn, agent }.
|
|
111
|
+
*/
|
|
112
|
+
export function modelRuleBaseText(g, lang = userLanguage()) {
|
|
113
|
+
if (lang === 'ko') {
|
|
114
|
+
return g.tier === 'T2'
|
|
115
|
+
? `"${g.label}" 유형의 단순 요청(예: "${g.example}")은 ${agentPhrase(g.agent)} 서브에이전트로 위임한다 (설계 판단·배포·스토어 제출 같은 비가역 작업이 섞이면 위임하지 않음)`
|
|
116
|
+
: `"${g.label}" 유형의 중간 난도 요청(예: "${g.example}")은 model: sonnet 서브에이전트로 위임한다 (설계 판단·비가역 작업·반복 에러 발생 시 메인 모델이 이어받음)`;
|
|
117
|
+
}
|
|
118
|
+
const example = g.exampleEn || g.example;
|
|
119
|
+
return g.tier === 'T2'
|
|
120
|
+
? `Delegate simple "${g.labelEn}" requests (e.g. "${example}") to ${agentPhraseEn(g.agent)} — never when the request mixes in design judgement or irreversible work like deploy/release/submission`
|
|
121
|
+
: `Delegate moderate "${g.labelEn}" requests (e.g. "${example}") to a model: sonnet subagent — hand back to the main model on design judgement, irreversible work, or repeated errors`;
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
// See the note in route-scan.js — paths.js is the only place that resolves
|
|
104
125
|
// this, so an XDG_CONFIG_HOME override moves every state file together.
|
|
105
126
|
const stateDir = userDataDir;
|
|
@@ -234,7 +255,12 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
|
|
|
234
255
|
: ` ⚠ rule-health: recent error rate ${rate}% for the delegated category — narrow the condition or remove`;
|
|
235
256
|
};
|
|
236
257
|
const statsOf = (r) => {
|
|
237
|
-
|
|
258
|
+
// A seeded rule carries no recurrence of its own until a scan measures one.
|
|
259
|
+
// Printing `×0` would read as "never happened"; printing the author's count
|
|
260
|
+
// would pass someone else's evidence off as the user's.
|
|
261
|
+
const base = r.origin === 'preset' && !r.count
|
|
262
|
+
? `preset (curated), registered ${r.promotedAt}`
|
|
263
|
+
: `×${r.count || 0}, err ${pct(r.errRate)}%, seen ${r.lastSeen || r.promotedAt}`;
|
|
238
264
|
if (!r.delegatedRuns) return base;
|
|
239
265
|
const saved = r.savedUsd ? `, saved ~$${r.savedUsd.toFixed(2)}` : '';
|
|
240
266
|
return `${base}, delegated ×${r.delegatedRuns} err ${pct(r.delegatedErrRate)}%${saved}`;
|
|
@@ -260,13 +286,16 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
|
|
|
260
286
|
const t2 = group.find((r) => r.tier === 'T2');
|
|
261
287
|
const t1 = group.find((r) => r.tier === 'T1');
|
|
262
288
|
if (t2 && t1) {
|
|
289
|
+
// Seeded rules carry a localized example; scan-produced ones only have the
|
|
290
|
+
// user's own prompt, so exampleEn is a preference, not a requirement.
|
|
291
|
+
const ex = (r) => (ko ? r.example : (r.exampleEn || r.example));
|
|
263
292
|
const rule = ko
|
|
264
|
-
? `"${t2.label}" 유형 요청은 기본적으로 ${agentPhrase(t2.agent)} 서브에이전트로 위임한다(예: "${t2
|
|
265
|
-
`여러 단계·여러 파일 수정이 얽힌 중간 난도 요청(예: "${t1
|
|
293
|
+
? `"${t2.label}" 유형 요청은 기본적으로 ${agentPhrase(t2.agent)} 서브에이전트로 위임한다(예: "${ex(t2)}"). ` +
|
|
294
|
+
`여러 단계·여러 파일 수정이 얽힌 중간 난도 요청(예: "${ex(t1)}")은 model: sonnet 서브에이전트로 위임한다. ` +
|
|
266
295
|
`설계 판단이나 배포·릴리스·스토어 제출 같은 비가역 작업이 섞여 있을 때만 위임하지 않는다. ` +
|
|
267
296
|
mergedBudget(t2, t1)
|
|
268
|
-
: `Delegate "${t2.labelEn || t2.label}" requests to ${agentPhraseEn(t2.agent)} by default (e.g. "${t2
|
|
269
|
-
`Escalate moderate ones that span multiple steps or file edits (e.g. "${t1
|
|
297
|
+
: `Delegate "${t2.labelEn || t2.label}" requests to ${agentPhraseEn(t2.agent)} by default (e.g. "${ex(t2)}"). ` +
|
|
298
|
+
`Escalate moderate ones that span multiple steps or file edits (e.g. "${ex(t1)}") to a model: sonnet subagent. ` +
|
|
270
299
|
`Keep it on the main model only when the request mixes in design judgement or irreversible work (deploy, release, store submission). ` +
|
|
271
300
|
mergedBudget(t2, t1);
|
|
272
301
|
lines.push(`- ${rule}${healthOf(t2)}${healthOf(t1)} <!-- T2 ${statsOf(t2)} / T1 ${statsOf(t1)} -->`);
|
package/src/route-scan.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from './subagent-records.js';
|
|
28
28
|
import { estimateCost, modelRank, isRecognizedModelId, TIER_TARGET_RANK, tierForRank } from './cost.js';
|
|
29
29
|
import { learnProfileMapping, resetModelAliasCache } from './model-alias.js';
|
|
30
|
-
import {
|
|
30
|
+
import { modelRuleBaseText } from './model-rules.js';
|
|
31
31
|
|
|
32
32
|
// ── Tier bands (docs/TIER_CRITERIA.md §3) ────────────────────────────────
|
|
33
33
|
// T2 (haiku): finished in few calls, tiny output, near-zero mutation, no
|
|
@@ -615,12 +615,11 @@ export async function runRouteScan({ days = 14 } = {}) {
|
|
|
615
615
|
calls: g.tier === 'T2' ? T2_MAX_CALLS + 2 : null,
|
|
616
616
|
out: g.tier === 'T2' ? thresholds.t2Out : thresholds.t1Out,
|
|
617
617
|
});
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
: `Delegate moderate "${g.labelEn}" requests (e.g. "${g.example}") to a model: sonnet subagent — hand back to the main model on design judgement, irreversible work, or repeated errors`;
|
|
618
|
+
// Shared with the seed presets (src/seed-rules.js) through model-rules.js —
|
|
619
|
+
// one template, so a reworded rule cannot mean two different things
|
|
620
|
+
// depending on which producer wrote it.
|
|
621
|
+
const ruleText = (g) => modelRuleBaseText(g, 'ko');
|
|
622
|
+
const ruleTextEn = (g) => modelRuleBaseText(g, 'en');
|
|
624
623
|
|
|
625
624
|
const candidates = [...groups.values()]
|
|
626
625
|
.filter((g) => g.count >= MIN_RECURRENCE && !hasRule(g))
|