claude-usage-limits 1.18.0 → 1.23.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +220 -2
- package/bin/cli.js +16 -0
- package/commands/usage-mode.md +64 -0
- package/hooks/hooks.json +1 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +166 -18
- package/skills/usage-limits/references/tactics.md +40 -9
- package/skills/usage-limits/scripts/agy-hook.js +175 -0
- package/skills/usage-limits/scripts/brief.js +406 -46
- package/skills/usage-limits/scripts/ceiling.js +191 -0
- package/skills/usage-limits/scripts/codex-lowpower.js +95 -4
- package/skills/usage-limits/scripts/codex.js +87 -6
- package/skills/usage-limits/scripts/drift.js +254 -0
- package/skills/usage-limits/scripts/feed.js +23 -1
- package/skills/usage-limits/scripts/host.js +23 -3
- package/skills/usage-limits/scripts/install-antigravity.js +215 -0
- package/skills/usage-limits/scripts/install-codex-hook.js +22 -2
- package/skills/usage-limits/scripts/lowpower.js +48 -0
- package/skills/usage-limits/scripts/mode.js +1637 -0
- package/skills/usage-limits/scripts/pulse.js +254 -17
- package/skills/usage-limits/scripts/reading.js +12 -3
- package/skills/usage-limits/scripts/sessionend.js +8 -0
- package/skills/usage-limits/scripts/stop.js +43 -0
- package/skills/usage-limits/scripts/usage.js +364 -17
- package/skills/usage-limits/scripts/view.js +4 -0
- package/skills/usage-limits/scripts/voice.js +10 -1
|
@@ -87,15 +87,46 @@ reason, if none of them are in use.
|
|
|
87
87
|
|
|
88
88
|
Cache reads are ten times cheaper than fresh input, and the cache matches on
|
|
89
89
|
an exact prefix. Any byte that changes early invalidates everything after it.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- switching models
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
the
|
|
90
|
+
What actually invalidates it mid-session:
|
|
91
|
+
|
|
92
|
+
- **switching models** - each model has its own cache, so the next request
|
|
93
|
+
re-reads the whole conversation even though the content is identical
|
|
94
|
+
- **changing the effort level** - on most models each effort level has its own
|
|
95
|
+
cache too. This one matters here because dropping effort is the saving this
|
|
96
|
+
plugin recommends most often. It is still worth doing, but it is not free
|
|
97
|
+
mid-session: on a large context the one-off rebuild can cost more than a few
|
|
98
|
+
cheaper turns save. Choose effort at the START of a session where you can
|
|
99
|
+
- **turning on fast mode** - it adds a request header that is part of the cache key
|
|
100
|
+
- **connecting or disconnecting an MCP server, but only when its tools sit in
|
|
101
|
+
the prefix.** With tool search - the default on supported models - a server
|
|
102
|
+
connecting, disconnecting or changing its tool list only appends, and the
|
|
103
|
+
cached prefix survives
|
|
104
|
+
- **enabling or disabling a plugin that provides MCP servers**, by the same
|
|
105
|
+
rule. A plugin ships skills, commands, agents and hooks by appending them,
|
|
106
|
+
and those never invalidate anything
|
|
107
|
+
- **adding or removing a bare tool-name deny rule** (`Bash`, `WebFetch`), which
|
|
108
|
+
takes the tool out of the system prompt. Scoped rules like `Bash(rm *)` do not
|
|
109
|
+
- **compacting**, by design, and **upgrading Claude Code**
|
|
110
|
+
|
|
111
|
+
What does NOT invalidate it, despite being widely believed to:
|
|
112
|
+
|
|
113
|
+
- **editing `CLAUDE.md` mid-session.** It is read once at session start and held
|
|
114
|
+
in memory. The edit does not invalidate the cache - and it also does not
|
|
115
|
+
apply, until `/clear`, `/compact` or a restart
|
|
116
|
+
- editing files in the repository, changing permission mode, changing output
|
|
117
|
+
style, invoking a skill or command, and `/recap`
|
|
118
|
+
|
|
119
|
+
None of the invalidating ones are forbidden. Just do them at a session boundary
|
|
120
|
+
instead of in the middle of a long run.
|
|
121
|
+
|
|
122
|
+
**`/rewind` rather than `/compact`** when abandoning a path: it truncates back to
|
|
123
|
+
a prefix that is already cached, where compaction builds a new one and pays a
|
|
124
|
+
summarisation call to do it.
|
|
125
|
+
|
|
126
|
+
**Cache scope is one machine and one directory.** Parallel sessions in the same
|
|
127
|
+
directory read each other cache; different directories do not - and that
|
|
128
|
+
includes two worktrees of the same repository, which is a real and unobvious
|
|
129
|
+
cost of isolating agents that way.
|
|
99
130
|
|
|
100
131
|
### 6. Batch tool calls
|
|
101
132
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The Antigravity entry point.
|
|
5
|
+
//
|
|
6
|
+
// Antigravity (and the `agy` CLI behind it) has a lifecycle-hook system that is
|
|
7
|
+
// close to Claude Code's in spirit and different from it in every detail that
|
|
8
|
+
// matters to a script:
|
|
9
|
+
//
|
|
10
|
+
// - Events are PreInvocation, PostInvocation, PreToolUse, PostToolUse and
|
|
11
|
+
// Stop. PreInvocation is the one that corresponds to UserPromptSubmit.
|
|
12
|
+
// - Every payload is protojson, so the keys are camelCase: conversationId,
|
|
13
|
+
// workspacePaths, transcriptPath, modelName, stepIdx, invocationNum.
|
|
14
|
+
// - Output is JSON on stdout, and each event has its own shape. Plain text
|
|
15
|
+
// on stdout is not context; it is a parse failure, and a parse failure is
|
|
16
|
+
// silent. That is the whole reason this file exists rather than pointing
|
|
17
|
+
// Antigravity at brief.js and hoping.
|
|
18
|
+
// - PreToolUse takes a decision of allow, deny, ask or force_ask.
|
|
19
|
+
//
|
|
20
|
+
// The contract is documented on the machine itself, in the built-in
|
|
21
|
+
// agy-customizations skill at
|
|
22
|
+
// ~/.gemini/antigravity-cli/builtin/skills/agy-customizations/docs/hooks.md,
|
|
23
|
+
// which is where these shapes were read from rather than guessed.
|
|
24
|
+
//
|
|
25
|
+
// node agy-hook.js --event PreInvocation
|
|
26
|
+
// node agy-hook.js --event PreToolUse
|
|
27
|
+
//
|
|
28
|
+
// Nothing here ever exits non-zero. Antigravity runs hooks synchronously and
|
|
29
|
+
// they block the agent loop, so a hook that fails is a hook that stops the
|
|
30
|
+
// user's work, and no budget figure is worth that.
|
|
31
|
+
|
|
32
|
+
const usage = require('./usage.js');
|
|
33
|
+
const host = require('./host.js');
|
|
34
|
+
const mode = require('./mode.js');
|
|
35
|
+
const ceiling = require('./ceiling.js');
|
|
36
|
+
const brief = require('./brief.js');
|
|
37
|
+
|
|
38
|
+
const EVENTS = ['PreInvocation', 'PostInvocation', 'PreToolUse', 'PostToolUse', 'Stop'];
|
|
39
|
+
|
|
40
|
+
function eventFrom(argv, input) {
|
|
41
|
+
const args = argv || [];
|
|
42
|
+
const at = args.indexOf('--event');
|
|
43
|
+
if (at !== -1 && EVENTS.includes(args[at + 1])) return args[at + 1];
|
|
44
|
+
// Antigravity does not name the event in the payload, so the shape is the
|
|
45
|
+
// only other evidence. toolCall is only ever present on the two tool events,
|
|
46
|
+
// and only PreToolUse can act on one.
|
|
47
|
+
if (input && input.toolCall) return 'PreToolUse';
|
|
48
|
+
if (input && input.terminationReason !== undefined) return 'Stop';
|
|
49
|
+
if (input && input.invocationNum !== undefined) return 'PreInvocation';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readInput() {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
if (process.stdin.isTTY) return resolve(null);
|
|
56
|
+
let raw = '';
|
|
57
|
+
let settled = false;
|
|
58
|
+
const done = () => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
try {
|
|
62
|
+
resolve(raw ? JSON.parse(raw) : null);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
resolve(null);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const timer = setTimeout(done, 500);
|
|
68
|
+
if (timer.unref) timer.unref();
|
|
69
|
+
process.stdin.setEncoding('utf8');
|
|
70
|
+
process.stdin.on('data', (chunk) => {
|
|
71
|
+
raw += chunk;
|
|
72
|
+
});
|
|
73
|
+
process.stdin.on('end', done);
|
|
74
|
+
process.stdin.on('error', done);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The tool name, as Antigravity spells it.
|
|
79
|
+
//
|
|
80
|
+
// Tool names there are the step type lowercased with the CORTEX_STEP_TYPE_
|
|
81
|
+
// prefix removed, so they are snake_case: run_command, view_file, browser_*.
|
|
82
|
+
// ceiling.js matches both spellings, so nothing has to be translated here.
|
|
83
|
+
function toolNameOf(input) {
|
|
84
|
+
if (!input || !input.toolCall) return '';
|
|
85
|
+
return String(input.toolCall.name || '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The cheapest percentage worth enforcing against.
|
|
89
|
+
//
|
|
90
|
+
// Antigravity does not publish remaining quota anywhere this can read - see
|
|
91
|
+
// collectGemini in usage.js, which says so rather than inventing a number - so
|
|
92
|
+
// on a Gemini host this is usually null and the ceiling never fires. It is
|
|
93
|
+
// still wired, because the same hook file runs when USAGE_LIMITS_HOST names a
|
|
94
|
+
// host that DOES have a meter, and because a ceiling that silently does
|
|
95
|
+
// nothing on the day Antigravity starts publishing one would be worse.
|
|
96
|
+
function percentNow(now) {
|
|
97
|
+
let worst = null;
|
|
98
|
+
try {
|
|
99
|
+
const snapshot = usage.collect(now);
|
|
100
|
+
const utilization = snapshot && snapshot.utilization;
|
|
101
|
+
if (utilization && typeof utilization === 'object') {
|
|
102
|
+
for (const key of Object.keys(utilization)) {
|
|
103
|
+
const window = utilization[key];
|
|
104
|
+
if (!window || typeof window !== 'object') continue;
|
|
105
|
+
const value = Number(window.utilization);
|
|
106
|
+
if (Number.isFinite(value) && (worst === null || value > worst)) worst = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// No reading is a reason not to enforce, never a reason to throw.
|
|
111
|
+
}
|
|
112
|
+
return worst;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function run(now, input, argv) {
|
|
116
|
+
const event = eventFrom(argv, input);
|
|
117
|
+
if (!event) return {};
|
|
118
|
+
|
|
119
|
+
usage.setHost(host.GEMINI);
|
|
120
|
+
const sessionId = input && input.conversationId ? String(input.conversationId) : null;
|
|
121
|
+
const budget = mode.forSession({ sessionId });
|
|
122
|
+
if (budget.policy.briefStyle === 'none') return {};
|
|
123
|
+
|
|
124
|
+
if (event === 'PreToolUse') {
|
|
125
|
+
const tool = toolNameOf(input);
|
|
126
|
+
if (!ceiling.isMultiplier(tool)) return {};
|
|
127
|
+
const at = ceiling.assess({ percent: percentNow(now), state: budget.state, env: process.env });
|
|
128
|
+
const call = ceiling.verdict(at, tool);
|
|
129
|
+
if (call.decision !== 'deny') return {};
|
|
130
|
+
return { decision: 'deny', reason: call.reason };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (event === 'PreInvocation') {
|
|
134
|
+
// The one place a budget line can reach the model. Antigravity takes it as
|
|
135
|
+
// an injected step rather than as stdout text; an ephemeralMessage is a
|
|
136
|
+
// transient system message, which is exactly what a per-turn figure is.
|
|
137
|
+
let text = '';
|
|
138
|
+
try {
|
|
139
|
+
text = await brief.run(now, input);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
text = '';
|
|
142
|
+
}
|
|
143
|
+
const warning = ceiling.warning(
|
|
144
|
+
ceiling.assess({ percent: percentNow(now), state: budget.state, env: process.env })
|
|
145
|
+
);
|
|
146
|
+
const message = [text, warning].filter(Boolean).join(' ');
|
|
147
|
+
return message ? { injectSteps: [{ ephemeralMessage: message }] } : { injectSteps: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// PostToolUse, PostInvocation and Stop all want an object and none of them
|
|
151
|
+
// wants anything from this plugin. Deliberately NOT returning
|
|
152
|
+
// terminationBehavior on PostInvocation, and NOT returning decision
|
|
153
|
+
// "continue" on Stop: both would keep the loop running, which is the
|
|
154
|
+
// opposite of what a budget plugin should ever do to a user's quota.
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (require.main === module) {
|
|
159
|
+
readInput()
|
|
160
|
+
.then((input) => run(Date.now(), input, process.argv.slice(2)))
|
|
161
|
+
.then(
|
|
162
|
+
(result) => {
|
|
163
|
+
process.stdout.write(JSON.stringify(result || {}) + '\n');
|
|
164
|
+
process.exit(0);
|
|
165
|
+
},
|
|
166
|
+
() => {
|
|
167
|
+
// Hooks block the agent loop here, so a failure has to be silent and
|
|
168
|
+
// well formed rather than loud.
|
|
169
|
+
process.stdout.write('{}\n');
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { EVENTS, eventFrom, toolNameOf, percentNow, run };
|