claude-token-saver 2.0.3 → 2.2.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 +81 -23
- package/bin/cli.js +340 -11
- package/package.json +1 -1
- package/src/advice.js +48 -41
- package/src/caps-cache.js +51 -0
- package/src/config.js +151 -0
- package/src/demo.js +251 -0
- package/src/formatters/statusline.js +102 -23
- package/src/formatters/table.js +51 -11
- package/src/handoff.js +162 -0
- package/src/history.js +258 -0
- package/src/installer.js +150 -0
- package/src/paths.js +41 -0
- package/examples/statusline-with-rz1989s.sh +0 -52
package/src/history.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warning history — appends an entry whenever the active chip transitions
|
|
3
|
+
* (none → warning, warning A → warning B, warning → none). One markdown file
|
|
4
|
+
* per calendar day so users can pinpoint "when did this start" easily.
|
|
5
|
+
*
|
|
6
|
+
* Each event is written bilingually: the canonical English line first, the
|
|
7
|
+
* Korean translation as an indented "└" continuation right below. The chip
|
|
8
|
+
* text itself stays as-is (its symbol+English is part of the UX surface), but
|
|
9
|
+
* the diagnostic detail and resolved-status verbs are translated.
|
|
10
|
+
*
|
|
11
|
+
* Storage path is platform-aware (see paths.userDataDir):
|
|
12
|
+
* Windows: %APPDATA%\claude-token-saver\history\YYYY-MM-DD.md
|
|
13
|
+
* macOS: ~/Library/Application Support/claude-token-saver/history/YYYY-MM-DD.md
|
|
14
|
+
* Linux: ~/.config/claude-token-saver/history/YYYY-MM-DD.md
|
|
15
|
+
*
|
|
16
|
+
* State (last-seen chip, prevents duplicate appends every 1s refresh):
|
|
17
|
+
* <userDataDir>/last-chip.json
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { userDataDir } from './paths.js';
|
|
23
|
+
|
|
24
|
+
const BASE_DIR = userDataDir();
|
|
25
|
+
const HISTORY_DIR = join(BASE_DIR, 'history');
|
|
26
|
+
const STATE_PATH = join(BASE_DIR, 'last-chip.json');
|
|
27
|
+
|
|
28
|
+
export function historyDir() {
|
|
29
|
+
return HISTORY_DIR;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensureDir(p) {
|
|
33
|
+
if (!existsSync(p)) mkdirSync(p, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pad(n) {
|
|
37
|
+
return String(n).padStart(2, '0');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ymd(d = new Date()) {
|
|
41
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hms(d = new Date()) {
|
|
45
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadState() {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(STATE_PATH, 'utf8'));
|
|
51
|
+
} catch {
|
|
52
|
+
return { chip: null, ts: null };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveState(state) {
|
|
57
|
+
ensureDir(BASE_DIR);
|
|
58
|
+
writeFileSync(STATE_PATH, JSON.stringify(state) + '\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Map a chip's English label to its Korean equivalent.
|
|
63
|
+
* Returns the input unchanged if no mapping is registered (forward-compat
|
|
64
|
+
* with chips added in advice.js after this map was last updated).
|
|
65
|
+
*/
|
|
66
|
+
function chipKo(chip) {
|
|
67
|
+
if (!chip) return chip;
|
|
68
|
+
const map = {
|
|
69
|
+
'⚠ 1M ON': '⚠ 1M 컨텍스트 활성',
|
|
70
|
+
'⚠ Cache miss': '⚠ 캐시 미스',
|
|
71
|
+
'⚠ Rebuild churn': '⚠ 캐시 재빌드 빈발',
|
|
72
|
+
'⚠ Input spike': '⚠ 입력 급증',
|
|
73
|
+
'⚠ Output heavy': '⚠ 출력 과다',
|
|
74
|
+
'⚠ Call surge': '⚠ 호출 급증',
|
|
75
|
+
'⚠ 5m TTL': '⚠ 5분 TTL',
|
|
76
|
+
'⏳ Cache expires': '⏳ 캐시 만료 임박',
|
|
77
|
+
'💰 Cache saved': '💰 캐시 절약',
|
|
78
|
+
'🧠 Cache hit': '🧠 캐시 적중',
|
|
79
|
+
};
|
|
80
|
+
return map[chip] || chip;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Translate the diagnostic detail string to Korean. The detail is constructed
|
|
85
|
+
* in cli.js and follows two stable shapes:
|
|
86
|
+
* "Context auto-promoted to 1M (max single-request {N}k tokens)"
|
|
87
|
+
* "session {ID}: CODE_A, CODE_B"
|
|
88
|
+
* Anything else falls through unchanged.
|
|
89
|
+
*/
|
|
90
|
+
function detailKo(detail) {
|
|
91
|
+
if (!detail) return detail;
|
|
92
|
+
const m1 = detail.match(/^Context auto-promoted to 1M \(max single-request (\d+)k tokens\)$/);
|
|
93
|
+
if (m1) return `1M 컨텍스트 자동 활성 (단일 요청 최대 ${m1[1]}k 토큰)`;
|
|
94
|
+
const m2 = detail.match(/^session ([^:]+): (.+)$/);
|
|
95
|
+
if (m2) {
|
|
96
|
+
const codeKo = {
|
|
97
|
+
LOW_HIT_RATE: '캐시 적중률 낮음',
|
|
98
|
+
FREQUENT_CACHE_REBUILD: '캐시 재빌드 빈발',
|
|
99
|
+
OUTPUT_HEAVY: '출력 과다',
|
|
100
|
+
INPUT_SPIKE: '입력 급증',
|
|
101
|
+
CALL_SURGE: '호출 급증',
|
|
102
|
+
TTL_5M: '5분 TTL',
|
|
103
|
+
};
|
|
104
|
+
const codes = m2[2]
|
|
105
|
+
.split(',')
|
|
106
|
+
.map((c) => c.trim())
|
|
107
|
+
.map((c) => codeKo[c] || c)
|
|
108
|
+
.join(', ');
|
|
109
|
+
return `세션 ${m2[1]}: ${codes}`;
|
|
110
|
+
}
|
|
111
|
+
return detail;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function appendDayLine(en, ko, date = new Date()) {
|
|
115
|
+
ensureDir(HISTORY_DIR);
|
|
116
|
+
const path = join(HISTORY_DIR, `${ymd(date)}.md`);
|
|
117
|
+
const block = ko && ko !== en ? `${en}\n └ ${ko}\n` : `${en}\n`;
|
|
118
|
+
if (!existsSync(path)) {
|
|
119
|
+
const header = `# Token Monitor / 토큰 모니터 — ${ymd(date)}\n\n## Events / 이벤트\n`;
|
|
120
|
+
writeFileSync(path, header + block);
|
|
121
|
+
} else {
|
|
122
|
+
const existing = readFileSync(path, 'utf8');
|
|
123
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
124
|
+
writeFileSync(path, existing + sep + block);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Record a chip transition. Called from the statusline render path.
|
|
130
|
+
* Returns `true` if a transition was logged, `false` if duplicate (same chip
|
|
131
|
+
* as last call).
|
|
132
|
+
*/
|
|
133
|
+
export function recordChip(chip, contextHints = {}) {
|
|
134
|
+
const state = loadState();
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const current = chip || null;
|
|
137
|
+
const last = state.chip || null;
|
|
138
|
+
|
|
139
|
+
if (current === last) return false;
|
|
140
|
+
|
|
141
|
+
const detail = contextHints.detail || null;
|
|
142
|
+
const detailEn = detail ? ` — ${detail}` : '';
|
|
143
|
+
const detailKr = detail ? ` — ${detailKo(detail)}` : '';
|
|
144
|
+
|
|
145
|
+
let en, ko;
|
|
146
|
+
if (current && !last) {
|
|
147
|
+
en = `- ${hms(now)} ${current}${detailEn}`;
|
|
148
|
+
ko = `${chipKo(current)}${detailKr}`;
|
|
149
|
+
} else if (current && last) {
|
|
150
|
+
en = `- ${hms(now)} ${last} → ${current}${detailEn}`;
|
|
151
|
+
ko = `${chipKo(last)} → ${chipKo(current)}${detailKr}`;
|
|
152
|
+
} else {
|
|
153
|
+
// current === null, last was something — warning resolved
|
|
154
|
+
en = `- ${hms(now)} ✓ resolved (was ${last})`;
|
|
155
|
+
ko = `✓ 해소됨 (이전: ${chipKo(last)})`;
|
|
156
|
+
}
|
|
157
|
+
appendDayLine(en, ko, now);
|
|
158
|
+
saveState({ chip: current, ts: now.toISOString() });
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Read history files for the most recent N days (oldest first).
|
|
164
|
+
* Returns array of { date, content } — empty content for days with no file.
|
|
165
|
+
*/
|
|
166
|
+
export function readRecent(days = 7) {
|
|
167
|
+
ensureDir(HISTORY_DIR);
|
|
168
|
+
const out = [];
|
|
169
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
170
|
+
const d = new Date();
|
|
171
|
+
d.setDate(d.getDate() - i);
|
|
172
|
+
const date = ymd(d);
|
|
173
|
+
const path = join(HISTORY_DIR, `${date}.md`);
|
|
174
|
+
if (existsSync(path)) {
|
|
175
|
+
out.push({ date, content: readFileSync(path, 'utf8') });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
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
|
+
/**
|
|
196
|
+
* Record entering or exiting the cap-warn (>=90%) zone for a rate-limit
|
|
197
|
+
* window. Each window (`five_hour`, `seven_day`) has its own dedup slot, so
|
|
198
|
+
* the daily file gets two transitions max per window per warning episode.
|
|
199
|
+
*
|
|
200
|
+
* @param {'five_hour'|'seven_day'} kind
|
|
201
|
+
* @param {{ usedPct: number, resetsAt: number|null } | null} info
|
|
202
|
+
* @returns {boolean} true when a line was appended
|
|
203
|
+
*/
|
|
204
|
+
export function recordCapTransition(kind, info) {
|
|
205
|
+
const state = loadState();
|
|
206
|
+
const slotKey = `cap_${kind}`;
|
|
207
|
+
const wasWarn = !!state[slotKey];
|
|
208
|
+
const isWarn = !!(info && Number.isFinite(info.usedPct) && info.usedPct >= 90);
|
|
209
|
+
if (wasWarn === isWarn) return false;
|
|
210
|
+
|
|
211
|
+
const now = new Date();
|
|
212
|
+
const labelEn = kind === 'five_hour' ? '5H' : '7D';
|
|
213
|
+
const labelKo = kind === 'five_hour' ? '5시간 윈도' : '7일 윈도';
|
|
214
|
+
let en;
|
|
215
|
+
let ko;
|
|
216
|
+
if (isWarn) {
|
|
217
|
+
const pct = Math.round(info.usedPct);
|
|
218
|
+
const reset = formatResetIn(info.resetsAt, now);
|
|
219
|
+
const tail = reset ? ` (resets in ${reset})` : '';
|
|
220
|
+
const tailKo = reset ? ` (리셋까지 ${reset})` : '';
|
|
221
|
+
en = `- ${hms(now)} 🚨 ${labelEn} ${pct}% cap warning${tail}`;
|
|
222
|
+
ko = `🚨 ${labelKo} ${pct}% 캡 경고${tailKo}`;
|
|
223
|
+
} else {
|
|
224
|
+
en = `- ${hms(now)} ✓ ${labelEn} cap warning resolved`;
|
|
225
|
+
ko = `✓ ${labelKo} 캡 경고 해소`;
|
|
226
|
+
}
|
|
227
|
+
appendDayLine(en, ko, now);
|
|
228
|
+
state[slotKey] = isWarn;
|
|
229
|
+
saveState(state);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Record a handoff write — invoked by the `handoff` subcommand so
|
|
235
|
+
* `claude-token-saver history` shows when work was backed up to a HANDOFF file.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} filePath
|
|
238
|
+
* @returns {boolean}
|
|
239
|
+
*/
|
|
240
|
+
export function recordHandoff(filePath) {
|
|
241
|
+
const now = new Date();
|
|
242
|
+
const en = `- ${hms(now)} 📝 handoff written: ${filePath}`;
|
|
243
|
+
const ko = `📝 핸드오프 백업 작성: ${filePath}`;
|
|
244
|
+
appendDayLine(en, ko, now);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* List all available history file dates (sorted newest first).
|
|
250
|
+
*/
|
|
251
|
+
export function listDates() {
|
|
252
|
+
ensureDir(HISTORY_DIR);
|
|
253
|
+
return readdirSync(HISTORY_DIR)
|
|
254
|
+
.filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
|
|
255
|
+
.map((f) => f.replace(/\.md$/, ''))
|
|
256
|
+
.sort()
|
|
257
|
+
.reverse();
|
|
258
|
+
}
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs the Claude Code integration assets:
|
|
3
|
+
* - Skill: ~/.claude/skills/claude-token-saver/SKILL.md
|
|
4
|
+
* - Slash: ~/.claude/commands/token-monitor.md
|
|
5
|
+
*
|
|
6
|
+
* All paths are resolved with node:path so Windows backslashes and POSIX
|
|
7
|
+
* forward-slashes are both handled. Directories are created with
|
|
8
|
+
* `mkdirSync(..., { recursive: true })` which is a no-op if they already
|
|
9
|
+
* exist on every platform.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { claudeUserDir } from './paths.js';
|
|
15
|
+
|
|
16
|
+
const SKILL_BODY = `---
|
|
17
|
+
name: claude-token-saver
|
|
18
|
+
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%", "⚠ 1M ON", "⚠ 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\`.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# claude-token-saver — Claude Code Token Monitor
|
|
22
|
+
|
|
23
|
+
This skill helps users interpret and act on the \`claude-token-saver\` statusline
|
|
24
|
+
in Claude Code. The statusline updates every ~1s and shows cache health, TTL
|
|
25
|
+
countdown, savings, and (when relevant) a leading warning chip.
|
|
26
|
+
|
|
27
|
+
## When this skill should activate
|
|
28
|
+
|
|
29
|
+
- The user references any chip wording: \`🚨 5H NN%\`, \`🚨 7D NN%\`,
|
|
30
|
+
\`⚠ 1M ON\`, \`⚠ Input spike\`, \`⚠ Cache miss\`, \`⚠ 5m TTL\`,
|
|
31
|
+
\`⚠ Rebuild churn\`, \`⚠ Output heavy\`, \`⚠ Call surge\`.
|
|
32
|
+
- The user asks "why is my cache hit rate low", "what does this warning mean",
|
|
33
|
+
"when did this start happening", or similar.
|
|
34
|
+
- The user is approaching a rate-limit cap and wants to back up the current
|
|
35
|
+
work so a fresh session can continue (point them at
|
|
36
|
+
\`claude-token-saver handoff\`).
|
|
37
|
+
- The user wants to see the token-usage history file or asks for a summary
|
|
38
|
+
of recent warnings.
|
|
39
|
+
|
|
40
|
+
## What to do
|
|
41
|
+
|
|
42
|
+
1. **Identify the chip.** If the user pasted a statusline, pull out the leading
|
|
43
|
+
\`⚠ ...\` chip. That maps to a specific issue category.
|
|
44
|
+
2. **Show recent history.** Run \`claude-token-saver history\` (default last 7
|
|
45
|
+
days) to see the chronology of warning transitions. Each entry is timestamped
|
|
46
|
+
and includes a short detail string.
|
|
47
|
+
3. **Drill down on the live state.** Run \`claude-token-saver --days 1\` (or
|
|
48
|
+
another window) to render the full table view, which lists per-session
|
|
49
|
+
spikes and recommended actions.
|
|
50
|
+
4. **Explain the warning** in plain language. Use the chip → cause table:
|
|
51
|
+
|
|
52
|
+
| Chip | Likely cause |
|
|
53
|
+
| ------------------ | ----------------------------------------------------- |
|
|
54
|
+
| \`🚨 5H NN%\` | 5-hour rate-limit window at NN% (>=90%). Cap is imminent. |
|
|
55
|
+
| \`🚨 7D NN%\` | 7-day rate-limit window at NN% (>=90%). Pace yourself. |
|
|
56
|
+
| \`⚠ 1M ON\` | Auto-promoted to 1M context (Opus 4.7+ Max default). |
|
|
57
|
+
| \`⚠ Input spike\` | One request consumed >250k or >3× the recent p95. |
|
|
58
|
+
| \`⚠ Cache miss\` | Cache hit rate dropped below ~70%. |
|
|
59
|
+
| \`⚠ 5m TTL\` | Most cache writes are 5-min ephemeral (Pro plan default). |
|
|
60
|
+
| \`⚠ Rebuild churn\` | Cache being re-written rapidly — prefix is unstable. |
|
|
61
|
+
| \`⚠ Output heavy\` | Output ratio dominates input — inspect long generations. |
|
|
62
|
+
| \`⚠ Call surge\` | Request count is well above baseline. |
|
|
63
|
+
|
|
64
|
+
5. **Suggest the next action.** For \`🚨 5H/7D\` chips, recommend running
|
|
65
|
+
\`claude-token-saver handoff\` to back up the current work to a
|
|
66
|
+
\`HANDOFF-*.md\` file before the cap hits, then continue in a fresh
|
|
67
|
+
session. For 1M ON, mention \`CLAUDE_CODE_DISABLE_1M_CONTEXT=1\`. For
|
|
68
|
+
5m TTL, point at the Max plan's 1h bucket. For input spike, suggest
|
|
69
|
+
splitting the conversation or compacting context.
|
|
70
|
+
|
|
71
|
+
## Useful commands
|
|
72
|
+
|
|
73
|
+
- \`claude-token-saver\` — full table report (default last 1 day).
|
|
74
|
+
- \`claude-token-saver --days 7\` — wider window.
|
|
75
|
+
- \`claude-token-saver history\` — recent warning transitions per day.
|
|
76
|
+
- \`claude-token-saver history --days 30\` — longer history.
|
|
77
|
+
- \`claude-token-saver handoff\` — write a HANDOFF-*.md template in cwd
|
|
78
|
+
capturing git status + cap snapshot, so a fresh session can resume cleanly.
|
|
79
|
+
- \`claude-token-saver mode\` — show statusline preferences.
|
|
80
|
+
- \`claude-token-saver mode icon verbose 1d\` — change preferences.
|
|
81
|
+
|
|
82
|
+
## Storage layout (for reference)
|
|
83
|
+
|
|
84
|
+
History files live under the OS-appropriate user-data dir:
|
|
85
|
+
- Windows: \`%APPDATA%\\claude-token-saver\\history\\YYYY-MM-DD.md\`
|
|
86
|
+
- macOS: \`~/Library/Application Support/claude-token-saver/history/YYYY-MM-DD.md\`
|
|
87
|
+
- Linux: \`~/.config/claude-token-saver/history/YYYY-MM-DD.md\`
|
|
88
|
+
|
|
89
|
+
Each day's file is plain Markdown — safe to open in any editor.
|
|
90
|
+
`;
|
|
91
|
+
|
|
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
|
+
function writeIfNeeded(file, body, force) {
|
|
125
|
+
const existed = existsSync(file);
|
|
126
|
+
if (existed && !force) return { path: file, action: 'exists' };
|
|
127
|
+
writeFileSync(file, body);
|
|
128
|
+
return { path: file, action: existed ? 'updated' : 'created' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function installSkill({ force = false } = {}) {
|
|
132
|
+
const dir = join(claudeUserDir(), 'skills', 'claude-token-saver');
|
|
133
|
+
const file = join(dir, 'SKILL.md');
|
|
134
|
+
mkdirSync(dir, { recursive: true });
|
|
135
|
+
return writeIfNeeded(file, SKILL_BODY, force);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function installCommand({ force = false } = {}) {
|
|
139
|
+
const dir = join(claudeUserDir(), 'commands');
|
|
140
|
+
const file = join(dir, 'token-monitor.md');
|
|
141
|
+
mkdirSync(dir, { recursive: true });
|
|
142
|
+
return writeIfNeeded(file, COMMAND_BODY, force);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function installAll({ force = false } = {}) {
|
|
146
|
+
return {
|
|
147
|
+
skill: installSkill({ force }),
|
|
148
|
+
command: installCommand({ force }),
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform user-data path resolution.
|
|
3
|
+
*
|
|
4
|
+
* Order of precedence:
|
|
5
|
+
* 1. $XDG_CONFIG_HOME (explicit override, honored on every platform)
|
|
6
|
+
* 2. %APPDATA% on Windows (e.g. C:\Users\foo\AppData\Roaming)
|
|
7
|
+
* 3. ~/Library/Application Support on macOS
|
|
8
|
+
* 4. ~/.config on Linux / fallback
|
|
9
|
+
*
|
|
10
|
+
* All paths are joined via node:path so the OS-correct separator is used
|
|
11
|
+
* automatically. Callers are responsible for `mkdirSync(..., { recursive: true })`
|
|
12
|
+
* before writing — every helper here returns a path string only.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the base directory for this tool's user-level data
|
|
20
|
+
* (config, history, last-chip state).
|
|
21
|
+
*/
|
|
22
|
+
export function userDataDir() {
|
|
23
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
24
|
+
return join(process.env.XDG_CONFIG_HOME, 'claude-token-saver');
|
|
25
|
+
}
|
|
26
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
27
|
+
return join(process.env.APPDATA, 'claude-token-saver');
|
|
28
|
+
}
|
|
29
|
+
if (process.platform === 'darwin') {
|
|
30
|
+
return join(homedir(), 'Library', 'Application Support', 'claude-token-saver');
|
|
31
|
+
}
|
|
32
|
+
return join(homedir(), '.config', 'claude-token-saver');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the user's Claude Code config root (~/.claude on every OS Claude
|
|
37
|
+
* Code supports — the CLI itself uses this path on Windows and macOS too).
|
|
38
|
+
*/
|
|
39
|
+
export function claudeUserDir() {
|
|
40
|
+
return join(homedir(), '.claude');
|
|
41
|
+
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Combine rz1989s/claude-code-statusline (rich layout: repo, cost, MCP, prayer times)
|
|
3
|
-
# with claude-token-saver (cache hit rate, TTL countdown, 1M-context detection,
|
|
4
|
-
# spike diagnosis). The two projects don't overlap — rz1989s runs first, our
|
|
5
|
-
# cache chip is appended as the final segment.
|
|
6
|
-
#
|
|
7
|
-
# Install:
|
|
8
|
-
# 1) Follow rz1989s install instructions so bash ~/.claude/statusline.sh works:
|
|
9
|
-
# https://github.com/rz1989s/claude-code-statusline
|
|
10
|
-
# 2) npm install -g claude-token-saver (or rely on npx — fallback below)
|
|
11
|
-
# 3) Save this file as: ~/.claude/statusline-with-rz1989s.sh
|
|
12
|
-
# chmod +x ~/.claude/statusline-with-rz1989s.sh
|
|
13
|
-
# 4) In ~/.claude/settings.json:
|
|
14
|
-
# {
|
|
15
|
-
# "statusLine": {
|
|
16
|
-
# "type": "command",
|
|
17
|
-
# "command": "bash ~/.claude/statusline-with-rz1989s.sh",
|
|
18
|
-
# "refreshInterval": 1
|
|
19
|
-
# }
|
|
20
|
-
# }
|
|
21
|
-
#
|
|
22
|
-
# refreshInterval: 1 keeps our TTL countdown ticking while you're idle.
|
|
23
|
-
# Drop to 2 or 5 for lower local CPU if your rz1989s config does heavy work.
|
|
24
|
-
|
|
25
|
-
# Claude Code sends the session JSON on stdin. Both tools want to read it,
|
|
26
|
-
# so we buffer it and tee to each.
|
|
27
|
-
input=$(cat)
|
|
28
|
-
|
|
29
|
-
# --- 1) rz1989s layout (if installed) ---
|
|
30
|
-
RZ_STATUSLINE="${CLAUDE_RZ_STATUSLINE:-$HOME/.claude/statusline.sh}"
|
|
31
|
-
if [ -f "$RZ_STATUSLINE" ]; then
|
|
32
|
-
printf '%s' "$input" | bash "$RZ_STATUSLINE"
|
|
33
|
-
# Separator between the two tools. Dim pipe.
|
|
34
|
-
printf ' \033[90m|\033[00m '
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
|
-
# --- 2) claude-token-saver ---
|
|
38
|
-
# Pass --exclude-session so the current session's tool calls don't reset the
|
|
39
|
-
# TTL countdown. The path comes from the session JSON if present.
|
|
40
|
-
session_path=$(printf '%s' "$input" | sed -n 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\.jsonl\)".*/\1/p' | head -n1)
|
|
41
|
-
exclude_flag=""
|
|
42
|
-
if [ -n "$session_path" ]; then
|
|
43
|
-
exclude_flag="--exclude-session $session_path"
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
if command -v claude-token-saver >/dev/null 2>&1; then
|
|
47
|
-
# shellcheck disable=SC2086
|
|
48
|
-
claude-token-saver --statusline --icon $exclude_flag 2>/dev/null || true
|
|
49
|
-
else
|
|
50
|
-
# shellcheck disable=SC2086
|
|
51
|
-
npx --yes claude-token-saver@latest --statusline --icon $exclude_flag 2>/dev/null || true
|
|
52
|
-
fi
|