claude-token-saver 2.2.0 → 2.6.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.md +25 -6
- package/bin/cli.js +210 -51
- package/package.json +1 -1
- package/src/advice.js +120 -0
- package/src/caps-cache.js +46 -13
- package/src/demo.js +73 -1
- package/src/format-time.js +44 -0
- package/src/formatters/statusline.js +176 -38
- package/src/formatters/table.js +15 -21
- package/src/handoff.js +19 -20
- package/src/history.js +96 -31
- package/src/installer.js +35 -50
- package/src/window-labels.js +61 -0
package/src/handoff.js
CHANGED
|
@@ -16,6 +16,9 @@ import { writeFileSync, existsSync } from 'node:fs';
|
|
|
16
16
|
import { execSync } from 'node:child_process';
|
|
17
17
|
import { join, resolve } from 'node:path';
|
|
18
18
|
|
|
19
|
+
import { formatResetIn, formatResetClock } from './format-time.js';
|
|
20
|
+
import { labelForKey } from './window-labels.js';
|
|
21
|
+
|
|
19
22
|
function pad(n) {
|
|
20
23
|
return String(n).padStart(2, '0');
|
|
21
24
|
}
|
|
@@ -52,16 +55,6 @@ function gitSnapshot(cwd) {
|
|
|
52
55
|
return { branch, head, status };
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
function formatResetIn(resetsAt, now = new Date()) {
|
|
56
|
-
if (!Number.isFinite(resetsAt)) return null;
|
|
57
|
-
const remaining = Math.max(0, resetsAt - Math.floor(now.getTime() / 1000));
|
|
58
|
-
if (remaining <= 0) return '0m';
|
|
59
|
-
const h = Math.floor(remaining / 3600);
|
|
60
|
-
const m = Math.floor((remaining % 3600) / 60);
|
|
61
|
-
if (h > 0) return `${h}h ${m}m`;
|
|
62
|
-
return `${m}m`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
58
|
function pickPath(cwd, now) {
|
|
66
59
|
const stem = `HANDOFF-${ymd(now)}-${hhmm(now)}`;
|
|
67
60
|
const direct = join(cwd, `${stem}.md`);
|
|
@@ -99,15 +92,21 @@ function renderTemplate({ now, cwd, git, caps }) {
|
|
|
99
92
|
|
|
100
93
|
lines.push('## Cap snapshot');
|
|
101
94
|
lines.push('');
|
|
102
|
-
if (caps) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
if (caps && Array.isArray(caps.windows) && caps.windows.length > 0) {
|
|
96
|
+
for (const win of caps.windows) {
|
|
97
|
+
const label = labelForKey(win.key).long;
|
|
98
|
+
if (!Number.isFinite(win.usedPct)) {
|
|
99
|
+
lines.push(`- ${label}: (unknown)`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const reset = formatResetIn(win.resetsAt, now);
|
|
103
|
+
const clock = formatResetClock(win.resetsAt, now);
|
|
104
|
+
let tail = '';
|
|
105
|
+
if (reset && clock) tail = `, resets in ${reset} (at ${clock})`;
|
|
106
|
+
else if (reset) tail = `, resets in ${reset}`;
|
|
107
|
+
else if (clock) tail = `, resets at ${clock}`;
|
|
108
|
+
lines.push(`- ${label}: ${Math.round(win.usedPct)}%${tail}`);
|
|
109
|
+
}
|
|
111
110
|
} else {
|
|
112
111
|
lines.push('- (no cap data — run `handoff` from a Claude Code session for live numbers)');
|
|
113
112
|
}
|
|
@@ -148,7 +147,7 @@ function renderTemplate({ now, cwd, git, caps }) {
|
|
|
148
147
|
*
|
|
149
148
|
* @param {object} [opts]
|
|
150
149
|
* @param {string} [opts.cwd=process.cwd()]
|
|
151
|
-
* @param {object|null} [opts.caps] - {
|
|
150
|
+
* @param {object|null} [opts.caps] - { windows: [...] } from extractCaps
|
|
152
151
|
* @param {Date} [opts.now=new Date()]
|
|
153
152
|
* @returns {{ path: string, git: { branch: string, head: string, status: string } | null }}
|
|
154
153
|
*/
|
package/src/history.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { userDataDir } from './paths.js';
|
|
23
|
+
import { formatResetIn, formatResetClock } from './format-time.js';
|
|
24
|
+
import { labelForKey } from './window-labels.js';
|
|
25
|
+
import { ISSUE_TIPS, CHIP_TO_CODES, CAP_TIPS } from './advice.js';
|
|
23
26
|
|
|
24
27
|
const BASE_DIR = userDataDir();
|
|
25
28
|
const HISTORY_DIR = join(BASE_DIR, 'history');
|
|
@@ -111,10 +114,56 @@ function detailKo(detail) {
|
|
|
111
114
|
return detail;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the diagnostic codes that apply to a transition. We try, in order:
|
|
119
|
+
* 1. `detail` of shape "session ID: CODE_A, CODE_B" (chip transitions with
|
|
120
|
+
* explicit per-session codes — the richest source).
|
|
121
|
+
* 2. `chip` text mapped via CHIP_TO_CODES (covers 1M ON and chips that fire
|
|
122
|
+
* without a per-session detail).
|
|
123
|
+
* Returns an array of unique codes, possibly empty.
|
|
124
|
+
*/
|
|
125
|
+
function codesForEvent(chip, detail) {
|
|
126
|
+
const out = [];
|
|
127
|
+
if (detail) {
|
|
128
|
+
const m = detail.match(/^session [^:]+:\s*(.+)$/);
|
|
129
|
+
if (m) {
|
|
130
|
+
for (const c of m[1].split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
131
|
+
if (!out.includes(c)) out.push(c);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (chip && CHIP_TO_CODES[chip]) {
|
|
136
|
+
for (const c of CHIP_TO_CODES[chip]) if (!out.includes(c)) out.push(c);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the "💡 ..." tip block for a given chip+detail pair. Returns
|
|
143
|
+
* { en: string, ko: string } where each may be empty if no tips apply
|
|
144
|
+
* (resolution events, unknown chips, etc.).
|
|
145
|
+
*/
|
|
146
|
+
function tipsForEvent(chip, detail) {
|
|
147
|
+
const codes = codesForEvent(chip, detail);
|
|
148
|
+
const enLines = [];
|
|
149
|
+
const koLines = [];
|
|
150
|
+
for (const code of codes) {
|
|
151
|
+
const tip = ISSUE_TIPS[code];
|
|
152
|
+
if (!tip) continue;
|
|
153
|
+
enLines.push(` 💡 ${tip.en}`);
|
|
154
|
+
koLines.push(` 💡 ${tip.ko}`);
|
|
155
|
+
}
|
|
156
|
+
return { en: enLines.join('\n'), ko: koLines.join('\n') };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function appendDayLine(en, ko, date = new Date(), tips = null) {
|
|
115
160
|
ensureDir(HISTORY_DIR);
|
|
116
161
|
const path = join(HISTORY_DIR, `${ymd(date)}.md`);
|
|
117
|
-
|
|
162
|
+
// Bilingual event line. Tip lines (when present) follow on their own lines so
|
|
163
|
+
// the file reads as: event-en / └ event-ko / 💡 tip-en / 💡 tip-ko.
|
|
164
|
+
let block = ko && ko !== en ? `${en}\n └ ${ko}\n` : `${en}\n`;
|
|
165
|
+
if (tips && tips.en) block += `${tips.en}\n`;
|
|
166
|
+
if (tips && tips.ko) block += `${tips.ko}\n`;
|
|
118
167
|
if (!existsSync(path)) {
|
|
119
168
|
const header = `# Token Monitor / 토큰 모니터 — ${ymd(date)}\n\n## Events / 이벤트\n`;
|
|
120
169
|
writeFileSync(path, header + block);
|
|
@@ -143,18 +192,23 @@ export function recordChip(chip, contextHints = {}) {
|
|
|
143
192
|
const detailKr = detail ? ` — ${detailKo(detail)}` : '';
|
|
144
193
|
|
|
145
194
|
let en, ko;
|
|
195
|
+
// Tips only on warning entry/transition (not resolution) — based on the
|
|
196
|
+
// *new* chip so users see how to handle what's currently active.
|
|
197
|
+
let tips = null;
|
|
146
198
|
if (current && !last) {
|
|
147
199
|
en = `- ${hms(now)} ${current}${detailEn}`;
|
|
148
200
|
ko = `${chipKo(current)}${detailKr}`;
|
|
201
|
+
tips = tipsForEvent(current, detail);
|
|
149
202
|
} else if (current && last) {
|
|
150
203
|
en = `- ${hms(now)} ${last} → ${current}${detailEn}`;
|
|
151
204
|
ko = `${chipKo(last)} → ${chipKo(current)}${detailKr}`;
|
|
205
|
+
tips = tipsForEvent(current, detail);
|
|
152
206
|
} else {
|
|
153
207
|
// current === null, last was something — warning resolved
|
|
154
208
|
en = `- ${hms(now)} ✓ resolved (was ${last})`;
|
|
155
209
|
ko = `✓ 해소됨 (이전: ${chipKo(last)})`;
|
|
156
210
|
}
|
|
157
|
-
appendDayLine(en, ko, now);
|
|
211
|
+
appendDayLine(en, ko, now, tips);
|
|
158
212
|
saveState({ chip: current, ts: now.toISOString() });
|
|
159
213
|
return true;
|
|
160
214
|
}
|
|
@@ -178,53 +232,64 @@ export function readRecent(days = 7) {
|
|
|
178
232
|
return out;
|
|
179
233
|
}
|
|
180
234
|
|
|
181
|
-
/**
|
|
182
|
-
* Format "resets in Hh Mm" / "Mm" given a Unix-epoch resets_at value.
|
|
183
|
-
* Returns null when the input isn't a finite number.
|
|
184
|
-
*/
|
|
185
|
-
function formatResetIn(resetsAt, now = new Date()) {
|
|
186
|
-
if (!Number.isFinite(resetsAt)) return null;
|
|
187
|
-
const remainingSec = Math.max(0, resetsAt - Math.floor(now.getTime() / 1000));
|
|
188
|
-
if (remainingSec <= 0) return '0m';
|
|
189
|
-
const h = Math.floor(remainingSec / 3600);
|
|
190
|
-
const m = Math.floor((remainingSec % 3600) / 60);
|
|
191
|
-
if (h > 0) return `${h}h ${m}m`;
|
|
192
|
-
return `${m}m`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
235
|
/**
|
|
196
236
|
* Record entering or exiting the cap-warn (>=90%) zone for a rate-limit
|
|
197
|
-
* window. Each window (`five_hour`, `seven_day`)
|
|
198
|
-
* the daily file gets two transitions max per
|
|
237
|
+
* window. Each window (keyed by its stdin name — e.g. `five_hour`, `seven_day`)
|
|
238
|
+
* has its own dedup slot, so the daily file gets two transitions max per
|
|
239
|
+
* window per warning episode.
|
|
199
240
|
*
|
|
200
|
-
* @param {
|
|
201
|
-
* @param {{ usedPct: number, resetsAt: number|null } | null} info
|
|
241
|
+
* @param {{ key: string, usedPct: number, resetsAt: number|null } | null} window
|
|
202
242
|
* @returns {boolean} true when a line was appended
|
|
203
243
|
*/
|
|
204
|
-
export function recordCapTransition(
|
|
244
|
+
export function recordCapTransition(window) {
|
|
245
|
+
if (!window || typeof window.key !== 'string') return false;
|
|
205
246
|
const state = loadState();
|
|
206
|
-
const slotKey = `cap_${
|
|
247
|
+
const slotKey = `cap_${window.key}`;
|
|
207
248
|
const wasWarn = !!state[slotKey];
|
|
208
|
-
const isWarn =
|
|
249
|
+
const isWarn = Number.isFinite(window.usedPct) && window.usedPct >= 90;
|
|
209
250
|
if (wasWarn === isWarn) return false;
|
|
210
251
|
|
|
211
252
|
const now = new Date();
|
|
212
|
-
const
|
|
213
|
-
const
|
|
253
|
+
const labels = labelForKey(window.key);
|
|
254
|
+
const labelEn = labels.short;
|
|
255
|
+
// Korean labels map only the well-known windows; everything else falls back
|
|
256
|
+
// to the English short label (still readable for the bilingual line).
|
|
257
|
+
const KO_OVERRIDES = {
|
|
258
|
+
five_hour: '5시간 윈도',
|
|
259
|
+
seven_day: '7일 윈도',
|
|
260
|
+
seven_day_sonnet: '7일 윈도 (Sonnet)',
|
|
261
|
+
seven_day_opus: '7일 윈도 (Opus)',
|
|
262
|
+
};
|
|
263
|
+
const labelKo = KO_OVERRIDES[window.key] || labels.short;
|
|
214
264
|
let en;
|
|
215
265
|
let ko;
|
|
216
266
|
if (isWarn) {
|
|
217
|
-
const pct = Math.round(
|
|
218
|
-
const reset = formatResetIn(
|
|
219
|
-
const
|
|
220
|
-
const
|
|
267
|
+
const pct = Math.round(window.usedPct);
|
|
268
|
+
const reset = formatResetIn(window.resetsAt, now);
|
|
269
|
+
const clock = formatResetClock(window.resetsAt, now);
|
|
270
|
+
const tail = reset && clock
|
|
271
|
+
? ` (resets in ${reset}, at ${clock})`
|
|
272
|
+
: reset
|
|
273
|
+
? ` (resets in ${reset})`
|
|
274
|
+
: clock
|
|
275
|
+
? ` (resets at ${clock})`
|
|
276
|
+
: '';
|
|
277
|
+
const tailKo = reset && clock
|
|
278
|
+
? ` (${clock}에 리셋, 남은 ${reset})`
|
|
279
|
+
: reset
|
|
280
|
+
? ` (리셋까지 ${reset})`
|
|
281
|
+
: clock
|
|
282
|
+
? ` (${clock}에 리셋)`
|
|
283
|
+
: '';
|
|
221
284
|
en = `- ${hms(now)} 🚨 ${labelEn} ${pct}% cap warning${tail}`;
|
|
222
285
|
ko = `🚨 ${labelKo} ${pct}% 캡 경고${tailKo}`;
|
|
223
286
|
} else {
|
|
224
287
|
en = `- ${hms(now)} ✓ ${labelEn} cap warning resolved`;
|
|
225
288
|
ko = `✓ ${labelKo} 캡 경고 해소`;
|
|
226
289
|
}
|
|
227
|
-
|
|
290
|
+
// Cap-warn entry → handoff tip; resolution → no tip (just the ✓ line).
|
|
291
|
+
const capTips = isWarn ? { en: ` 💡 ${CAP_TIPS.en}`, ko: ` 💡 ${CAP_TIPS.ko}` } : null;
|
|
292
|
+
appendDayLine(en, ko, now, capTips);
|
|
228
293
|
state[slotKey] = isWarn;
|
|
229
294
|
saveState(state);
|
|
230
295
|
return true;
|
package/src/installer.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Installs the Claude Code integration
|
|
2
|
+
* Installs the Claude Code integration asset:
|
|
3
3
|
* - Skill: ~/.claude/skills/claude-token-saver/SKILL.md
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* v2.6.0 consolidates `/token-monitor` into the skill (was redundant with the
|
|
6
|
+
* auto-trigger). On install we actively remove a legacy
|
|
7
|
+
* ~/.claude/commands/token-monitor.md if present so users don't see two
|
|
8
|
+
* overlapping entry points.
|
|
5
9
|
*
|
|
6
10
|
* All paths are resolved with node:path so Windows backslashes and POSIX
|
|
7
11
|
* forward-slashes are both handled. Directories are created with
|
|
@@ -9,7 +13,7 @@
|
|
|
9
13
|
* exist on every platform.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
|
-
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
16
|
+
import { writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
13
17
|
import { join } from 'node:path';
|
|
14
18
|
import { claudeUserDir } from './paths.js';
|
|
15
19
|
|
|
@@ -36,18 +40,26 @@ countdown, savings, and (when relevant) a leading warning chip.
|
|
|
36
40
|
\`claude-token-saver handoff\`).
|
|
37
41
|
- The user wants to see the token-usage history file or asks for a summary
|
|
38
42
|
of recent warnings.
|
|
43
|
+
- The user asks for a quick token report or "current state" check (the
|
|
44
|
+
skill replaces the legacy \`/token-monitor\` slash command — same workflow,
|
|
45
|
+
triggered by intent rather than a typed slash).
|
|
39
46
|
|
|
40
47
|
## What to do
|
|
41
48
|
|
|
42
|
-
1. **
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
1. **Lead with the most recent warning + how to handle it.** Run
|
|
50
|
+
\`claude-token-saver last\` first. It returns the most recent warning event
|
|
51
|
+
(chip + detail + timestamp) plus the full advice block for it. Surface that
|
|
52
|
+
to the user before anything else — this is what they came for.
|
|
53
|
+
2. **Identify the chip.** If the user pasted a statusline (instead of relying
|
|
54
|
+
on \`last\`), pull out the leading \`⚠ ...\` chip. That maps to a specific
|
|
55
|
+
issue category.
|
|
56
|
+
3. **Show recent history.** Run \`claude-token-saver history\` (default last 7
|
|
57
|
+
days) to see the chronology of warning transitions. Each entry is timestamped,
|
|
58
|
+
bilingual (English line + 한국어), and includes a \`💡\` action tip inline.
|
|
59
|
+
4. **Drill down on the live state.** Run \`claude-token-saver --days 1\` (or
|
|
48
60
|
another window) to render the full table view, which lists per-session
|
|
49
61
|
spikes and recommended actions.
|
|
50
|
-
|
|
62
|
+
5. **Explain the warning** in plain language. Use the chip → cause table:
|
|
51
63
|
|
|
52
64
|
| Chip | Likely cause |
|
|
53
65
|
| ------------------ | ----------------------------------------------------- |
|
|
@@ -61,7 +73,7 @@ countdown, savings, and (when relevant) a leading warning chip.
|
|
|
61
73
|
| \`⚠ Output heavy\` | Output ratio dominates input — inspect long generations. |
|
|
62
74
|
| \`⚠ Call surge\` | Request count is well above baseline. |
|
|
63
75
|
|
|
64
|
-
|
|
76
|
+
6. **Suggest the next action.** For \`🚨 5H/7D\` chips, recommend running
|
|
65
77
|
\`claude-token-saver handoff\` to back up the current work to a
|
|
66
78
|
\`HANDOFF-*.md\` file before the cap hits, then continue in a fresh
|
|
67
79
|
session. For 1M ON, mention \`CLAUDE_CODE_DISABLE_1M_CONTEXT=1\`. For
|
|
@@ -70,9 +82,12 @@ countdown, savings, and (when relevant) a leading warning chip.
|
|
|
70
82
|
|
|
71
83
|
## Useful commands
|
|
72
84
|
|
|
85
|
+
- \`claude-token-saver last\` — most recent warning + full advice (start here).
|
|
86
|
+
- \`claude-token-saver last --days 7\` — widen the lookback window.
|
|
73
87
|
- \`claude-token-saver\` — full table report (default last 1 day).
|
|
74
88
|
- \`claude-token-saver --days 7\` — wider window.
|
|
75
|
-
- \`claude-token-saver history\` — recent warning transitions per day
|
|
89
|
+
- \`claude-token-saver history\` — recent warning transitions per day, with
|
|
90
|
+
inline \`💡\` action tips.
|
|
76
91
|
- \`claude-token-saver history --days 30\` — longer history.
|
|
77
92
|
- \`claude-token-saver handoff\` — write a HANDOFF-*.md template in cwd
|
|
78
93
|
capturing git status + cap snapshot, so a fresh session can resume cleanly.
|
|
@@ -89,38 +104,6 @@ History files live under the OS-appropriate user-data dir:
|
|
|
89
104
|
Each day's file is plain Markdown — safe to open in any editor.
|
|
90
105
|
`;
|
|
91
106
|
|
|
92
|
-
const COMMAND_BODY = `---
|
|
93
|
-
description: Show recent claude-token-saver warning history and a fresh report.
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
You are responding to the \`/token-monitor\` slash command. The user wants a
|
|
97
|
-
quick read of their Claude Code token usage and any active warnings.
|
|
98
|
-
|
|
99
|
-
Steps:
|
|
100
|
-
|
|
101
|
-
1. Run \`claude-token-saver history --days 7\` and capture the output. This
|
|
102
|
-
prints recent warning transitions (timestamps + chip + short detail),
|
|
103
|
-
including any \`🚨 5H NN%\` / \`🚨 7D NN%\` cap-warn entries and any
|
|
104
|
-
\`📝 handoff written: ...\` events.
|
|
105
|
-
2. Run \`claude-token-saver --days 1\` and capture the output. This prints the
|
|
106
|
-
full table view: TTL breakdown, cost impact, daily trend, and any active
|
|
107
|
-
spikes with recommended actions. When a rate-limit cap is at >=90% the
|
|
108
|
-
table leads with a "🚨 Rate-limit cap is closing in" section.
|
|
109
|
-
3. Summarize for the user:
|
|
110
|
-
- **Active warnings** — list the most recent unresolved chip(s) with the
|
|
111
|
-
time they appeared. Cap-warn (\`🚨 5H/7D NN%\`) outranks everything else.
|
|
112
|
-
- **Today's pattern** — when warnings cluster in time, mention it.
|
|
113
|
-
- **Recommended action** — for cap-warn, point at \`claude-token-saver
|
|
114
|
-
handoff\` so the user can back up state before the cap blocks them.
|
|
115
|
-
Otherwise pick the highest-leverage suggestion from the table report's
|
|
116
|
-
"Recommended actions" section.
|
|
117
|
-
4. If the history is empty, say so plainly — no warnings means the cache has
|
|
118
|
-
been healthy and no caps were close in the configured window.
|
|
119
|
-
|
|
120
|
-
Keep the summary to ~10 lines. The user can re-run the underlying commands
|
|
121
|
-
themselves for the full output.
|
|
122
|
-
`;
|
|
123
|
-
|
|
124
107
|
function writeIfNeeded(file, body, force) {
|
|
125
108
|
const existed = existsSync(file);
|
|
126
109
|
if (existed && !force) return { path: file, action: 'exists' };
|
|
@@ -135,16 +118,18 @@ export function installSkill({ force = false } = {}) {
|
|
|
135
118
|
return writeIfNeeded(file, SKILL_BODY, force);
|
|
136
119
|
}
|
|
137
120
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
121
|
+
// Removes the legacy /token-monitor slash command from prior versions.
|
|
122
|
+
// v2.6.0 consolidated it into the skill — the file would otherwise linger.
|
|
123
|
+
export function removeLegacyCommand() {
|
|
124
|
+
const file = join(claudeUserDir(), 'commands', 'token-monitor.md');
|
|
125
|
+
if (!existsSync(file)) return { path: file, action: 'absent' };
|
|
126
|
+
unlinkSync(file);
|
|
127
|
+
return { path: file, action: 'removed' };
|
|
143
128
|
}
|
|
144
129
|
|
|
145
130
|
export function installAll({ force = false } = {}) {
|
|
146
131
|
return {
|
|
147
132
|
skill: installSkill({ force }),
|
|
148
|
-
|
|
133
|
+
legacy: removeLegacyCommand(),
|
|
149
134
|
};
|
|
150
135
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friendly labels + icons for `rate_limits.*` keys from Claude Code's stdin
|
|
3
|
+
* payload. Known keys (`five_hour`, `seven_day`) get curated short/long labels
|
|
4
|
+
* and dedicated icons. Unknown keys are passed through with a derived label so
|
|
5
|
+
* any future window Anthropic adds (e.g. `seven_day_sonnet`) renders without
|
|
6
|
+
* a code change.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// `short` is used by the cap-warn chip and history records (stable shape so
|
|
10
|
+
// parsers/dedup keep working). `usageLabel` is the friendlier word that the
|
|
11
|
+
// always-on usage segment renders next to the icon — empty string means the
|
|
12
|
+
// icon alone carries the meaning (5H 'session/now' is the implicit default).
|
|
13
|
+
const KNOWN = {
|
|
14
|
+
// ✦ reads as "AI token unit" (Anthropic/OpenAI/Gemini sparkle motif), more
|
|
15
|
+
// on-theme than the 🪙 coin which suggested in-app currency. Same width as
|
|
16
|
+
// 📅 in monospace terminals so the chip alignment stays stable.
|
|
17
|
+
// 'current' mirrors how `weekly` reads next to 7D — names the window in
|
|
18
|
+
// plain English so a glance at `✦ current ████▒░ 72%` tells the eye what's
|
|
19
|
+
// being measured without parsing the icon's meaning.
|
|
20
|
+
five_hour: { short: '5H', long: 'Current session', icon: '✦', usageLabel: 'current' },
|
|
21
|
+
seven_day: { short: '7D', long: 'Current week', icon: '📅', usageLabel: 'weekly' },
|
|
22
|
+
// Speculative — `/usage` shows a Sonnet-only weekly bucket, so if Anthropic
|
|
23
|
+
// ever surfaces it on stdin we render with a sensible default already.
|
|
24
|
+
seven_day_sonnet: { short: '7D-S', long: 'Current week (Sonnet)', icon: '🅂', usageLabel: 'weekly (Sonnet)' },
|
|
25
|
+
seven_day_opus: { short: '7D-O', long: 'Current week (Opus)', icon: '🅾', usageLabel: 'weekly (Opus)' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function deriveShort(key) {
|
|
29
|
+
// "five_hour" → "5H"; "seven_day_sonnet" → "7DS"; arbitrary key → uppercase initials
|
|
30
|
+
const m = key.match(/^(\d+)_?([a-z]+)/);
|
|
31
|
+
if (m) {
|
|
32
|
+
const num = m[1];
|
|
33
|
+
const word = m[2];
|
|
34
|
+
const letter = word.charAt(0).toUpperCase();
|
|
35
|
+
const tail = key.slice(m[0].length);
|
|
36
|
+
const suffix = tail
|
|
37
|
+
.split('_')
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((s) => s.charAt(0).toUpperCase())
|
|
40
|
+
.join('');
|
|
41
|
+
return `${num}${letter}${suffix}`;
|
|
42
|
+
}
|
|
43
|
+
return key
|
|
44
|
+
.split('_')
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.map((s) => s.charAt(0).toUpperCase())
|
|
47
|
+
.join('') || key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deriveLong(key) {
|
|
51
|
+
return key
|
|
52
|
+
.split('_')
|
|
53
|
+
.map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
|
|
54
|
+
.join(' ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function labelForKey(key) {
|
|
58
|
+
if (KNOWN[key]) return KNOWN[key];
|
|
59
|
+
const short = deriveShort(key);
|
|
60
|
+
return { short, long: deriveLong(key), icon: '⏱', usageLabel: short };
|
|
61
|
+
}
|