claude-token-saver 2.7.3 β 2.8.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 +2 -2
- package/README.md +2 -2
- package/bin/cli.js +8 -0
- package/package.json +1 -1
- package/src/formatters/statusline.js +23 -6
- package/src/installer.js +58 -1
package/README.en.md
CHANGED
|
@@ -53,12 +53,12 @@ Risk chips lead when something's wrong: `π¨ 5H 94%`, `β 1M ON`, `β Cache m
|
|
|
53
53
|
"statusLine": {
|
|
54
54
|
"type": "command",
|
|
55
55
|
"command": "claude-token-saver --statusline --icon",
|
|
56
|
-
"refreshInterval":
|
|
56
|
+
"refreshInterval": 5
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
`refreshInterval:
|
|
61
|
+
`refreshInterval: 5` keeps the TTL countdown ticking while idle (Claude Code's statusline is otherwise event-driven). 1s also works, but 5s is the recommended default to avoid constant I/O. For Windows PowerShell, see `examples/statusline-command.ps1`.
|
|
62
62
|
|
|
63
63
|
## Commands
|
|
64
64
|
|
package/README.md
CHANGED
|
@@ -53,12 +53,12 @@ TTL Breakdown / Cost Impact / Daily Trend ...
|
|
|
53
53
|
"statusLine": {
|
|
54
54
|
"type": "command",
|
|
55
55
|
"command": "claude-token-saver --statusline --icon",
|
|
56
|
-
"refreshInterval":
|
|
56
|
+
"refreshInterval": 5
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
`refreshInterval:
|
|
61
|
+
`refreshInterval: 5`λ TTL μΉ΄μ΄νΈλ€μ΄μ΄ idle μνμμλ 5μ΄λ§λ€ κ°±μ λκ² ν©λλ€ (1μ΄λ κ°λ₯νμ§λ§ μμ I/O λΆλ΄μ νΌνλ €κ³ 5μ΄λ₯Ό κΈ°λ³Έκ°μΌλ‘ κΆμ₯). Windows(PowerShell)λ `examples/statusline-command.ps1` μ°Έκ³ .
|
|
62
62
|
|
|
63
63
|
## μ£Όμ λͺ
λ Ή
|
|
64
64
|
|
package/bin/cli.js
CHANGED
|
@@ -344,6 +344,14 @@ async function main() {
|
|
|
344
344
|
};
|
|
345
345
|
const r = installAll({ force });
|
|
346
346
|
print('skill', r.skill);
|
|
347
|
+
{
|
|
348
|
+
const s = r.statusline;
|
|
349
|
+
const verb = s.action === 'exists' ? 'already configured (refreshInterval=5)'
|
|
350
|
+
: s.action === 'skipped' ? `skipped β ${s.reason}`
|
|
351
|
+
: s.reason ? `${s.action} β ${s.reason}`
|
|
352
|
+
: s.action;
|
|
353
|
+
console.log(` statusline: ${s.path} (${verb})`);
|
|
354
|
+
}
|
|
347
355
|
if (r.legacy.action === 'removed') {
|
|
348
356
|
print('legacy /token-monitor', r.legacy);
|
|
349
357
|
console.log(' (consolidated into the skill β same workflow, triggered by intent)');
|
package/package.json
CHANGED
|
@@ -100,11 +100,18 @@ function gaugeBar(pct) {
|
|
|
100
100
|
* Format a remaining-seconds countdown as MM:SS (or H:MM when β₯ 1h).
|
|
101
101
|
*/
|
|
102
102
|
function formatTimer(remainingSec) {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Defensive: non-finite/NaN inputs (e.g. clock skew, stringified Date) used
|
|
104
|
+
// to slip through and render as "NaN:NaN" or stretched seconds. Treat any
|
|
105
|
+
// weird input as expired rather than rendering garbage in the statusline.
|
|
106
|
+
if (!Number.isFinite(remainingSec) || remainingSec <= 0) return 'EXPIRED';
|
|
107
|
+
const totalSec = Math.max(0, Math.floor(remainingSec));
|
|
105
108
|
const h = Math.floor(totalSec / 3600);
|
|
106
|
-
const
|
|
107
|
-
const
|
|
109
|
+
const mRaw = Math.floor((totalSec % 3600) / 60);
|
|
110
|
+
const sRaw = totalSec % 60;
|
|
111
|
+
// Clamp explicitly so a future regression in the math (or padStart no-op
|
|
112
|
+
// truncation) can never produce m:sss like "4:547".
|
|
113
|
+
const m = Math.min(59, Math.max(0, mRaw));
|
|
114
|
+
const s = Math.min(59, Math.max(0, sRaw));
|
|
108
115
|
if (h > 0) return `${h}:${String(m).padStart(2, '0')}`;
|
|
109
116
|
return `${m}:${String(s).padStart(2, '0')}`;
|
|
110
117
|
}
|
|
@@ -195,8 +202,18 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
195
202
|
// icon verbose: "β³ Expires 1h 59:58"
|
|
196
203
|
let ttlSeg;
|
|
197
204
|
if (timer && lastActivity) {
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
// Coerce to a numeric ms timestamp. Some upstream paths handed in a Date,
|
|
206
|
+
// a stringified ISO timestamp, or epoch-seconds β any of which silently
|
|
207
|
+
// produces NaN/huge values when subtracted from Date.now(), which then
|
|
208
|
+
// bypasses formatTimer's normal MM:SS shape.
|
|
209
|
+
const laMs =
|
|
210
|
+
typeof lastActivity === 'number'
|
|
211
|
+
? (lastActivity < 1e12 ? lastActivity * 1000 : lastActivity) // seconds β ms
|
|
212
|
+
: (lastActivity instanceof Date ? lastActivity.getTime() : Date.parse(lastActivity));
|
|
213
|
+
const elapsed = Number.isFinite(laMs) ? (Date.now() - laMs) / 1000 : Infinity;
|
|
214
|
+
// Clamp remaining into the bucket so a clock-skew or stale-state edge case
|
|
215
|
+
// can't display a value larger than the bucket itself.
|
|
216
|
+
const remaining = Math.min(ttlSeconds, ttlSeconds - elapsed);
|
|
200
217
|
const text = formatTimer(remaining);
|
|
201
218
|
const pct = remaining / ttlSeconds;
|
|
202
219
|
const timerColor =
|
package/src/installer.js
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
* exist on every platform.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
16
|
+
import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { claudeUserDir } from './paths.js';
|
|
19
19
|
|
|
20
|
+
const STATUSLINE_COMMAND = 'claude-token-saver --statusline --icon';
|
|
21
|
+
const STATUSLINE_REFRESH_INTERVAL = 5;
|
|
22
|
+
|
|
20
23
|
const SKILL_BODY = `---
|
|
21
24
|
name: claude-token-saver
|
|
22
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%", "β 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\`.
|
|
@@ -127,9 +130,63 @@ export function removeLegacyCommand() {
|
|
|
127
130
|
return { path: file, action: 'removed' };
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
// Registers/repairs the Claude Code statusLine entry in ~/.claude/settings.json.
|
|
134
|
+
// - No statusLine yet: insert ours with refreshInterval:1.
|
|
135
|
+
// - statusLine already points at claude-token-saver: ensure refreshInterval:1
|
|
136
|
+
// (this is the bit that makes the TTL countdown tick every second while idle).
|
|
137
|
+
// - statusLine points at a different command: leave it alone unless --force.
|
|
138
|
+
export function installStatusline({ force = false } = {}) {
|
|
139
|
+
const dir = claudeUserDir();
|
|
140
|
+
const file = join(dir, 'settings.json');
|
|
141
|
+
mkdirSync(dir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
let settings = {};
|
|
144
|
+
if (existsSync(file)) {
|
|
145
|
+
try {
|
|
146
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return { path: file, action: 'skipped', reason: `unreadable JSON (${e.message})` };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const cur = settings.statusLine;
|
|
153
|
+
const targetsUs = cur && typeof cur.command === 'string' && cur.command.includes('claude-token-saver');
|
|
154
|
+
|
|
155
|
+
if (!cur) {
|
|
156
|
+
settings.statusLine = {
|
|
157
|
+
type: 'command',
|
|
158
|
+
command: STATUSLINE_COMMAND,
|
|
159
|
+
refreshInterval: STATUSLINE_REFRESH_INTERVAL,
|
|
160
|
+
};
|
|
161
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
162
|
+
return { path: file, action: 'created' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (targetsUs) {
|
|
166
|
+
if (cur.refreshInterval === STATUSLINE_REFRESH_INTERVAL) {
|
|
167
|
+
return { path: file, action: 'exists' };
|
|
168
|
+
}
|
|
169
|
+
cur.refreshInterval = STATUSLINE_REFRESH_INTERVAL;
|
|
170
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
171
|
+
return { path: file, action: 'updated', reason: 'set refreshInterval=1' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!force) {
|
|
175
|
+
return { path: file, action: 'skipped', reason: `existing statusLine command (${cur.command}) β re-run with --force to overwrite` };
|
|
176
|
+
}
|
|
177
|
+
settings.statusLine = {
|
|
178
|
+
type: 'command',
|
|
179
|
+
command: STATUSLINE_COMMAND,
|
|
180
|
+
refreshInterval: STATUSLINE_REFRESH_INTERVAL,
|
|
181
|
+
};
|
|
182
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
183
|
+
return { path: file, action: 'updated', reason: 'replaced previous statusLine' };
|
|
184
|
+
}
|
|
185
|
+
|
|
130
186
|
export function installAll({ force = false } = {}) {
|
|
131
187
|
return {
|
|
132
188
|
skill: installSkill({ force }),
|
|
189
|
+
statusline: installStatusline({ force }),
|
|
133
190
|
legacy: removeLegacyCommand(),
|
|
134
191
|
};
|
|
135
192
|
}
|