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.
@@ -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
- Things that invalidate it mid-session:
91
-
92
- - switching models
93
- - editing `CLAUDE.md` or project settings
94
- - connecting or disconnecting an MCP server
95
- - changing the tool set
96
-
97
- None of these are forbidden. Just do them at a session boundary instead of in
98
- the middle of a long run.
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 };