claude-usage-limits 1.9.2 → 1.11.2
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +196 -25
- package/bin/cli.js +21 -1
- package/commands/panel.md +14 -0
- package/commands/statusline.md +15 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +36 -0
- package/skills/usage-limits/references/how-it-works.md +115 -2
- package/skills/usage-limits/scripts/activity.js +198 -0
- package/skills/usage-limits/scripts/bars.js +340 -0
- package/skills/usage-limits/scripts/brief.js +59 -6
- package/skills/usage-limits/scripts/feed.js +400 -0
- package/skills/usage-limits/scripts/live.js +489 -0
- package/skills/usage-limits/scripts/panel.js +731 -0
- package/skills/usage-limits/scripts/pulse.js +24 -0
- package/skills/usage-limits/scripts/recommend.js +56 -5
- package/skills/usage-limits/scripts/sessionend.js +5 -1
- package/skills/usage-limits/scripts/statusline.js +305 -0
- package/skills/usage-limits/scripts/stop.js +5 -1
- package/skills/usage-limits/scripts/tally.js +4 -10
- package/skills/usage-limits/scripts/usage.js +477 -17
- package/skills/usage-limits/scripts/view.js +222 -0
|
@@ -24,6 +24,8 @@ const path = require('path');
|
|
|
24
24
|
const usage = require('./usage.js');
|
|
25
25
|
const brief = require('./brief.js');
|
|
26
26
|
const host = require('./host.js');
|
|
27
|
+
const activity = require('./activity.js');
|
|
28
|
+
const live = require('./live.js');
|
|
27
29
|
|
|
28
30
|
const SECOND = 1000;
|
|
29
31
|
const DEFAULT_INTERVAL_SECONDS = 120;
|
|
@@ -116,6 +118,10 @@ async function run(now, hookInput) {
|
|
|
116
118
|
usage.setHost(host.detect(process.argv.slice(2), process.env));
|
|
117
119
|
|
|
118
120
|
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
121
|
+
// A tool call just finished, so the turn is still running. A few bytes, so
|
|
122
|
+
// the panel beside the chat keeps animating through a long turn.
|
|
123
|
+
activity.mark('working', sessionId, null, now);
|
|
124
|
+
|
|
119
125
|
const all = readState();
|
|
120
126
|
const every = intervalMs();
|
|
121
127
|
// The cheap path, and the one taken almost every time.
|
|
@@ -125,6 +131,24 @@ async function run(now, hookInput) {
|
|
|
125
131
|
// second tool call start another one.
|
|
126
132
|
writeState(trim(all, sessionId, now));
|
|
127
133
|
|
|
134
|
+
// A reading as old as the interval is replaced with the one Claude Code
|
|
135
|
+
// would take for /usage, so a turn that runs for an hour is measured
|
|
136
|
+
// against the account rather than against a guess from its own transcript.
|
|
137
|
+
if (!usage.isCodex()) {
|
|
138
|
+
try {
|
|
139
|
+
const cached = usage.collect(now);
|
|
140
|
+
await live.refreshIfStale({
|
|
141
|
+
now,
|
|
142
|
+
maxAgeMs: every,
|
|
143
|
+
cacheFetchedAtMs: cached.snapshotFetchedAt,
|
|
144
|
+
accountUuid: usage.accountUuid(),
|
|
145
|
+
timeoutMs: 4000,
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// The reading on disk is still there.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
const data = await usage.report(now, { sessionId });
|
|
129
153
|
const binding = data.binding;
|
|
130
154
|
if (!binding) return '';
|
|
@@ -32,15 +32,56 @@ const TIGHT_TURNS = 25;
|
|
|
32
32
|
|
|
33
33
|
const HOUR = 60 * 60 * 1000;
|
|
34
34
|
|
|
35
|
-
// Where the mechanical bulk should go when it is delegated.
|
|
36
|
-
//
|
|
37
|
-
|
|
35
|
+
// Where the mechanical bulk should go when it is delegated.
|
|
36
|
+
//
|
|
37
|
+
// The ladder below is an assumption: one tier down from whatever is doing the
|
|
38
|
+
// judgement, haiku being the floor. It is a reasonable assumption and it is
|
|
39
|
+
// still the fallback, but this account has measured the answer - what a call to
|
|
40
|
+
// each model has really cost here - and a measurement beats a ladder. On one
|
|
41
|
+
// machine the ladder said sonnet while the record said sonnet calls had cost
|
|
42
|
+
// $0.012 against Opus turns at $0.211, which is the same advice with a number
|
|
43
|
+
// behind it, and would have said haiku had haiku been the cheaper one.
|
|
44
|
+
function ladderModel(model) {
|
|
38
45
|
const name = String(model || '').toLowerCase();
|
|
39
46
|
if (name.indexOf('haiku') !== -1) return 'haiku';
|
|
40
47
|
if (name.indexOf('sonnet') !== -1) return 'haiku';
|
|
41
48
|
return 'sonnet';
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
// Whose family a model setting names, without requiring usage.js back: that
|
|
52
|
+
// would make these two modules a cycle.
|
|
53
|
+
function familyName(model) {
|
|
54
|
+
const name = String(model || '').toLowerCase();
|
|
55
|
+
for (const family of ['fable', 'mythos', 'opus', 'sonnet', 'haiku']) {
|
|
56
|
+
if (name.indexOf(family) !== -1) return family === 'mythos' ? 'fable' : family;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function delegateModel(model, headroom) {
|
|
62
|
+
const mine = familyName(model);
|
|
63
|
+
const measured = (headroom || []).filter(
|
|
64
|
+
(row) =>
|
|
65
|
+
row &&
|
|
66
|
+
row.family &&
|
|
67
|
+
row.family !== mine &&
|
|
68
|
+
Number.isFinite(row.usdPerTurn) &&
|
|
69
|
+
row.usdPerTurn > 0 &&
|
|
70
|
+
// A model whose own weekly is spent is not somewhere to send work.
|
|
71
|
+
!(row.ownWindow && row.turnsLeft === 0)
|
|
72
|
+
);
|
|
73
|
+
if (!measured.length) return ladderModel(model);
|
|
74
|
+
|
|
75
|
+
const cheapest = measured.reduce((best, row) => (row.usdPerTurn < best.usdPerTurn ? row : best));
|
|
76
|
+
// Never up the ladder. If everything measured is dearer than what is already
|
|
77
|
+
// running, delegating buys nothing and the assumption is the better answer.
|
|
78
|
+
const here = (headroom || []).find((row) => row && row.family === mine);
|
|
79
|
+
if (here && Number.isFinite(here.usdPerTurn) && cheapest.usdPerTurn >= here.usdPerTurn) {
|
|
80
|
+
return ladderModel(model);
|
|
81
|
+
}
|
|
82
|
+
return cheapest.family;
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
// The effort actually in force. settings.json says 'default' when nothing is
|
|
45
86
|
// set, and the measured dominant effort of recent turns is better evidence
|
|
46
87
|
// than a guess; xhigh is what Claude Code defaults to when neither knows.
|
|
@@ -163,10 +204,17 @@ function decide(inputs) {
|
|
|
163
204
|
// then it lands in settings.json for the next session: switching the running
|
|
164
205
|
// session's model mid-task invalidates the prompt cache, so the change
|
|
165
206
|
// belongs at a session boundary.
|
|
166
|
-
base.model.delegate = delegateModel(modelNow);
|
|
207
|
+
base.model.delegate = delegateModel(modelNow, inputs.headroom);
|
|
208
|
+
const mine = (inputs.headroom || []).find((row) => row && row.family === familyName(modelNow));
|
|
209
|
+
const there = (inputs.headroom || []).find((row) => row && row.family === base.model.delegate);
|
|
210
|
+
// With both prices measured, say the saving rather than asserting one.
|
|
211
|
+
const saving =
|
|
212
|
+
mine && there && Number.isFinite(mine.usdPerTurn) && Number.isFinite(there.usdPerTurn) && there.usdPerTurn > 0
|
|
213
|
+
? ', measured here at ' + Math.round(mine.usdPerTurn / there.usdPerTurn) + 'x cheaper'
|
|
214
|
+
: '';
|
|
167
215
|
base.model.why =
|
|
168
216
|
'keep ' + (modelNow === 'default' ? 'the current model' : modelNow) +
|
|
169
|
-
' for the judgement; the saving is in where the mechanical bulk runs';
|
|
217
|
+
' for the judgement; the saving is in where the mechanical bulk runs' + saving;
|
|
170
218
|
if (base.posture === 'critical' && base.model.delegate !== 'haiku') {
|
|
171
219
|
base.model.nextSession = 'sonnet';
|
|
172
220
|
}
|
|
@@ -213,6 +261,7 @@ function decide(inputs) {
|
|
|
213
261
|
function fromReport(data, turns) {
|
|
214
262
|
return {
|
|
215
263
|
binding: (data && data.binding) || null,
|
|
264
|
+
headroom: (data && data.modelHeadroom) || null,
|
|
216
265
|
rates: (data && data.rates) || null,
|
|
217
266
|
settings: (data && data.settings) || {},
|
|
218
267
|
recentEffort: data && data.recent ? data.recent.effort : null,
|
|
@@ -285,6 +334,8 @@ function renderRecommend(data, turns) {
|
|
|
285
334
|
module.exports = {
|
|
286
335
|
decide,
|
|
287
336
|
fromReport,
|
|
337
|
+
familyName,
|
|
338
|
+
ladderModel,
|
|
288
339
|
renderRecommend,
|
|
289
340
|
delegateModel,
|
|
290
341
|
currentEffort,
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
const usage = require('./usage.js');
|
|
12
12
|
const host = require('./host.js');
|
|
13
13
|
const tally = require('./tally.js');
|
|
14
|
+
const activity = require('./activity.js');
|
|
14
15
|
|
|
15
16
|
async function run(now, hookInput) {
|
|
17
|
+
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
18
|
+
// The session is over, so as far as the panel is concerned it is idle.
|
|
19
|
+
activity.mark('idle', sessionId, null, now);
|
|
20
|
+
|
|
16
21
|
if (String(process.env.USAGE_LIMITS_TALLY || '').toLowerCase() === 'off') return '';
|
|
17
22
|
usage.setHost(host.detect(process.argv.slice(2), process.env));
|
|
18
23
|
|
|
19
|
-
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
20
24
|
const transcript = hookInput && hookInput.transcript_path ? hookInput.transcript_path : null;
|
|
21
25
|
if (!sessionId || !transcript) return '';
|
|
22
26
|
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Puts the bars under the Claude Code prompt, and takes them out again.
|
|
5
|
+
//
|
|
6
|
+
// node scripts/statusline.js status
|
|
7
|
+
// node scripts/statusline.js on [--refresh N] [--no-chain] [--dry-run]
|
|
8
|
+
// node scripts/statusline.js off
|
|
9
|
+
//
|
|
10
|
+
// `on` points Claude Code's statusLine setting at a small launcher written into
|
|
11
|
+
// the config directory. The launcher finds wherever this plugin is currently
|
|
12
|
+
// installed and runs feed.js from there, so a plugin update that moves the
|
|
13
|
+
// install directory does not leave the status line pointing at a folder that
|
|
14
|
+
// no longer exists.
|
|
15
|
+
//
|
|
16
|
+
// A status line that was already there is kept: it is recorded, and feed.js
|
|
17
|
+
// runs it first and prints its output above ours, unless --no-chain. `off`
|
|
18
|
+
// puts back exactly what was there, including nothing.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
function configDir() {
|
|
25
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function settingsFile() {
|
|
29
|
+
return path.join(configDir(), 'settings.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stateFile() {
|
|
33
|
+
return path.join(configDir(), 'usage-limits-statusline.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function launcherFile() {
|
|
37
|
+
return path.join(configDir(), 'usage-limits-statusline.js');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function feedFile() {
|
|
41
|
+
return path.join(__dirname, 'feed.js');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJson(file) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.code === 'ENOENT') return null;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readState() {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
|
|
56
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Replace through a temporary file so an interrupted run cannot leave
|
|
63
|
+
// settings.json half written.
|
|
64
|
+
function writeJson(file, value) {
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
const temp = file + '.usage-limits-tmp';
|
|
67
|
+
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
68
|
+
fs.renameSync(temp, file);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The launcher. It is deliberately dumb: find feed.js, hand over, and if that
|
|
72
|
+
// is not possible print nothing and exit 0, because a status line that shows
|
|
73
|
+
// an error every 300ms is worse than one that is blank.
|
|
74
|
+
function launcherSource(recordedFeed) {
|
|
75
|
+
return [
|
|
76
|
+
'#!/usr/bin/env node',
|
|
77
|
+
"'use strict';",
|
|
78
|
+
'// Written by claude-usage-limits (statusline on). Finds the current install and',
|
|
79
|
+
'// runs its status line. Remove with: claude-usage-limits statusline off',
|
|
80
|
+
"const fs = require('fs');",
|
|
81
|
+
"const os = require('os');",
|
|
82
|
+
"const path = require('path');",
|
|
83
|
+
"const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');",
|
|
84
|
+
'const candidates = [];',
|
|
85
|
+
'try {',
|
|
86
|
+
" const installed = JSON.parse(fs.readFileSync(path.join(configDir, 'plugins', 'installed_plugins.json'), 'utf8'));",
|
|
87
|
+
' for (const key of Object.keys((installed && installed.plugins) || {})) {',
|
|
88
|
+
" if (key.indexOf('usage-limits@') !== 0) continue;",
|
|
89
|
+
' for (const entry of installed.plugins[key] || []) {',
|
|
90
|
+
" if (entry && entry.installPath) candidates.push(path.join(entry.installPath, 'skills', 'usage-limits', 'scripts', 'feed.js'));",
|
|
91
|
+
' }',
|
|
92
|
+
' }',
|
|
93
|
+
'} catch (err) {}',
|
|
94
|
+
"candidates.push(path.join(configDir, 'skills', 'usage-limits', 'scripts', 'feed.js'));",
|
|
95
|
+
'candidates.push(' + JSON.stringify(recordedFeed) + ');',
|
|
96
|
+
'let feed = null;',
|
|
97
|
+
'for (const file of candidates) {',
|
|
98
|
+
' try {',
|
|
99
|
+
' if (fs.statSync(file).isFile()) {',
|
|
100
|
+
' feed = file;',
|
|
101
|
+
' break;',
|
|
102
|
+
' }',
|
|
103
|
+
' } catch (err) {}',
|
|
104
|
+
'}',
|
|
105
|
+
'if (!feed) process.exit(0);',
|
|
106
|
+
'try {',
|
|
107
|
+
' Promise.resolve(require(feed).main(process.argv.slice(2))).then(',
|
|
108
|
+
' (code) => process.exit(code || 0),',
|
|
109
|
+
' () => process.exit(0)',
|
|
110
|
+
' );',
|
|
111
|
+
'} catch (err) {',
|
|
112
|
+
' process.exit(0);',
|
|
113
|
+
'}',
|
|
114
|
+
'',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseArgs(argv) {
|
|
119
|
+
const args = { command: null, refresh: null, chain: true, dryRun: false };
|
|
120
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
121
|
+
const arg = argv[i];
|
|
122
|
+
if (arg === '--dry-run') args.dryRun = true;
|
|
123
|
+
else if (arg === '--no-chain') args.chain = false;
|
|
124
|
+
else if (arg === '--refresh') args.refresh = Number(argv[++i]);
|
|
125
|
+
else if (arg.indexOf('--refresh=') === 0) args.refresh = Number(arg.slice('--refresh='.length));
|
|
126
|
+
else if (!args.command) args.command = arg;
|
|
127
|
+
}
|
|
128
|
+
if (!args.command) args.command = 'status';
|
|
129
|
+
return args;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isOurs(statusLine, launcher) {
|
|
133
|
+
return Boolean(
|
|
134
|
+
statusLine &&
|
|
135
|
+
statusLine.type === 'command' &&
|
|
136
|
+
typeof statusLine.command === 'string' &&
|
|
137
|
+
statusLine.command.indexOf(path.basename(launcher)) !== -1
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Work out the settings to write without touching disk.
|
|
142
|
+
function planOn(settings, state, options) {
|
|
143
|
+
const current = settings || {};
|
|
144
|
+
const opts = options || {};
|
|
145
|
+
const launcher = opts.launcher || launcherFile();
|
|
146
|
+
const existing = current.statusLine === undefined ? null : current.statusLine;
|
|
147
|
+
// A second "on" must not record our own launcher as the thing to restore.
|
|
148
|
+
const previous =
|
|
149
|
+
state && Object.prototype.hasOwnProperty.call(state, 'previous')
|
|
150
|
+
? state.previous
|
|
151
|
+
: isOurs(existing, launcher)
|
|
152
|
+
? null
|
|
153
|
+
: existing;
|
|
154
|
+
const command = 'node ' + JSON.stringify(launcher);
|
|
155
|
+
const statusLine = { type: 'command', command };
|
|
156
|
+
if (Number.isFinite(opts.refresh) && opts.refresh >= 1) statusLine.refreshInterval = Math.floor(opts.refresh);
|
|
157
|
+
const chain =
|
|
158
|
+
opts.chain !== false &&
|
|
159
|
+
Boolean(previous && previous.type === 'command' && typeof previous.command === 'string' && previous.command);
|
|
160
|
+
const next = Object.assign({}, current, { statusLine });
|
|
161
|
+
const changes = [];
|
|
162
|
+
if (JSON.stringify(existing) !== JSON.stringify(statusLine)) {
|
|
163
|
+
changes.push('statusLine: ' + (existing ? JSON.stringify(existing) : '(unset)') + ' -> ' + JSON.stringify(statusLine));
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
settings: next,
|
|
167
|
+
state: {
|
|
168
|
+
installedAt: new Date().toISOString(),
|
|
169
|
+
previous,
|
|
170
|
+
chain,
|
|
171
|
+
launcher,
|
|
172
|
+
feed: opts.feed || feedFile(),
|
|
173
|
+
},
|
|
174
|
+
changes,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function planOff(settings, state) {
|
|
179
|
+
const current = settings || {};
|
|
180
|
+
const next = Object.assign({}, current);
|
|
181
|
+
const previous = state && Object.prototype.hasOwnProperty.call(state, 'previous') ? state.previous : null;
|
|
182
|
+
const changes = [];
|
|
183
|
+
if (previous === null || previous === undefined) {
|
|
184
|
+
if (next.statusLine !== undefined) {
|
|
185
|
+
changes.push('statusLine: removed');
|
|
186
|
+
delete next.statusLine;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
changes.push('statusLine: restored');
|
|
190
|
+
next.statusLine = previous;
|
|
191
|
+
}
|
|
192
|
+
return { settings: next, changes };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function backupOnce(file) {
|
|
196
|
+
const backup = file + '.usage-limits-backup';
|
|
197
|
+
if (fs.existsSync(file) && !fs.existsSync(backup)) fs.copyFileSync(file, backup);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function describeStatus(settings, state) {
|
|
201
|
+
const current = (settings && settings.statusLine) || null;
|
|
202
|
+
const launcher = launcherFile();
|
|
203
|
+
const ours = isOurs(current, launcher);
|
|
204
|
+
const lines = [];
|
|
205
|
+
if (ours) {
|
|
206
|
+
lines.push('status line: on');
|
|
207
|
+
lines.push(' command ' + current.command);
|
|
208
|
+
if (current.refreshInterval) lines.push(' refresh every ' + current.refreshInterval + 's');
|
|
209
|
+
if (state && state.chain && state.previous && state.previous.command) {
|
|
210
|
+
lines.push(' chained ' + state.previous.command);
|
|
211
|
+
} else if (state && state.previous) {
|
|
212
|
+
lines.push(' replaced ' + JSON.stringify(state.previous) + ' (restored by "off")');
|
|
213
|
+
}
|
|
214
|
+
} else if (current) {
|
|
215
|
+
lines.push('status line: another command is set');
|
|
216
|
+
lines.push(' command ' + (current.command || JSON.stringify(current)));
|
|
217
|
+
lines.push(' "on" keeps it and prints its output above ours; --no-chain replaces it');
|
|
218
|
+
} else {
|
|
219
|
+
lines.push('status line: off');
|
|
220
|
+
lines.push(' "on" installs it; settings.json is edited through a temporary file and backed up once');
|
|
221
|
+
}
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function main(argv) {
|
|
226
|
+
const args = parseArgs(argv || []);
|
|
227
|
+
const file = settingsFile();
|
|
228
|
+
let settings;
|
|
229
|
+
try {
|
|
230
|
+
settings = readJson(file) || {};
|
|
231
|
+
} catch (err) {
|
|
232
|
+
process.stderr.write('statusline: could not read ' + file + ': ' + err.message + '\n');
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
const state = readState();
|
|
236
|
+
|
|
237
|
+
if (args.command === 'status') {
|
|
238
|
+
process.stdout.write(describeStatus(settings, state) + '\n');
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (args.command === 'on') {
|
|
243
|
+
if (args.refresh !== null && (!Number.isFinite(args.refresh) || args.refresh < 1)) {
|
|
244
|
+
process.stderr.write('statusline: --refresh takes a whole number of seconds, 1 or more\n');
|
|
245
|
+
return 2;
|
|
246
|
+
}
|
|
247
|
+
const planned = planOn(settings, state, { refresh: args.refresh, chain: args.chain });
|
|
248
|
+
if (args.dryRun) {
|
|
249
|
+
process.stdout.write((planned.changes.length ? planned.changes.join('\n') : 'no change') + '\n');
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
backupOnce(file);
|
|
253
|
+
fs.mkdirSync(path.dirname(planned.state.launcher), { recursive: true });
|
|
254
|
+
fs.writeFileSync(planned.state.launcher, launcherSource(planned.state.feed), 'utf8');
|
|
255
|
+
writeJson(file, planned.settings);
|
|
256
|
+
writeJson(stateFile(), planned.state);
|
|
257
|
+
process.stdout.write(
|
|
258
|
+
(planned.changes.length ? planned.changes.join('\n') : 'already on') +
|
|
259
|
+
'\nThe bars appear under the prompt in new Claude Code sessions' +
|
|
260
|
+
(planned.state.chain ? ', above the status line that was already there' : '') +
|
|
261
|
+
'.\n'
|
|
262
|
+
);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (args.command === 'off') {
|
|
267
|
+
const planned = planOff(settings, state);
|
|
268
|
+
if (args.dryRun) {
|
|
269
|
+
process.stdout.write((planned.changes.length ? planned.changes.join('\n') : 'no change') + '\n');
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
if (planned.changes.length) writeJson(file, planned.settings);
|
|
273
|
+
for (const stale of [stateFile(), launcherFile()]) {
|
|
274
|
+
try {
|
|
275
|
+
fs.unlinkSync(stale);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
// Already gone is the state we want.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
process.stdout.write((planned.changes.length ? planned.changes.join('\n') : 'already off') + '\n');
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
process.stderr.write('statusline: unknown command "' + args.command + '". Use status, on or off.\n');
|
|
285
|
+
return 2;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
settingsFile,
|
|
290
|
+
stateFile,
|
|
291
|
+
launcherFile,
|
|
292
|
+
feedFile,
|
|
293
|
+
readState,
|
|
294
|
+
launcherSource,
|
|
295
|
+
parseArgs,
|
|
296
|
+
isOurs,
|
|
297
|
+
planOn,
|
|
298
|
+
planOff,
|
|
299
|
+
describeStatus,
|
|
300
|
+
main,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (require.main === module) {
|
|
304
|
+
process.exitCode = main(process.argv.slice(2));
|
|
305
|
+
}
|
|
@@ -15,12 +15,16 @@
|
|
|
15
15
|
const usage = require('./usage.js');
|
|
16
16
|
const host = require('./host.js');
|
|
17
17
|
const tally = require('./tally.js');
|
|
18
|
+
const activity = require('./activity.js');
|
|
18
19
|
|
|
19
20
|
async function run(now, hookInput) {
|
|
21
|
+
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
22
|
+
// The reply is finished: the panel beside the chat can stop animating.
|
|
23
|
+
activity.mark('idle', sessionId, null, now);
|
|
24
|
+
|
|
20
25
|
if (String(process.env.USAGE_LIMITS_TALLY || '').toLowerCase() === 'off') return '';
|
|
21
26
|
usage.setHost(host.detect(process.argv.slice(2), process.env));
|
|
22
27
|
|
|
23
|
-
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
24
28
|
const transcript = hookInput && hookInput.transcript_path ? hookInput.transcript_path : null;
|
|
25
29
|
if (!sessionId || !transcript) return '';
|
|
26
30
|
|
|
@@ -123,18 +123,12 @@ function readNewLines(file, from) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Where Claude Code writes the transcripts of the subagents a session spawned.
|
|
126
|
+
// Plain subagents write straight into <session>/subagents/; the agents a
|
|
127
|
+
// Workflow runs write under subagents/workflows/<run id>/. Both spend this
|
|
128
|
+
// session's budget.
|
|
126
129
|
function subagentFiles(transcriptPath, sessionId) {
|
|
127
130
|
const dir = path.join(path.dirname(transcriptPath), sessionId, 'subagents');
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
names = fs.readdirSync(dir);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
return names
|
|
135
|
-
.filter((name) => name.endsWith('.jsonl'))
|
|
136
|
-
.sort()
|
|
137
|
-
.map((name) => path.join(dir, name));
|
|
131
|
+
return usage.subagentTranscripts(dir, 0).sort();
|
|
138
132
|
}
|
|
139
133
|
|
|
140
134
|
function apply(session, event, sidechain, delta) {
|