claude-usage-limits 1.39.2 → 1.39.4
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/package.json +1 -1
- package/skills/usage-limits/scripts/agy-hook.js +15 -8
- package/skills/usage-limits/scripts/brief.js +43 -14
- package/skills/usage-limits/scripts/ceiling.js +2 -1
- package/skills/usage-limits/scripts/codex.js +27 -2
- package/skills/usage-limits/scripts/host.js +14 -0
- package/skills/usage-limits/scripts/install-antigravity.js +3 -1
- package/skills/usage-limits/scripts/pulse.js +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.39.
|
|
4
|
+
"version": "1.39.4",
|
|
5
5
|
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Ridelink",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
|
-
"version": "1.39.
|
|
3
|
+
"version": "1.39.4",
|
|
4
4
|
"description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ridelink",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.39.
|
|
3
|
+
"version": "1.39.4",
|
|
4
4
|
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -33,7 +33,12 @@ const usage = require('./usage.js');
|
|
|
33
33
|
const host = require('./host.js');
|
|
34
34
|
const mode = require('./mode.js');
|
|
35
35
|
const ceiling = require('./ceiling.js');
|
|
36
|
-
|
|
36
|
+
// brief.js is loaded only on PreInvocation: PreToolUse runs before tool calls
|
|
37
|
+
// and must stay a few small requires, not the whole brief.
|
|
38
|
+
|
|
39
|
+
// PreToolUse must always name a decision. Antigravity reads an answer with no
|
|
40
|
+
// decision as a refusal, so the old bare {} blocked every tool it ran on.
|
|
41
|
+
const ALLOW = { decision: 'allow' };
|
|
37
42
|
|
|
38
43
|
const EVENTS = ['PreInvocation', 'PostInvocation', 'PreToolUse', 'PostToolUse', 'Stop'];
|
|
39
44
|
|
|
@@ -116,14 +121,14 @@ async function run(now, input, argv) {
|
|
|
116
121
|
usage.setHost(host.GEMINI);
|
|
117
122
|
const sessionId = input && input.conversationId ? String(input.conversationId) : null;
|
|
118
123
|
const budget = mode.forSession({ sessionId });
|
|
119
|
-
if (budget.policy.briefStyle === 'none') return {};
|
|
124
|
+
if (budget.policy.briefStyle === 'none') return event === 'PreToolUse' ? ALLOW : {};
|
|
120
125
|
|
|
121
126
|
if (event === 'PreToolUse') {
|
|
122
127
|
const tool = toolNameOf(input);
|
|
123
|
-
if (!ceiling.isMultiplier(tool)) return
|
|
128
|
+
if (!ceiling.isMultiplier(tool)) return ALLOW;
|
|
124
129
|
const at = ceiling.assess(Object.assign({ state: budget.state, env: process.env, sessionId }, reading(percentNow(now))));
|
|
125
130
|
const call = ceiling.verdict(at, tool);
|
|
126
|
-
if (call.decision !== 'deny') return
|
|
131
|
+
if (call.decision !== 'deny') return ALLOW;
|
|
127
132
|
return { decision: 'deny', reason: call.reason };
|
|
128
133
|
}
|
|
129
134
|
|
|
@@ -133,7 +138,7 @@ async function run(now, input, argv) {
|
|
|
133
138
|
// transient system message, which is exactly what a per-turn figure is.
|
|
134
139
|
let text = '';
|
|
135
140
|
try {
|
|
136
|
-
text = await brief.run(now, input);
|
|
141
|
+
text = await require('./brief.js').run(now, input, { host: host.GEMINI });
|
|
137
142
|
} catch (err) {
|
|
138
143
|
text = '';
|
|
139
144
|
}
|
|
@@ -162,11 +167,13 @@ if (require.main === module) {
|
|
|
162
167
|
},
|
|
163
168
|
() => {
|
|
164
169
|
// Hooks block the agent loop here, so a failure has to be silent and
|
|
165
|
-
// well formed rather than loud
|
|
166
|
-
|
|
170
|
+
// well formed rather than loud - and on PreToolUse, well formed means
|
|
171
|
+
// an explicit allow, never a bare {} that reads as a refusal.
|
|
172
|
+
const pre = process.argv.slice(2).join(' ').includes('--event PreToolUse');
|
|
173
|
+
process.stdout.write(JSON.stringify(pre ? ALLOW : {}) + '\n');
|
|
167
174
|
process.exit(0);
|
|
168
175
|
}
|
|
169
176
|
);
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
module.exports = { EVENTS, eventFrom, toolNameOf, percentNow, run };
|
|
179
|
+
module.exports = { EVENTS, ALLOW, eventFrom, toolNameOf, percentNow, run };
|
|
@@ -686,11 +686,19 @@ function briefText(input) {
|
|
|
686
686
|
if (Number.isFinite(parts.turnsLeft)) {
|
|
687
687
|
// Another session spending the same budget means fewer of those turns are
|
|
688
688
|
// yours, so say both numbers rather than the flattering one.
|
|
689
|
+
// The stance below is judged on yourTurnsLeft, so whenever that is the
|
|
690
|
+
// smaller number it has to be on the line too. Printing only the window's
|
|
691
|
+
// figure is how a brief came to say "the budget is nearly gone" beside
|
|
692
|
+
// "about 594 turns of headroom": both were true, of different numbers.
|
|
693
|
+
const yours = Number.isFinite(parts.yourTurnsLeft) ? parts.yourTurnsLeft : null;
|
|
694
|
+
const fewerYours = yours !== null && yours < parts.turnsLeft * 0.75;
|
|
689
695
|
const shared =
|
|
690
|
-
parts.sessions > 1 &&
|
|
691
|
-
? ' (' + parts.sessions + ' sessions active, roughly ' +
|
|
696
|
+
parts.sessions > 1 && yours !== null
|
|
697
|
+
? ' (' + parts.sessions + ' sessions active, roughly ' + yours +
|
|
692
698
|
' of them yours)'
|
|
693
|
-
:
|
|
699
|
+
: fewerYours
|
|
700
|
+
? ' (about ' + count(yours, 'turn') + ' of that yours at this context size)'
|
|
701
|
+
: '';
|
|
694
702
|
bound.push('about ' + count(parts.turnsLeft, 'turn') + ' of headroom' + shared);
|
|
695
703
|
// A turn count is a poor sense of urgency when several agents are spending
|
|
696
704
|
// at once: two hundred turns sounds like plenty and can be gone in ten
|
|
@@ -1411,7 +1419,12 @@ function fastModeFor(sessionId) {
|
|
|
1411
1419
|
}
|
|
1412
1420
|
}
|
|
1413
1421
|
|
|
1414
|
-
|
|
1422
|
+
const GEMINI_UNREADABLE =
|
|
1423
|
+
"[usage-limits] Antigravity's quota is not readable by this plugin, so there is no budget figure and no fan-out ceiling here; Antigravity's own usage panel has the number.";
|
|
1424
|
+
const LIMIT_REACHED =
|
|
1425
|
+
'[usage-limits] The meter says the usage limit is reached. Start nothing new: save the work, write the hand-off note in this turn, and end the turn.';
|
|
1426
|
+
|
|
1427
|
+
async function run(now, hookInput, opts) {
|
|
1415
1428
|
if (String(process.env.USAGE_LIMITS_BRIEF || '').toLowerCase() === 'off') return '';
|
|
1416
1429
|
|
|
1417
1430
|
const sessionId = hookInput && (hookInput.session_id || hookInput.conversationId) ? (hookInput.session_id || hookInput.conversationId) : null;
|
|
@@ -1424,15 +1437,21 @@ async function run(now, hookInput) {
|
|
|
1424
1437
|
const budget = mode.forSession({ sessionId });
|
|
1425
1438
|
if (budget.policy.briefStyle === 'none') return guardLine(now, budget);
|
|
1426
1439
|
|
|
1427
|
-
// Settle host here, before any file is read.
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1440
|
+
// Settle host here, before any file is read. A caller that already knows the
|
|
1441
|
+
// host (agy-hook runs only inside Antigravity) says so, and that wins over
|
|
1442
|
+
// detection: an empty or unparseable payload used to fall through to
|
|
1443
|
+
// Claude Code and put Claude's usage, model and session into Antigravity.
|
|
1444
|
+
const detectedHost = host.detectFromHook(process.argv.slice(2), process.env, hookInput);
|
|
1445
|
+
const effectiveHost = (opts && opts.host)
|
|
1446
|
+
|| ((hookInput && (hookInput.conversationId || hookInput.invocationNum !== undefined))
|
|
1447
|
+
? host.GEMINI
|
|
1448
|
+
: detectedHost);
|
|
1449
|
+
const gemini = usage.setHost(effectiveHost) === host.GEMINI;
|
|
1433
1450
|
// A prompt has arrived, so this session is working, and the prompt itself
|
|
1434
|
-
// says whether it asked for ultracode. The panel animates from this.
|
|
1435
|
-
activity
|
|
1451
|
+
// says whether it asked for ultracode. The panel animates from this. The
|
|
1452
|
+
// activity file is Claude Code's, so an Antigravity conversation is never
|
|
1453
|
+
// counted in it.
|
|
1454
|
+
if (!gemini) activity.mark(
|
|
1436
1455
|
'working',
|
|
1437
1456
|
sessionId,
|
|
1438
1457
|
Object.assign(
|
|
@@ -1469,7 +1488,11 @@ async function run(now, hookInput) {
|
|
|
1469
1488
|
// one quick failure and then a widening backoff, never a wait on every
|
|
1470
1489
|
// prompt; USAGE_LIMITS_FETCH=off turns it off.
|
|
1471
1490
|
try {
|
|
1472
|
-
if (
|
|
1491
|
+
if (gemini) {
|
|
1492
|
+
// Antigravity publishes no meter this plugin can read, and refreshing
|
|
1493
|
+
// Claude's /usage reading from inside Antigravity spends a request on
|
|
1494
|
+
// the wrong account.
|
|
1495
|
+
} else if (usage.isCodex()) {
|
|
1473
1496
|
// Codex only writes its meter when it makes a request, so between turns
|
|
1474
1497
|
// the newest figure can be half an hour old. Ask it, the way /status
|
|
1475
1498
|
// does, when the reading has aged.
|
|
@@ -1492,7 +1515,13 @@ async function run(now, hookInput) {
|
|
|
1492
1515
|
// The reading on disk is still there.
|
|
1493
1516
|
}
|
|
1494
1517
|
const base = usage.collect(now);
|
|
1495
|
-
if (!base.utilization)
|
|
1518
|
+
if (!base.utilization) {
|
|
1519
|
+
if (gemini) return sayOnce(sessionId, GEMINI_UNREADABLE, now);
|
|
1520
|
+
// Codex can say a limit is already hit while giving no percentage; that
|
|
1521
|
+
// is the plainest reading there is, not a missing one.
|
|
1522
|
+
if (base.reachedType || base.spendControlReached) return sayOnce(sessionId, LIMIT_REACHED, now);
|
|
1523
|
+
return '';
|
|
1524
|
+
}
|
|
1496
1525
|
|
|
1497
1526
|
const all = readCache();
|
|
1498
1527
|
// Which setting the fit question was already asked for, read BEFORE anything
|
|
@@ -50,7 +50,8 @@ const MULTIPLIERS = [
|
|
|
50
50
|
// Antigravity derives tool names by lowercasing the step type and stripping
|
|
51
51
|
// CORTEX_STEP_TYPE_, so its names are snake_case and cannot be matched by the
|
|
52
52
|
// list above.
|
|
53
|
-
|
|
53
|
+
// invoke_subagent and browser_subagent are the two Antigravity fan-out tools.
|
|
54
|
+
const MULTIPLIER_PATTERN = /^(agent|task|workflow|subagent|dispatch|spawn_[a-z_]*agent|run_[a-z_]*agent|invoke_subagent|browser_subagent)$/i;
|
|
54
55
|
|
|
55
56
|
// How far below the ceiling the warning starts. Ten points is about one long
|
|
56
57
|
// turn at xhigh on a 5-hour window, which is the last moment a warning can
|
|
@@ -777,6 +777,30 @@ function liveFile() {
|
|
|
777
777
|
return path.join(homeDir(), 'usage-limits-codex-live.json');
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// The app-server's account/rateLimits/read answers in camelCase (usedPercent,
|
|
781
|
+
// windowDurationMins, resetsAt, planType ...) while rollouts and every reader
|
|
782
|
+
// here use snake_case. Stored as received, a fresh live reading was invisible
|
|
783
|
+
// to readingsOf() and the brief, pulse and ceiling all went blind on Codex.
|
|
784
|
+
// One conversion, applied where a reading is taken and again where the file
|
|
785
|
+
// is read back, because a camelCase file may already be on disk.
|
|
786
|
+
const METER_RENAMES = { window_duration_mins: 'window_minutes' };
|
|
787
|
+
function snakeKey(key) {
|
|
788
|
+
const snake = String(key).replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
|
|
789
|
+
return METER_RENAMES[snake] || snake;
|
|
790
|
+
}
|
|
791
|
+
function normalizeMeter(meter) {
|
|
792
|
+
if (Array.isArray(meter)) return meter.map(normalizeMeter);
|
|
793
|
+
if (!meter || typeof meter !== 'object') return meter;
|
|
794
|
+
const out = {};
|
|
795
|
+
for (const [key, value] of Object.entries(meter)) {
|
|
796
|
+
const name = snakeKey(key);
|
|
797
|
+
// A snake_case field that is already there wins over a converted twin.
|
|
798
|
+
if (Object.prototype.hasOwnProperty.call(out, name) && name !== key) continue;
|
|
799
|
+
out[name] = normalizeMeter(value);
|
|
800
|
+
}
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
|
|
780
804
|
function readLiveMeter() {
|
|
781
805
|
let parsed;
|
|
782
806
|
try {
|
|
@@ -786,7 +810,7 @@ function readLiveMeter() {
|
|
|
786
810
|
}
|
|
787
811
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
788
812
|
if (!Number.isFinite(parsed.at) || !parsed.meter || typeof parsed.meter !== 'object') return null;
|
|
789
|
-
return parsed;
|
|
813
|
+
return Object.assign({}, parsed, { meter: normalizeMeter(parsed.meter) });
|
|
790
814
|
}
|
|
791
815
|
|
|
792
816
|
function writeLiveMeter(reading) {
|
|
@@ -1045,7 +1069,7 @@ function refresh(options) {
|
|
|
1045
1069
|
return;
|
|
1046
1070
|
}
|
|
1047
1071
|
const limits = message.result && message.result.rateLimits;
|
|
1048
|
-
finish(null, { at: Date.now(), meter: limits
|
|
1072
|
+
finish(null, { at: Date.now(), meter: limits ? normalizeMeter(limits) : null });
|
|
1049
1073
|
return;
|
|
1050
1074
|
}
|
|
1051
1075
|
}
|
|
@@ -1073,6 +1097,7 @@ module.exports = {
|
|
|
1073
1097
|
SLOTS,
|
|
1074
1098
|
WEIGHTS,
|
|
1075
1099
|
PLANS,
|
|
1100
|
+
normalizeMeter,
|
|
1076
1101
|
homeDir,
|
|
1077
1102
|
sessionsDir,
|
|
1078
1103
|
findExecutable,
|
|
@@ -112,7 +112,21 @@ function detect(argv, env) {
|
|
|
112
112
|
return CLAUDE;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Codex hook payloads always carry turn_id; Claude Code's never do. Codex also
|
|
116
|
+
// loads an installed plugin's Claude-style hooks/hooks.json, which runs these
|
|
117
|
+
// scripts with no --host and may set CLAUDE_PLUGIN_ROOT, so detect() alone
|
|
118
|
+
// called that Codex turn Claude Code and briefed Claude's budget into Codex.
|
|
119
|
+
// An explicit --host or USAGE_LIMITS_HOST still wins.
|
|
120
|
+
function detectFromHook(argv, env, input) {
|
|
121
|
+
const args = argv || [];
|
|
122
|
+
const environment = env || process.env;
|
|
123
|
+
if (args.includes('--host') || normalise(environment.USAGE_LIMITS_HOST)) return detect(args, environment);
|
|
124
|
+
if (input && typeof input === 'object' && input.turn_id !== undefined && input.turn_id !== null) return CODEX;
|
|
125
|
+
return detect(args, environment);
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
module.exports = {
|
|
129
|
+
detectFromHook,
|
|
116
130
|
CLAUDE,
|
|
117
131
|
CODEX,
|
|
118
132
|
GEMINI,
|
|
@@ -77,7 +77,9 @@ function hooks() {
|
|
|
77
77
|
'usage-limits-ceiling': {
|
|
78
78
|
PreToolUse: [
|
|
79
79
|
{
|
|
80
|
-
|
|
80
|
+
// Only the fan-out tools: a node start before every tool call cost
|
|
81
|
+
// 3-6 s each here, and ordinary tools have nothing to ask the ceiling.
|
|
82
|
+
matcher: 'invoke_subagent|browser_subagent',
|
|
81
83
|
hooks: [{ type: 'command', command: hookCommand('PreToolUse'), timeout: 10 }],
|
|
82
84
|
},
|
|
83
85
|
],
|
|
@@ -225,7 +225,7 @@ async function run(now, hookInput) {
|
|
|
225
225
|
const budget = mode.forSession({ sessionId });
|
|
226
226
|
if (budget.policy.refreshSeconds === 0) return '';
|
|
227
227
|
|
|
228
|
-
usage.setHost(host.
|
|
228
|
+
usage.setHost(host.detectFromHook(process.argv.slice(2), process.env, hookInput));
|
|
229
229
|
|
|
230
230
|
// A subagent finishing is the other reason to look, and on a busy afternoon
|
|
231
231
|
// it is the more important one.
|