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.
- 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/defer.md +47 -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 +196 -19
- 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/defer.js +318 -0
- 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/net.js +179 -0
- package/skills/usage-limits/scripts/pulse.js +254 -17
- package/skills/usage-limits/scripts/reading.js +12 -3
- package/skills/usage-limits/scripts/relay.js +266 -2
- package/skills/usage-limits/scripts/sessionend.js +8 -0
- package/skills/usage-limits/scripts/stop.js +145 -1
- package/skills/usage-limits/scripts/usage.js +244 -17
- package/skills/usage-limits/scripts/view.js +4 -0
- package/skills/usage-limits/scripts/voice.js +10 -1
- package/skills/usage-limits/scripts/wake.js +210 -30
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Installs the plugin into Antigravity.
|
|
5
|
+
//
|
|
6
|
+
// Antigravity discovers customizations from ~/.gemini/config/ on a machine, and
|
|
7
|
+
// a plugin there is a directory holding plugin.json, optionally hooks.json,
|
|
8
|
+
// rules/ and skills/. That is documented on the machine itself, in the built-in
|
|
9
|
+
// agy-customizations skill, and this installer follows it rather than guessing:
|
|
10
|
+
//
|
|
11
|
+
// ~/.gemini/config/plugins/usage-limits/
|
|
12
|
+
// plugin.json the marker that makes the directory a plugin
|
|
13
|
+
// hooks.json PreInvocation for the budget line, PreToolUse for the ceiling
|
|
14
|
+
// rules/AGENTS.md the always-on rules
|
|
15
|
+
//
|
|
16
|
+
// The installed directory is deliberately small. Hooks run with their working
|
|
17
|
+
// directory set to the folder containing hooks.json, so a copied plugin would
|
|
18
|
+
// need the whole script tree copied with it and would then go stale the moment
|
|
19
|
+
// the real one was updated. Instead the commands carry an absolute path back to
|
|
20
|
+
// this checkout, so there is one copy of the code and updating it updates what
|
|
21
|
+
// Antigravity runs.
|
|
22
|
+
//
|
|
23
|
+
// node install-antigravity.js status
|
|
24
|
+
// node install-antigravity.js on
|
|
25
|
+
// node install-antigravity.js off
|
|
26
|
+
//
|
|
27
|
+
// Everything written is confined to that one directory, and `off` removes
|
|
28
|
+
// exactly what `on` created and nothing else.
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
const host = require('./host.js');
|
|
35
|
+
|
|
36
|
+
const PLUGIN = 'usage-limits';
|
|
37
|
+
|
|
38
|
+
function pluginsDir() {
|
|
39
|
+
return path.join(host.geminiConfigDir(), 'config', 'plugins');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pluginDir() {
|
|
43
|
+
return path.join(pluginsDir(), PLUGIN);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Forward slashes on every platform. They work in Windows paths and keep the
|
|
47
|
+
// command free of escapes in both JSON and the shell that runs it. Antigravity
|
|
48
|
+
// runs hook commands through `cmd /c` on Windows and `sh -c` elsewhere, so the
|
|
49
|
+
// quoting has to survive both.
|
|
50
|
+
function quote(file) {
|
|
51
|
+
return '"' + String(file).replace(/\\/g, '/') + '"';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scriptPath(name) {
|
|
55
|
+
return path.join(__dirname, name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hookCommand(event) {
|
|
59
|
+
return 'node ' + quote(scriptPath('agy-hook.js')) + ' --event ' + event;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function manifest() {
|
|
63
|
+
return {
|
|
64
|
+
name: PLUGIN,
|
|
65
|
+
description:
|
|
66
|
+
'Puts the remaining usage budget in front of the agent before each turn, and enforces a ' +
|
|
67
|
+
'ceiling past which fan-out calls are refused.',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hooks() {
|
|
72
|
+
return {
|
|
73
|
+
'usage-limits-brief': {
|
|
74
|
+
PreInvocation: [{ type: 'command', command: hookCommand('PreInvocation'), timeout: 10 }],
|
|
75
|
+
},
|
|
76
|
+
'usage-limits-ceiling': {
|
|
77
|
+
PreToolUse: [
|
|
78
|
+
{
|
|
79
|
+
matcher: '.*',
|
|
80
|
+
hooks: [{ type: 'command', command: hookCommand('PreToolUse'), timeout: 10 }],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rulesText() {
|
|
88
|
+
// Shipped from the repo so there is one copy of the wording, but never fails
|
|
89
|
+
// the install over a missing file: the hooks are the part that matters.
|
|
90
|
+
try {
|
|
91
|
+
return fs.readFileSync(path.join(__dirname, '..', '..', '..', 'rules', 'AGENTS.md'), 'utf8');
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeFile(file, text) {
|
|
98
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
99
|
+
const tmp = file + '.' + process.pid + '.tmp';
|
|
100
|
+
fs.writeFileSync(tmp, text, 'utf8');
|
|
101
|
+
fs.renameSync(tmp, file);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function installed() {
|
|
105
|
+
try {
|
|
106
|
+
fs.accessSync(path.join(pluginDir(), 'plugin.json'));
|
|
107
|
+
return true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Whether Antigravity is on this machine at all. Its config directory is the
|
|
114
|
+
// evidence; ~/.gemini alone is not, because the Gemini CLI uses that too.
|
|
115
|
+
function present() {
|
|
116
|
+
return host.exists(path.join(host.geminiConfigDir(), 'antigravity-cli')) ||
|
|
117
|
+
host.exists(path.join(host.geminiConfigDir(), 'antigravity'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function status() {
|
|
121
|
+
const lines = [];
|
|
122
|
+
lines.push('Antigravity ' + (present() ? 'found at ' + host.geminiConfigDir() : 'not found on this machine'));
|
|
123
|
+
lines.push('Plugin ' + (installed() ? 'installed at ' + pluginDir() : 'not installed'));
|
|
124
|
+
if (installed()) {
|
|
125
|
+
let current = null;
|
|
126
|
+
try {
|
|
127
|
+
current = JSON.parse(fs.readFileSync(path.join(pluginDir(), 'hooks.json'), 'utf8'));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// An unreadable hooks.json is worth saying rather than throwing.
|
|
130
|
+
}
|
|
131
|
+
const events = current ? Object.keys(current).map((name) => Object.keys(current[name]).filter((k) => k !== 'enabled').join(', ')) : [];
|
|
132
|
+
lines.push('Hooks ' + (events.length ? events.join(', ') : 'none readable'));
|
|
133
|
+
lines.push(
|
|
134
|
+
'Enabled recorded in ' +
|
|
135
|
+
path.join(host.geminiConfigDir(), 'config', 'config.json') +
|
|
136
|
+
' under plugins.' + PLUGIN + '; a plugin with no entry there is on by default.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (!present()) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push('Nothing to install into. Antigravity keeps its configuration in ~/.gemini/config.');
|
|
142
|
+
}
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function enable() {
|
|
147
|
+
if (!present()) {
|
|
148
|
+
return (
|
|
149
|
+
'Antigravity was not found on this machine, so there is nothing to install into.\n' +
|
|
150
|
+
'Expected its configuration at ' + host.geminiConfigDir() + '.'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const dir = pluginDir();
|
|
154
|
+
writeFile(path.join(dir, 'plugin.json'), JSON.stringify(manifest(), null, 2) + '\n');
|
|
155
|
+
writeFile(path.join(dir, 'hooks.json'), JSON.stringify(hooks(), null, 2) + '\n');
|
|
156
|
+
const rules = rulesText();
|
|
157
|
+
if (rules) writeFile(path.join(dir, 'rules', 'AGENTS.md'), rules);
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
'Installed into ' + dir + '.',
|
|
161
|
+
' PreInvocation the budget line, as an injected ephemeral message',
|
|
162
|
+
' PreToolUse the ceiling, which refuses fan-out calls past it',
|
|
163
|
+
rules ? ' rules/AGENTS.md the always-on rules' : ' (rules/AGENTS.md was not found in this checkout and was skipped)',
|
|
164
|
+
'',
|
|
165
|
+
'The hooks run this checkout directly, so updating it updates what Antigravity runs.',
|
|
166
|
+
'Antigravity picks the plugin up when it next starts. There is no quota figure for it: ' +
|
|
167
|
+
'Antigravity refreshes its own quota but writes it nowhere readable, so the budget line ' +
|
|
168
|
+
'reports what it can and says so where it cannot.',
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Removes only what enable() wrote, one named file at a time, and only takes
|
|
173
|
+
// the directory away when nothing else has been put in it.
|
|
174
|
+
function disable() {
|
|
175
|
+
const dir = pluginDir();
|
|
176
|
+
if (!installed()) return 'Not installed. Nothing to remove.';
|
|
177
|
+
const removed = [];
|
|
178
|
+
for (const relative of ['plugin.json', 'hooks.json', path.join('rules', 'AGENTS.md')]) {
|
|
179
|
+
const file = path.join(dir, relative);
|
|
180
|
+
try {
|
|
181
|
+
fs.unlinkSync(file);
|
|
182
|
+
removed.push(relative);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// Already gone is the outcome that was asked for.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const relative of ['rules', '']) {
|
|
188
|
+
try {
|
|
189
|
+
fs.rmdirSync(path.join(dir, relative));
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Not empty, or already gone. Either way it is not ours to force.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return 'Removed ' + (removed.length ? removed.join(', ') : 'nothing') + ' from ' + dir + '.';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function main(argv) {
|
|
198
|
+
const command = (argv || []).find((arg) => !arg.startsWith('-')) || 'status';
|
|
199
|
+
if (command === 'status') return status();
|
|
200
|
+
if (command === 'on') return enable();
|
|
201
|
+
if (command === 'off') return disable();
|
|
202
|
+
throw new Error('Expected status, on or off');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (require.main === module) {
|
|
206
|
+
try {
|
|
207
|
+
process.stdout.write(main(process.argv.slice(2)) + '\n');
|
|
208
|
+
process.exitCode = 0;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
process.stderr.write('install-antigravity: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { PLUGIN, pluginsDir, pluginDir, hookCommand, manifest, hooks, installed, present, status, enable, disable, main };
|
|
@@ -53,6 +53,22 @@ const EVENTS = [
|
|
|
53
53
|
// tool calls of its own for as long as they run. Same quiet refresh as on
|
|
54
54
|
// Claude Code; pulse.js sees the event name and says nothing.
|
|
55
55
|
{ event: 'SubagentStop', script: 'pulse.js', status: 'Checking usage limits' },
|
|
56
|
+
// The ceiling, and the only entry here that can refuse anything.
|
|
57
|
+
//
|
|
58
|
+
// Codex is the host where a reported figure demonstrably does not change
|
|
59
|
+
// behaviour, and the reason is not stubbornness: gpt-6-astra's own system
|
|
60
|
+
// prompt, shipped in models_cache.json, tells it "do not settle for a partial
|
|
61
|
+
// or helpful enough solution that does not fully satisfy the user's task to
|
|
62
|
+
// save time, effort or tokens", and ranks the live user instruction above
|
|
63
|
+
// anything an AGENTS.md or a skill says. A line asking it to economise is
|
|
64
|
+
// arguing with its own instructions and losing.
|
|
65
|
+
//
|
|
66
|
+
// So this one does not ask. PreToolUse takes a permissionDecision of "deny",
|
|
67
|
+
// and past the ceiling the fan-out calls get one. The matcher is broad
|
|
68
|
+
// because ceiling.js decides what is actually a multiplier; a hook that fires
|
|
69
|
+
// and returns nothing costs a few milliseconds, and a matcher that misses a
|
|
70
|
+
// renamed tool costs the window.
|
|
71
|
+
{ event: 'PreToolUse', script: 'pulse.js', status: 'Checking usage limits', matcher: '.*' },
|
|
56
72
|
];
|
|
57
73
|
const EVENT = EVENTS[0].event;
|
|
58
74
|
// Ten seconds is the same budget the Claude hook gets. The brief caches the
|
|
@@ -360,7 +376,7 @@ function enable() {
|
|
|
360
376
|
|
|
361
377
|
for (const one of EVENTS) {
|
|
362
378
|
const rest = withoutOurs(config.hooks[one.event]);
|
|
363
|
-
|
|
379
|
+
const group = {
|
|
364
380
|
hooks: [
|
|
365
381
|
{
|
|
366
382
|
type: 'command',
|
|
@@ -369,7 +385,11 @@ function enable() {
|
|
|
369
385
|
statusMessage: one.status,
|
|
370
386
|
},
|
|
371
387
|
],
|
|
372
|
-
}
|
|
388
|
+
};
|
|
389
|
+
// Tool-scoped events take a matcher; the others ignore one, and writing a
|
|
390
|
+
// matcher where none belongs is the kind of thing a strict parser rejects.
|
|
391
|
+
if (one.matcher) group.matcher = one.matcher;
|
|
392
|
+
rest.push(group);
|
|
373
393
|
config.hooks[one.event] = rest;
|
|
374
394
|
}
|
|
375
395
|
if (!config.description) {
|
|
@@ -61,6 +61,8 @@ function parseArgs(argv) {
|
|
|
61
61
|
for (let i = 0; i < argv.length; i += 1) {
|
|
62
62
|
const arg = argv[i];
|
|
63
63
|
if (arg === '--dry-run') args.dryRun = true;
|
|
64
|
+
// Codex only: leave the [agents] subagent clamp alone.
|
|
65
|
+
else if (arg === '--no-agents') args.agents = false;
|
|
64
66
|
else if (arg === '--host') args.host = argv[++i];
|
|
65
67
|
else if (arg.startsWith('--host=')) args.host = arg.slice(7);
|
|
66
68
|
else if (arg === '--effort') args.effort = argv[++i];
|
|
@@ -169,6 +171,50 @@ function describe(settings, state) {
|
|
|
169
171
|
return lines.join('\n');
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
// Write down what was changed here, so "change my effort back" has a referent.
|
|
175
|
+
//
|
|
176
|
+
// This script is the ONLY thing in the plugin that writes settings.json, and
|
|
177
|
+
// it was the only change the log could not name: mode.js recorded fourteen
|
|
178
|
+
// kinds of mode-plane change and nothing ever wrote a user-plane entry, so
|
|
179
|
+
// `mode --history` answered "nothing has been changed through this plugin yet"
|
|
180
|
+
// immediately after this had rewritten the user's baseline, and undo's whole
|
|
181
|
+
// user-plane branch was unreachable code.
|
|
182
|
+
//
|
|
183
|
+
// Required lazily and inside a try: a settings write that already succeeded
|
|
184
|
+
// must not be reported as a failure because a log line could not be added, and
|
|
185
|
+
// this file otherwise depends on nothing.
|
|
186
|
+
//
|
|
187
|
+
// `by` is a reading of the environment, not a claim about intent. Claude Code
|
|
188
|
+
// and Codex both put a session id into the environment of everything they
|
|
189
|
+
// launch, so a run from inside an agent session is recorded as the agent's and
|
|
190
|
+
// a run from the user's own shell as theirs. It is the honest half of "at
|
|
191
|
+
// whose instruction": who typed it, not who wanted it.
|
|
192
|
+
function logSettingsChange(changes, direction, env) {
|
|
193
|
+
if (!changes || !changes.length) return;
|
|
194
|
+
const e = env || process.env;
|
|
195
|
+
const by = e.CLAUDE_SESSION_ID || e.CODEX_SESSION_ID ? 'claude' : 'user';
|
|
196
|
+
try {
|
|
197
|
+
const mode = require('./mode.js');
|
|
198
|
+
for (const change of changes) {
|
|
199
|
+
// "effortLevel: xhigh -> low", as planApply and planRestore build them.
|
|
200
|
+
const at = change.indexOf(': ');
|
|
201
|
+
const key = at === -1 ? change : change.slice(0, at);
|
|
202
|
+
const rest = at === -1 ? '' : change.slice(at + 2);
|
|
203
|
+
const arrow = rest.indexOf(' -> ');
|
|
204
|
+
mode.logChange({
|
|
205
|
+
plane: 'user',
|
|
206
|
+
key,
|
|
207
|
+
from: arrow === -1 ? null : rest.slice(0, arrow),
|
|
208
|
+
to: arrow === -1 ? rest || null : rest.slice(arrow + 4),
|
|
209
|
+
by,
|
|
210
|
+
reason: direction === 'off' ? 'lowpower off' : 'lowpower on',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// A record of the change is worth less than the change itself.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
172
218
|
function main(argv) {
|
|
173
219
|
const args = parseArgs(argv);
|
|
174
220
|
const host = require('./host.js');
|
|
@@ -196,6 +242,7 @@ function main(argv) {
|
|
|
196
242
|
if (!state && fs.existsSync(file)) fs.copyFileSync(file, file + '.usage-limits-backup');
|
|
197
243
|
writeJson(file, plan.settings);
|
|
198
244
|
writeJson(stateFile(), plan.state);
|
|
245
|
+
logSettingsChange(plan.changes, 'on');
|
|
199
246
|
process.stdout.write(
|
|
200
247
|
'Low power on.\n ' + (plan.changes.join('\n ') || '(nothing to change)') + '\n' +
|
|
201
248
|
'Applies to new sessions. For the session you are in, run /effort ' +
|
|
@@ -216,6 +263,7 @@ function main(argv) {
|
|
|
216
263
|
}
|
|
217
264
|
writeJson(file, plan.settings);
|
|
218
265
|
fs.unlinkSync(stateFile());
|
|
266
|
+
logSettingsChange(plan.changes, 'off');
|
|
219
267
|
process.stdout.write(
|
|
220
268
|
'Low power off.\n ' + (plan.changes.join('\n ') || '(nothing to change)') + '\n'
|
|
221
269
|
);
|