claude-usage-limits 1.19.0 → 1.24.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.
Files changed (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +220 -2
  4. package/bin/cli.js +16 -0
  5. package/commands/defer.md +47 -0
  6. package/commands/usage-mode.md +64 -0
  7. package/hooks/hooks.json +1 -1
  8. package/package.json +1 -1
  9. package/skills/usage-limits/SKILL.md +196 -19
  10. package/skills/usage-limits/references/tactics.md +40 -9
  11. package/skills/usage-limits/scripts/agy-hook.js +175 -0
  12. package/skills/usage-limits/scripts/brief.js +406 -46
  13. package/skills/usage-limits/scripts/ceiling.js +191 -0
  14. package/skills/usage-limits/scripts/codex-lowpower.js +95 -4
  15. package/skills/usage-limits/scripts/codex.js +87 -6
  16. package/skills/usage-limits/scripts/defer.js +318 -0
  17. package/skills/usage-limits/scripts/drift.js +254 -0
  18. package/skills/usage-limits/scripts/feed.js +23 -1
  19. package/skills/usage-limits/scripts/host.js +23 -3
  20. package/skills/usage-limits/scripts/install-antigravity.js +215 -0
  21. package/skills/usage-limits/scripts/install-codex-hook.js +22 -2
  22. package/skills/usage-limits/scripts/lowpower.js +48 -0
  23. package/skills/usage-limits/scripts/mode.js +1637 -0
  24. package/skills/usage-limits/scripts/net.js +179 -0
  25. package/skills/usage-limits/scripts/pulse.js +254 -17
  26. package/skills/usage-limits/scripts/reading.js +12 -3
  27. package/skills/usage-limits/scripts/relay.js +266 -2
  28. package/skills/usage-limits/scripts/sessionend.js +8 -0
  29. package/skills/usage-limits/scripts/stop.js +145 -1
  30. package/skills/usage-limits/scripts/usage.js +244 -17
  31. package/skills/usage-limits/scripts/view.js +4 -0
  32. package/skills/usage-limits/scripts/voice.js +10 -1
  33. package/skills/usage-limits/scripts/wake.js +210 -30
@@ -275,6 +275,137 @@ The user can also ask any time with `/usage-limits:session`, or run
275
275
  `--sessions` for the history of recent sessions on this machine.
276
276
 
277
277
 
278
+ ## Budget modes
279
+
280
+ How hard this plugin leans, and what it costs to say it. Four modes, set by the
281
+ user and read by every hook:
282
+
283
+ ```
284
+ node scripts/mode.js # which one, where it came from, what it changes
285
+ node scripts/mode.js max # set it
286
+ node scripts/mode.js --list # all four, and the aliases
287
+ node scripts/mode.js --explain high # one mode's full record
288
+ ```
289
+
290
+ | mode | one line |
291
+ | --- | --- |
292
+ | `max` | fewest tokens that can still finish the job: one terse line, readings every ten minutes, silent when nothing a decision depends on has moved |
293
+ | `high` | full capability, re-measured every two minutes mid-turn, and said only when the answer changes |
294
+ | `standard` | what the plugin does today, unchanged. The default |
295
+ | `off` | nothing injected at all; every hook returns before reading anything, including the end-of-reply tally |
296
+
297
+ Aliases: `ultra`, `ultra-efficient`, `maxtoken`, `maxefficient` map to `max`;
298
+ `smart`, `high-efficient` to `high`; `efficient`, `token-efficient`, `default`,
299
+ `on` to `standard`; `none`, `quiet`, `silent`, `ignore` to `off`.
300
+
301
+ **`normal` is not an alias for either.** To some people it means "the plugin
302
+ working as usual" (`standard`) and to others "the plugin stays out of the way"
303
+ (`off`) - opposite instructions, so a silent guess is wrong half the time. If
304
+ the user says "normal", ask which they meant and say what each one does. The
305
+ script answers the same way.
306
+
307
+ `auto` picks from how full the binding window is - standard under 50 per cent
308
+ used, high from 50 to 79, max at 80 or above - and always reports itself as
309
+ what it resolved to (`auto -> max`). It reads the snapshot already on disk
310
+ rather than scanning, because the mode has to be settled before anything
311
+ expensive runs, and with no readable window it stays on `standard` rather than
312
+ guessing. It never picks `off`: turning the plugin off is a decision a person
313
+ makes.
314
+
315
+ `off` means off, including at 100 per cent. That is deliberate. If the user
316
+ wants one line at the wall and nothing else, `node scripts/mode.js off --guard 95`
317
+ is it.
318
+
319
+ Two rules hold in every mode, and they are not negotiable:
320
+
321
+ - **A mode never lowers the quality of the work.** When things are tight you
322
+ change the ORDER of the work, never the amount or the quality. The savings
323
+ come from ceremony - speculative reads, re-reads, preamble, subagents nobody
324
+ needed, workflows that cost more context than they save.
325
+ - **The modes govern the agent plane only**, and never write `settings.json`.
326
+
327
+ ### Two planes, and the conversation between them
328
+
329
+ The **user plane** is `settings.json` (`model`, `effortLevel`), the `/effort`
330
+ and `/model` pickers, and `lowpower.js`. It is the user saying what they want
331
+ for themselves. It is theirs.
332
+
333
+ The **agent plane** is the tier actually running this turn, and the tier of
334
+ everything this turn spawns. That is what costs money.
335
+
336
+ What is actually yours to move, stated exactly, because a claim beyond this
337
+ would be a lie the user cannot check:
338
+
339
+ - **The model on an `Agent` call is yours.** `model: "sonnet" | "opus" |
340
+ "haiku" | "fable"` on any Agent call, and it overrides both the agent
341
+ definition and the configured default. It is ignored for `subagent_type:
342
+ "fork"`, which always inherits the parent model.
343
+ - **Model and effort inside a `Workflow` script are yours**, per `agent()`
344
+ call: `opts.model` and `opts.effort` (`low` | `medium` | `high` | `xhigh` |
345
+ `max`). The host's own advice is to omit `opts.model` unless you are
346
+ confident, and to use `opts.effort: 'low'` for mechanical stages - so lean on
347
+ effort rather than model when sizing what you spawn.
348
+ - **Your own model and effort are NOT yours to change mid-session.** Nothing a
349
+ hook can emit changes them: there is no such field anywhere in the hook
350
+ output contract, and `PreModelSwitch` can only veto a switch someone else
351
+ started. `/model` and `/effort` are typed by a person and
352
+ `CLAUDE_CODE_EFFORT_LEVEL` is read at launch. So say the exact command in one
353
+ line and let the user run it. Never imply you changed it yourself.
354
+ - `xhigh` and `max` are refused outright when thinking is disabled, so never
355
+ point at either without that being true.
356
+
357
+ The two planes talk to each other, in both directions, through the
358
+ conversation. The whole rule in one line:
359
+
360
+ **You may RECOMMEND a user-plane change. You may MAKE one when asked. You may
361
+ never make one unasked.**
362
+
363
+ Recommending, when you do it:
364
+
365
+ - **Evidence or silence.** Cite a measurement - what this account has actually
366
+ measured, from `usage.js --recommend`, the fit line in the budget briefing,
367
+ or `scripts/drift.js`. "Recommended" with no number is nagging.
368
+ - **At most one per session**, and none at all in `off`. The plugin enforces
369
+ this; do not route around it.
370
+ - **A declined recommendation is not raised again.** When the user says no,
371
+ record it - `node scripts/mode.js --decline` - and it is never volunteered
372
+ again, in this session or any later one. `--advice` shows what is pending.
373
+ - **Name the exact command, the plane it changes, and when it takes effect.**
374
+
375
+ When the user asks you to change something, or says **"change it back"**: read
376
+ `node scripts/mode.js --history` first, say out loud what you are reverting and
377
+ to what, then do it. Never infer a target that is not in the log. `node
378
+ scripts/mode.js undo` reverses the last change the plugin owns and names it
379
+ first; for a change to the user's own settings it names the command and leaves
380
+ the file alone, because that plane is not the plugin's to write.
381
+
382
+ The surprise to head off, every time: **a user-plane change does not move the
383
+ session already running.** If the user says "turn low power on" and you do it,
384
+ nothing about this session changes - `lowpower.js` says so itself ("Applies to
385
+ new sessions. For the session you are in, run /effort X"). Relay that in your
386
+ own words whenever you make one, and the same in reverse: putting the baseline
387
+ back does not restore this session's tier either.
388
+
389
+ ### Bounds the user can set
390
+
391
+ ```
392
+ node scripts/mode.js --floor sonnet/medium # never point below this, even in max
393
+ node scripts/mode.js --ceiling opus/xhigh # nor above it, even on the hard part
394
+ node scripts/mode.js --pin # do not self-switch at all: report only
395
+ ```
396
+
397
+ Under `--pin` the plugin reports the gap between the baseline and what is
398
+ running and suggests nothing. Respect it in your own words too: report, do not
399
+ switch.
400
+
401
+ ### Stepping back up
402
+
403
+ The efficient modes are two-directional, and that matters more than it sounds.
404
+ Having dropped to a cheap tier for a mechanical stretch, say so when the work
405
+ turns hard again and point back at the baseline. A one-way ratchet down is how
406
+ a long session quietly degrades, which is the thing the quality rule forbids.
407
+
408
+
278
409
  ## 4. Low power
279
410
 
280
411
  Two halves, and the second one is the half that actually binds.
@@ -404,41 +535,48 @@ waste at 10 per cent used exactly as much as at 80; the only difference is that
404
535
  at 80 somebody notices.
405
536
 
406
537
  It cannot judge how hard your work is. You can. So when you see it, look at what
407
- is actually in front of you and take one of three levers yourself, saying which
408
- in one line:
538
+ is actually in front of you and reach for one of three levers, saying which in
539
+ one line. Two of them are yours to pull; the first is not.
409
540
 
410
541
  1. **Drop the effort** for that stretch - a rename, a docs pass, running tests,
411
- applying a fix you have already worked out.
412
- 2. **Hand the stretch to a cheaper model**, and keep the dear one for the
413
- decisions that need it.
542
+ applying a fix you have already worked out. `/effort` and `settings.json` are
543
+ the user's own baseline, so this one you **offer**: name the command, say why
544
+ the stretch does not need the tier, and leave the pulling of it to them.
545
+ 2. **Hand the stretch to a cheaper model.** Your own model is the user's setting
546
+ too, so say that in one line rather than switching - but the model on
547
+ anything you SPAWN is entirely yours: an Agent call takes a model, and a
548
+ Workflow's `agent()` takes a model and an effort. Size those to the stage.
414
549
  3. **Do less of it at this setting** - a fan-out multiplies the setting across
415
- every agent, so six agents at ultra is six ultra turns, not one.
550
+ every agent, so six agents at ultra is six ultra turns, not one. Yours.
416
551
 
417
552
  Put it back when the work gets hard again. The question is asked once per
418
553
  setting per session, not every prompt, and never at all unless there is a real
419
554
  measured comparison behind it - a ratio from a price list would have you drop
420
555
  effort on a hunch and call it evidence.
421
556
 
422
- **This is a lever you may pull yourself, not only an emergency exit.** Use it
423
- whenever the current setting is dearer than the work in front of you needs,
424
- without being asked and long before any window is tight: a mechanical rename,
425
- a docs pass or a mass find-and-replace does not need the model and effort a
426
- hard design decision does. Drop it for that stretch, say in one line that you
427
- did and why, and put it back when the work gets hard again. An agent that only
428
- ever reads this as a wall notice runs every trivial turn at the top setting and
429
- then wonders where the window went.
557
+ **This is worth raising early, not only at the wall.** Say it whenever the
558
+ current setting is dearer than the work in front of you needs, long before any
559
+ window is tight: a mechanical rename, a docs pass or a mass find-and-replace
560
+ does not need the model and effort a hard design decision does. An agent that
561
+ only ever reads this as a wall notice runs every trivial turn at the top setting
562
+ and then wonders where the window went.
563
+
564
+ **And "offer it" is one line, not a stop.** Naming the lever and carrying on is
565
+ the whole action. Waiting for an answer before doing the work would cost more
566
+ than the tier ever did.
430
567
 
431
568
  The budget line does this arithmetic for you. Once the binding window is half
432
569
  gone it says which lever applies, and at the wall it says outright that you are
433
570
  not out of budget and must not stop as though you were. When it does, the
434
- sequence is: switch, say in one line that you switched and why, carry on with
435
- the whole request at full quality.
571
+ sequence is: say in one line which command frees it and why, and carry on with
572
+ the whole request at full quality. A per-model weekly is not the account's
573
+ budget, and treating it as one is the error.
436
574
 
437
575
  This is written down because it was got wrong. A session ended with the Fable
438
576
  weekly at 89 per cent and the line saying the budget was nearly gone - while the
439
577
  5-hour sat at 46 and every other model on the account was untouched. One command
440
- would have carried it on. Stopping there was not caution; it was quitting with a
441
- reason that sounded like one.
578
+ would have carried it on, and naming that command costs a line. Stopping there
579
+ was not caution; it was quitting with a reason that sounded like one.
442
580
 
443
581
  Only when the switch is genuinely unavailable - no other window has room, or the
444
582
  user has ruled it out - does the checkpoint below apply.
@@ -482,6 +620,32 @@ which files are mid-change, what must be verified before anything is built on
482
620
  it. It is delivered as a prompt, so an instruction beats a summary. If nothing
483
621
  is written, the relay falls back to the outstanding todo list, which is worse.
484
622
 
623
+ **It arms at the end of a reply, not in the middle of one.** Crossing the
624
+ threshold no longer schedules anything by itself: the relay waits for the reply
625
+ to finish, so what it carries is work that reached a boundary rather than a
626
+ snapshot of an interruption. Past the backstop (95 per cent by default) it stops
627
+ waiting and arms anyway, because a completion that never comes - the limit cut
628
+ the reply off mid-sentence - is a relay that was never armed at all. That is
629
+ what "waiting for this reply to finish before arming" means when the budget line
630
+ says it, and it is not an error. `relay armon threshold` restores the old
631
+ behaviour.
632
+
633
+ **Being offline is not failing.** Before it launches anything the wake checks it
634
+ can actually reach the API, and a machine that cannot is held, not spent: it
635
+ retries on a backoff for hours rather than burning the relay on a launch that
636
+ could not work. This exists because it happened - a relay woke on time, said
637
+ `SSL certificate hostname mismatch` 1.5 seconds later, recorded a failure and
638
+ deleted itself, and a night's work was lost to a router being off. A failed
639
+ launch is also classified now: the network and a busy service buy another
640
+ window, an invalid key or a missing CLI does not, because retrying those would
641
+ waste the window and saying "retrying" would be a lie.
642
+
643
+ Run `relay doctor` before trusting it with anything. It checks the things that
644
+ fail silently hours later - a CLI that moved, a permission mode nobody set, a
645
+ machine whose power plan forbids wake timers, a scheduled task this account
646
+ cannot register, no continuation written, no network - and says which would
647
+ bite. Every resumed run's full output is kept: `relay log --run`.
648
+
485
649
  The relay is off unless the user turned it on, and it only arms while there is
486
650
  an unfinished todo list or an approved plan to carry. Do not turn it on for
487
651
  them, and do not promise behaviour it does not have:
@@ -490,6 +654,8 @@ them, and do not promise behaviour it does not have:
490
654
  input to a shell on purpose; the relay uses it only to tell whether somebody
491
655
  is at the keyboard, and to show a banner.
492
656
  - In `notify` mode — the default — it raises a notification and starts nothing.
657
+ - The resumed run opens in a window you can see. `relay show off` hides it; the
658
+ output is kept either way.
493
659
  - In `resume` mode it runs the CLI itself. A headless resume does **not**
494
660
  inherit the session's permission mode, so unless one was set the resumed run
495
661
  will sit waiting for an approval nobody is there to give.
@@ -540,6 +706,14 @@ of it.
540
706
 
541
707
  ## Running under Codex
542
708
 
709
+ One difference that matters for the budget modes. Every entry in Codex's
710
+ `hooks.json` carries an `additionalContextLimit` - a per-hook cap on how much
711
+ context a hook may inject, which Claude Code has no equivalent of. A long
712
+ briefing can therefore be truncated there without a word. If a line looks cut
713
+ off under Codex, that is why: `node scripts/mode.js max` keeps it to one line,
714
+ and `high` is the verbose one.
715
+
716
+
543
717
  Everything above works the same. The numbers come from a different place and
544
718
  one thing about how they arrive is different, and both are worth knowing.
545
719
 
@@ -654,6 +828,7 @@ stop.
654
828
  | `scripts/host.js` | Works out which agent this is running inside, so one host's percentages are never reported against the other's turns. |
655
829
  | `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
656
830
  | `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
831
+ | `scripts/mode.js` | The budget mode: no arguments to report it, `max`/`high`/`standard`/`off` to set it, `auto`, `off --guard 95`, `--list`, `--explain <name>`, `--floor`/`--ceiling`/`--pin`, `--baseline`, `--advice`/`--no-advice`, `--history`, `undo`, `--ledger`. Reads settings.json and never writes it. |
657
832
  | `scripts/lowpower.js` | `status`, `on`, `off`. Restores what it replaced. Claude Code only. |
658
833
  | `scripts/recommend.js` | The chooser behind `usage.js --recommend`: posture, then the effort and model commands for each lever. Not meant to be called by hand. |
659
834
  | `references/tactics.md` | Every lever that lowers cost, and why it works. |
@@ -661,8 +836,10 @@ stop.
661
836
  | `scripts/statusline.js` | `status`, `on`, `off`. Puts the bars under the prompt and restores what was there. |
662
837
  | `scripts/feed.js` | The status line command Claude Code runs. Not meant to be called by hand. |
663
838
  | `scripts/live.js` | The usage reading itself, taken the way Claude Code takes it for `/usage`, kept in `usage-limits-live.json` where `collect()` prefers it when newer than the cache. |
839
+ | `scripts/drift.js` | How wrong the reading was: each correction written down against the one it replaced. Run it with no arguments for the median and worst gap measured so far, `--json` for the fields. Answer "how far behind does this plugin actually run" from here rather than from memory. |
664
840
  | `scripts/view.js`, `scripts/bars.js`, `scripts/activity.js` | The display model, the drawing in Claude's colours, and the working/idle marks the hooks leave for the panel. Not meant to be called by hand. |
665
- | `scripts/relay.js` | The relay: `status`, `on`/`off`, `at N`, `grace N`, `mode notify\|resume`, `permission MODE`, `thinking off\|resume\|always`, `note "<text>"`, `cancel`, `log`. |
841
+ | `scripts/relay.js` | The relay: `status`, `on`/`off`, `at N`, `grace N`, `mode notify\|resume`, `permission MODE`, `thinking off\|resume\|always`, `armon threshold\|completion`, `backstop N`, `show on\|off`, `onfailure rearm\|stop`, `rearms N`, `offline N`, `doctor`, `note "<text>"`, `cancel`, `log [--run]`. |
842
+ | `scripts/net.js` | Can this machine reach the API, and was a failed run the network's fault. Three probes, TLS-interception detection, and the backoff the offline retries use. |
666
843
  | `scripts/wake.js` | What the scheduler runs after the reset: re-checks the meter, then notifies or resumes. Never called by hand. |
667
844
  | `scripts/voice.js` | The local writing profile: `show`, `card`, `set "<instruction>"`, `clear`, `off`/`on`, `forget`. |
668
845
  | `references/how-it-works.md` | Where the numbers come from and where they are soft. |
@@ -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 };