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
|
@@ -45,6 +45,10 @@ function isCodex() {
|
|
|
45
45
|
return currentHost() === host.CODEX;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function isGemini() {
|
|
49
|
+
return currentHost() === host.GEMINI;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
const MINUTE = 60 * 1000;
|
|
49
53
|
const HOUR = 60 * MINUTE;
|
|
50
54
|
const DAY = 24 * HOUR;
|
|
@@ -2160,6 +2164,85 @@ function criticalOthers(windows, bindingKey, threshold) {
|
|
|
2160
2164
|
);
|
|
2161
2165
|
}
|
|
2162
2166
|
|
|
2167
|
+
// What effort is ACTUALLY running, and how we know.
|
|
2168
|
+
//
|
|
2169
|
+
// This looked like a plugin bug and is not one. Ultracode is not a separate
|
|
2170
|
+
// effort level: Claude Code's own words are "ultracode: xhigh + dynamic
|
|
2171
|
+
// workflow orchestration (this session only)". So a session in ultracode has
|
|
2172
|
+
// effortLevel xhigh in settings.json and writes effort "xhigh" into every
|
|
2173
|
+
// transcript line - both correct, and both unable to say which MODE is on.
|
|
2174
|
+
// "This session only" means it is never written to disk at all.
|
|
2175
|
+
//
|
|
2176
|
+
// So the honest answer is a chain, and every reading says where it came from,
|
|
2177
|
+
// because "xhigh" from a stale settings file and "xhigh" from a live override
|
|
2178
|
+
// deserve different confidence:
|
|
2179
|
+
//
|
|
2180
|
+
// 1. CLAUDE_CODE_EFFORT_LEVEL - Claude Code's own session override. Hooks
|
|
2181
|
+
// inherit the environment, so this is live and authoritative.
|
|
2182
|
+
// 2. An override recorded here by hand, for the one case nothing can see:
|
|
2183
|
+
// /effort ultracode typed in the UI, which touches neither disk nor env.
|
|
2184
|
+
// 3. settings.effortLevel - the persisted default.
|
|
2185
|
+
// 4. The transcript - stamped at session start and never re-stamped.
|
|
2186
|
+
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'ultracode'];
|
|
2187
|
+
|
|
2188
|
+
function effortOverrideFile() {
|
|
2189
|
+
return path.join(configDir(), 'usage-limits-effort.json');
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function readEffortOverride() {
|
|
2193
|
+
try {
|
|
2194
|
+
const parsed = JSON.parse(fs.readFileSync(effortOverrideFile(), 'utf8'));
|
|
2195
|
+
if (!parsed || !EFFORT_LEVELS.includes(parsed.effort)) return null;
|
|
2196
|
+
// A hand-set override describes one session. Left lying around it would
|
|
2197
|
+
// outlive the session it was true for, so it expires.
|
|
2198
|
+
if (!Number.isFinite(parsed.at) || Date.now() - parsed.at > 12 * 60 * 60 * 1000) return null;
|
|
2199
|
+
return parsed;
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function writeEffortOverride(level, now) {
|
|
2206
|
+
if (level === null) {
|
|
2207
|
+
try {
|
|
2208
|
+
fs.unlinkSync(effortOverrideFile());
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
// Already gone is the outcome asked for.
|
|
2211
|
+
}
|
|
2212
|
+
return { ok: true, cleared: true };
|
|
2213
|
+
}
|
|
2214
|
+
if (!EFFORT_LEVELS.includes(level)) return { ok: false, error: 'effort must be one of: ' + EFFORT_LEVELS.join(', ') };
|
|
2215
|
+
try {
|
|
2216
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
2217
|
+
writeJsonAtomic(effortOverrideFile(), { effort: level, at: Number.isFinite(now) ? now : Date.now() });
|
|
2218
|
+
return { ok: true, effort: level };
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
return { ok: false, error: err.message };
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function effortNow(sessionId, env) {
|
|
2225
|
+
const environment = env || process.env;
|
|
2226
|
+
const fromEnv = String(environment.CLAUDE_CODE_EFFORT_LEVEL || '').trim().toLowerCase();
|
|
2227
|
+
if (EFFORT_LEVELS.includes(fromEnv)) return { effort: fromEnv, source: 'environment', live: true };
|
|
2228
|
+
|
|
2229
|
+
const override = readEffortOverride();
|
|
2230
|
+
if (override) return { effort: override.effort, source: 'set by hand', live: true };
|
|
2231
|
+
|
|
2232
|
+
let configured = null;
|
|
2233
|
+
try {
|
|
2234
|
+
configured = collect(Date.now()).settings;
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
configured = null;
|
|
2237
|
+
}
|
|
2238
|
+
const level = configured && configured.effortLevel;
|
|
2239
|
+
if (EFFORT_LEVELS.includes(level)) return { effort: level, source: 'settings', live: false };
|
|
2240
|
+
|
|
2241
|
+
const seen = liveEffort(sessionId);
|
|
2242
|
+
if (seen && EFFORT_LEVELS.includes(seen.effort)) return { effort: seen.effort, source: 'transcript', live: false };
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2163
2246
|
// Is the setting bigger than the work needs?
|
|
2164
2247
|
//
|
|
2165
2248
|
// Everything else here is budget-triggered: it speaks when a window is filling.
|
|
@@ -2180,16 +2263,30 @@ function settingFit(events, effortNow, which) {
|
|
|
2180
2263
|
// Guarded here rather than trusted from the caller: a cache hit has no event
|
|
2181
2264
|
// list to hand over, and dominantEffort iterates without checking.
|
|
2182
2265
|
const list = Array.isArray(events) ? events : [];
|
|
2183
|
-
|
|
2266
|
+
return fitFromRates(effortRates(list), effortNow || dominantEffort(list), which);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// The same judgement, from the per-effort table rather than from the events.
|
|
2270
|
+
//
|
|
2271
|
+
// This is the form the callers actually have. report() builds the event list,
|
|
2272
|
+
// derives effortRates from it and returns the TABLE - the events themselves
|
|
2273
|
+
// never leave the function - so every production caller of settingFit was
|
|
2274
|
+
// handing it `data.events`, which is undefined, and getting null back on every
|
|
2275
|
+
// call. That silently emptied the whole recommendation channel: no fit
|
|
2276
|
+
// sentence in the brief, no advice to offer or decline, and `high`'s mid-turn
|
|
2277
|
+
// re-cost reduced to its escape clause. Taking the table means the caller
|
|
2278
|
+
// passes the thing it has.
|
|
2279
|
+
function fitFromRates(rates, effortNow, which) {
|
|
2280
|
+
const current = effortNow || null;
|
|
2184
2281
|
if (!current) return null;
|
|
2185
|
-
const
|
|
2186
|
-
const here =
|
|
2282
|
+
const table = Array.isArray(rates) ? rates : [];
|
|
2283
|
+
const here = table.find((row) => row.effort === current);
|
|
2187
2284
|
if (!here || here.turns < MIN_EFFORT_SAMPLE) return null;
|
|
2188
2285
|
const measure = (row) => (Number.isFinite(row.outputPerTurn) && row.outputPerTurn > 0 ? row.outputPerTurn : row.perTurn);
|
|
2189
2286
|
const mine = measure(here);
|
|
2190
2287
|
if (!Number.isFinite(mine) || mine <= 0) return null;
|
|
2191
2288
|
let best = null;
|
|
2192
|
-
for (const row of
|
|
2289
|
+
for (const row of table) {
|
|
2193
2290
|
if (row.effort === current || row.turns < MIN_EFFORT_SAMPLE) continue;
|
|
2194
2291
|
const theirs = measure(row);
|
|
2195
2292
|
if (!Number.isFinite(theirs) || theirs <= 0) continue;
|
|
@@ -2230,23 +2327,46 @@ function levers(which) {
|
|
|
2230
2327
|
// `host` is the required module here, not a parameter - shadowing it was a
|
|
2231
2328
|
// ReferenceError on the constant it owns.
|
|
2232
2329
|
const codex = which === host.CODEX;
|
|
2330
|
+
const gemini = which === host.GEMINI;
|
|
2233
2331
|
return {
|
|
2234
2332
|
model: (family) =>
|
|
2235
|
-
|
|
2236
|
-
?
|
|
2237
|
-
|
|
2238
|
-
|
|
2333
|
+
gemini
|
|
2334
|
+
? 'Gemini CLI model flag (agy --model ' + (family || '<model>') + ')'
|
|
2335
|
+
: codex
|
|
2336
|
+
? "Codex's own model control (the script only saves defaults for new sessions: " +
|
|
2337
|
+
'node scripts/lowpower.js on --host codex --model <id>)'
|
|
2338
|
+
: '/model ' + (family || '<another model>') + ' (or node scripts/lowpower.js on --model <id>)',
|
|
2239
2339
|
effort: (level) =>
|
|
2240
|
-
|
|
2241
|
-
?
|
|
2242
|
-
|
|
2243
|
-
|
|
2340
|
+
gemini
|
|
2341
|
+
? 'Gemini CLI effort flag (agy --effort ' + (level || 'low|medium|high') + ')'
|
|
2342
|
+
: codex
|
|
2343
|
+
? "Codex's own effort control for this task (node scripts/lowpower.js on --host codex --effort " +
|
|
2344
|
+
(level || '<level>') + ' saves it for new sessions and cannot change one already running)'
|
|
2345
|
+
: '/effort ' + (level || '<level>'),
|
|
2244
2346
|
};
|
|
2245
2347
|
}
|
|
2246
2348
|
|
|
2247
|
-
|
|
2349
|
+
// `policy` is the budget mode's own record, when there is one, and `bounds` is
|
|
2350
|
+
// the user's own limits. One field of each reaches here, and they come from
|
|
2351
|
+
// different objects on purpose because that is where they live.
|
|
2352
|
+
//
|
|
2353
|
+
// `switchGapPoints` is the mode's: how much emptier another window has to be
|
|
2354
|
+
// before a switch is worth naming - the efficient modes act on a smaller
|
|
2355
|
+
// improvement because taking it is the whole point of being in them, while the
|
|
2356
|
+
// standard mode keeps the wider margin that stops a lateral move being sold as
|
|
2357
|
+
// a way out.
|
|
2358
|
+
//
|
|
2359
|
+
// `pin` is the user saying "report it, do not do it", and it is a BOUND, not a
|
|
2360
|
+
// mode field: mode.resolve() surfaces it as `bounds.pin` and no MODES record
|
|
2361
|
+
// has ever had a `pin` key. Reading it off the policy made `route.report`
|
|
2362
|
+
// unconditionally false, so the flag documented as carrying the pin carried
|
|
2363
|
+
// nothing, and the only reason no user ever saw a pinned suggestion phrased as
|
|
2364
|
+
// an instruction is that both renderers re-derived the pin for themselves.
|
|
2365
|
+
function escapeRoute(windows, binding, effortNote, host, policy, bounds) {
|
|
2248
2366
|
if (!binding || binding.percentUsed === null || binding.percentUsed === undefined) return null;
|
|
2249
2367
|
const lever = levers(host || currentHost());
|
|
2368
|
+
const gapPoints = policy && Number.isFinite(policy.switchGapPoints) ? policy.switchGapPoints : 10;
|
|
2369
|
+
const pinned = Boolean(bounds && bounds.pin);
|
|
2250
2370
|
const live = (windows || []).filter(
|
|
2251
2371
|
(w) => w && w.percentUsed !== null && w.percentUsed !== undefined && !w.stale && w.key !== binding.key
|
|
2252
2372
|
);
|
|
@@ -2263,12 +2383,13 @@ function escapeRoute(windows, binding, effortNote, host) {
|
|
|
2263
2383
|
// no other readable window there is no evidence a switch helps at all: it
|
|
2264
2384
|
// is true that the scoped window would retire, but "you have room" is a
|
|
2265
2385
|
// claim, and a claim with nothing behind it is the thing not to make.
|
|
2266
|
-
if (!next || next.percentUsed >= binding.percentUsed -
|
|
2386
|
+
if (!next || next.percentUsed >= binding.percentUsed - gapPoints) return null;
|
|
2267
2387
|
const roomier = after
|
|
2268
2388
|
.filter((w) => w.family && w.family !== binding.family)
|
|
2269
2389
|
.sort((a, b) => a.percentUsed - b.percentUsed)[0] || null;
|
|
2270
2390
|
return {
|
|
2271
2391
|
kind: 'model',
|
|
2392
|
+
report: pinned,
|
|
2272
2393
|
frees: binding.label || binding.key,
|
|
2273
2394
|
family: binding.family,
|
|
2274
2395
|
nextLabel: next ? next.label || next.key : null,
|
|
@@ -2283,6 +2404,7 @@ function escapeRoute(windows, binding, effortNote, host) {
|
|
|
2283
2404
|
if (effortNote && effortNote.cheaper && effortNote.cheaper.effort) {
|
|
2284
2405
|
return {
|
|
2285
2406
|
kind: 'effort',
|
|
2407
|
+
report: pinned,
|
|
2286
2408
|
from: effortNote.effort || null,
|
|
2287
2409
|
to: effortNote.cheaper.effort,
|
|
2288
2410
|
multiple: Number.isFinite(effortNote.cheaper.multiple) ? effortNote.cheaper.multiple : null,
|
|
@@ -2494,7 +2616,142 @@ function limitWindows(utilization) {
|
|
|
2494
2616
|
return rows;
|
|
2495
2617
|
}
|
|
2496
2618
|
|
|
2619
|
+
// Every window the snapshot knows about, without a scan.
|
|
2620
|
+
//
|
|
2621
|
+
// buildWindows() is the full answer and it costs a transcript read. Two
|
|
2622
|
+
// callers cannot afford that and still have to reason about windows rather
|
|
2623
|
+
// than about one field of the snapshot: the `off` guard, whose whole promise
|
|
2624
|
+
// is that the mode costs nothing, and `auto`, which has to pick a mode inside
|
|
2625
|
+
// a hook before anything expensive runs.
|
|
2626
|
+
//
|
|
2627
|
+
// Both used to read `utilization.limits` alone. That array is the account's
|
|
2628
|
+
// own description of its limits and it is genuinely useful, but it is also
|
|
2629
|
+
// optional - nothing else in the plugin requires it, and every fixture here
|
|
2630
|
+
// carries the top-level `five_hour`/`seven_day` keys instead. A guard wired to
|
|
2631
|
+
// the one field nothing else needs is a guard that is silent on a snapshot
|
|
2632
|
+
// every other reader handles, which is exactly what happened: at 97 per cent
|
|
2633
|
+
// used with no `limits` array, the only line `off` is allowed to emit was
|
|
2634
|
+
// never built.
|
|
2635
|
+
//
|
|
2636
|
+
// So: the bucket keys first, the `limits` array for anything they did not
|
|
2637
|
+
// cover, real labels on both, corrections applied, and the same applies-flag
|
|
2638
|
+
// every other reader makes its decisions from.
|
|
2639
|
+
function snapshotWindows(collected, now, codexHome) {
|
|
2640
|
+
const utilization = collected && collected.utilization;
|
|
2641
|
+
if (!utilization) return [];
|
|
2642
|
+
const at = Number.isFinite(now) ? now : Date.now();
|
|
2643
|
+
// No transcripts here on purpose - that is the cost being avoided - so the
|
|
2644
|
+
// setting is the only thing that says which model is running. It is enough
|
|
2645
|
+
// to keep a weekly for a model this agent is not using out of a worst-of.
|
|
2646
|
+
const families = familiesInUse(null, null, [
|
|
2647
|
+
collected.settings && collected.settings.model,
|
|
2648
|
+
process.env.ANTHROPIC_MODEL,
|
|
2649
|
+
]);
|
|
2650
|
+
const specs = collected.windowSpecs && collected.windowSpecs.length ? collected.windowSpecs : WINDOWS;
|
|
2651
|
+
const byKey = new Map();
|
|
2652
|
+
for (const spec of specs) {
|
|
2653
|
+
const snapshot = utilization[spec.key];
|
|
2654
|
+
if (!snapshot || typeof snapshot.utilization !== 'number') continue;
|
|
2655
|
+
const resetsAt = snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
|
|
2656
|
+
const corrected = reading.correctedFor(spec.key, at, collected.snapshotFetchedAt, codexHome);
|
|
2657
|
+
byKey.set(spec.key, {
|
|
2658
|
+
key: spec.key,
|
|
2659
|
+
label: spec.label,
|
|
2660
|
+
percentUsed: corrected && Number.isFinite(corrected.percentUsed) ? corrected.percentUsed : snapshot.utilization,
|
|
2661
|
+
resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
|
|
2662
|
+
stale: Number.isFinite(resetsAt) && resetsAt <= at,
|
|
2663
|
+
applies: appliesTo(spec, families),
|
|
2664
|
+
family: spec.family || null,
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
for (const limit of limitWindows(utilization)) {
|
|
2668
|
+
if (byKey.has(limit.key)) continue;
|
|
2669
|
+
const spec = WINDOWS.find((w) => w.key === limit.key) || null;
|
|
2670
|
+
const corrected = reading.correctedFor(limit.key, at, collected.snapshotFetchedAt, codexHome);
|
|
2671
|
+
const family = limit.family || (spec && spec.family) || null;
|
|
2672
|
+
byKey.set(limit.key, {
|
|
2673
|
+
key: limit.key,
|
|
2674
|
+
// A raw key is an internal name, not a window: "five_hour is 97% used"
|
|
2675
|
+
// in the one line `off` gets to say is the plugin talking to itself.
|
|
2676
|
+
label: limit.label || (spec && spec.label) || limit.key.replace(/_/g, ' '),
|
|
2677
|
+
percentUsed: corrected && Number.isFinite(corrected.percentUsed) ? corrected.percentUsed : limit.percent,
|
|
2678
|
+
resetsAt: limit.resetsAt,
|
|
2679
|
+
stale: Number.isFinite(limit.resetsAt) && limit.resetsAt <= at,
|
|
2680
|
+
applies: appliesTo({ family }, families),
|
|
2681
|
+
family,
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
return [...byKey.values()];
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// Antigravity, and the `agy` CLI behind it.
|
|
2688
|
+
//
|
|
2689
|
+
// This one is mostly a list of things that are NOT known, and that is the
|
|
2690
|
+
// honest shape of it. Antigravity refreshes its quota - its own log says
|
|
2691
|
+
// `quota_manager.go: doRefreshQuota: starting reload` - but it writes the
|
|
2692
|
+
// answer nowhere this can read. Its on-disk state is a protobuf text state
|
|
2693
|
+
// file, per-conversation SQLite databases and a settings file holding
|
|
2694
|
+
// agentMode, artifactReviewPolicy, colorScheme and trustedWorkspaces. There is
|
|
2695
|
+
// no percentage, no window and no reset time anywhere in it.
|
|
2696
|
+
//
|
|
2697
|
+
// An earlier version of this function filled that gap by reading CLAUDE's
|
|
2698
|
+
// meter and reporting it under a `cross-agent-claude` source, alongside a
|
|
2699
|
+
// hardcoded "Google AI Plus" plan and a made-up model of "Gemini 3.8 Flash
|
|
2700
|
+
// (High)" for a settings file that has no model key in it at all. Every one of
|
|
2701
|
+
// those numbers was about a different account, a different product and a
|
|
2702
|
+
// different budget. A plugin whose entire purpose is to be right about a
|
|
2703
|
+
// number must not invent one, so all of it is gone: where there is nothing to
|
|
2704
|
+
// read this returns null and the report says the quota is unreadable.
|
|
2705
|
+
//
|
|
2706
|
+
// What IS readable is worth having, because it is what a person can act on:
|
|
2707
|
+
// the agent mode, and whether the artifact review policy is set to wave
|
|
2708
|
+
// everything through. Both come from the real file.
|
|
2709
|
+
function collectGemini(now) {
|
|
2710
|
+
const settingsFile = path.join(host.geminiConfigDir(), 'antigravity-cli', 'settings.json');
|
|
2711
|
+
const settings = readJson(settingsFile) || {};
|
|
2712
|
+
const configFile = path.join(host.geminiConfigDir(), 'config', 'config.json');
|
|
2713
|
+
const config = readJson(configFile) || {};
|
|
2714
|
+
const userSettings = (config && config.userSettings) || {};
|
|
2715
|
+
|
|
2716
|
+
return {
|
|
2717
|
+
now,
|
|
2718
|
+
host: host.GEMINI,
|
|
2719
|
+
money: false,
|
|
2720
|
+
accountFile: settingsFile,
|
|
2721
|
+
// The plan is not on disk either. Naming one would be the same mistake in
|
|
2722
|
+
// a smaller font.
|
|
2723
|
+
plan: 'unknown',
|
|
2724
|
+
planId: 'gemini_unknown',
|
|
2725
|
+
planTier: null,
|
|
2726
|
+
planAdvice:
|
|
2727
|
+
'Antigravity does not publish remaining quota anywhere readable on disk, so this ' +
|
|
2728
|
+
'plugin cannot report a percentage for it. Its own /usage command is the only place ' +
|
|
2729
|
+
'the figure appears. What is enforced here instead is the ceiling: past it, fan-out ' +
|
|
2730
|
+
'calls are refused, which is the largest single saving available without a meter.',
|
|
2731
|
+
snapshotAgeMs: null,
|
|
2732
|
+
snapshotFetchedAt: null,
|
|
2733
|
+
snapshotSource: null,
|
|
2734
|
+
// Not "zero used". Unknown.
|
|
2735
|
+
utilization: null,
|
|
2736
|
+
// Said outright so the report can distinguish "no quota system" from
|
|
2737
|
+
// "quota system this cannot read". It is the second one.
|
|
2738
|
+
quotaUnreadable: true,
|
|
2739
|
+
settings: {
|
|
2740
|
+
// Only keys that exist in the real file. Antigravity chooses its model
|
|
2741
|
+
// per conversation and records it in a protobuf state file as an opaque
|
|
2742
|
+
// placeholder id, so there is no model name to report.
|
|
2743
|
+
model: 'unknown',
|
|
2744
|
+
effortLevel: 'unknown',
|
|
2745
|
+
agentMode: settings.agentMode || 'unknown',
|
|
2746
|
+
artifactReviewPolicy: settings.artifactReviewPolicy || null,
|
|
2747
|
+
autoExecutionPolicy: userSettings.autoExecutionPolicy || null,
|
|
2748
|
+
},
|
|
2749
|
+
extraUsage: null,
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2497
2753
|
function collect(now) {
|
|
2754
|
+
if (isGemini()) return collectGemini(now);
|
|
2498
2755
|
if (isCodex()) return codex.collect(now);
|
|
2499
2756
|
return collectClaude(now);
|
|
2500
2757
|
}
|
|
@@ -2950,6 +3207,11 @@ async function report(now, options) {
|
|
|
2950
3207
|
return Object.assign({}, base, {
|
|
2951
3208
|
windows,
|
|
2952
3209
|
binding,
|
|
3210
|
+
// Carried so a renderer can ask which mode THIS session is in. Without it
|
|
3211
|
+
// render() asked mode.forSession({}) with no id, which skips the session
|
|
3212
|
+
// override entirely and reported the persisted mode while a `--session`
|
|
3213
|
+
// override was the one actually in force.
|
|
3214
|
+
sessionId: (options && options.sessionId) || null,
|
|
2953
3215
|
otherLimits: otherLimits(base.utilization),
|
|
2954
3216
|
// The plan moved since anything was last learned about it, so the cached
|
|
2955
3217
|
// percentage was measured against a different allowance and everything
|
|
@@ -3136,7 +3398,7 @@ function render(data) {
|
|
|
3136
3398
|
// honest money column. Everything else in the table means the same thing on
|
|
3137
3399
|
// both hosts.
|
|
3138
3400
|
const money = data.money !== false;
|
|
3139
|
-
lines.push((data.host === host.CODEX ? 'Codex usage' : 'Claude Code usage'));
|
|
3401
|
+
lines.push((data.host === host.CODEX ? 'Codex usage' : data.host === host.GEMINI ? 'Gemini usage' : 'Claude Code usage'));
|
|
3140
3402
|
lines.push('');
|
|
3141
3403
|
lines.push(' Plan ' + data.plan);
|
|
3142
3404
|
lines.push(
|
|
@@ -3146,6 +3408,16 @@ function render(data) {
|
|
|
3146
3408
|
: formatDuration(data.snapshotAgeMs) + ' old' + (data.snapshotSource === 'live' ? ' (live reading)' : ''))
|
|
3147
3409
|
);
|
|
3148
3410
|
lines.push(' Settings model=' + data.settings.model + ' effort=' + data.settings.effortLevel);
|
|
3411
|
+
// Which budget mode the hooks are in, and who said so. Printed here because
|
|
3412
|
+
// the commonest confusion about this plugin is a line that did not appear:
|
|
3413
|
+
// in `off` nothing is injected at all, and the report is the one place that
|
|
3414
|
+
// can say why without injecting anything itself.
|
|
3415
|
+
try {
|
|
3416
|
+
const decided = require('./mode.js').forSession({ sessionId: data.sessionId || null });
|
|
3417
|
+
lines.push(' Mode ' + decided.label + ' (from ' + decided.source + ') - ' + decided.policy.summary);
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
// A report that fails over an optional row is worse than a missing row.
|
|
3420
|
+
}
|
|
3149
3421
|
if (data.planChanged) {
|
|
3150
3422
|
lines.push(' Plan change this is a different plan from the one the figures below');
|
|
3151
3423
|
lines.push(' were learned on, so what a point of a window is worth has');
|
|
@@ -3540,6 +3812,23 @@ function formatPercent(value) {
|
|
|
3540
3812
|
return value.toFixed(1) + '%';
|
|
3541
3813
|
}
|
|
3542
3814
|
|
|
3815
|
+
// The same figure divided by a turn count, which is a different scale.
|
|
3816
|
+
//
|
|
3817
|
+
// formatPercent is built for window totals, where one decimal is plenty. A
|
|
3818
|
+
// per-turn share of a window is one or two orders of magnitude smaller: a
|
|
3819
|
+
// 15-turn job costing 0.6 points of the window is 0.04 a turn, and one decimal
|
|
3820
|
+
// prints that as "0.0%", which reads as free. The sentence exists to make the
|
|
3821
|
+
// table above checkable, so it has to carry enough precision to multiply back.
|
|
3822
|
+
function formatRatePercent(value) {
|
|
3823
|
+
if (!Number.isFinite(value)) return '-';
|
|
3824
|
+
if (value >= 10) return Math.round(value) + '%';
|
|
3825
|
+
if (value >= 0.1) return value.toFixed(1) + '%';
|
|
3826
|
+
if (value <= 0) return '0%';
|
|
3827
|
+
// Two decimals down to 0.01, and an honest bound below that rather than a
|
|
3828
|
+
// string of zeroes pretending to be a measurement.
|
|
3829
|
+
return value >= 0.005 ? value.toFixed(2) + '%' : 'under 0.01%';
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3543
3832
|
function renderForecast(data, turns) {
|
|
3544
3833
|
const lines = [];
|
|
3545
3834
|
|
|
@@ -3586,8 +3875,17 @@ function renderForecast(data, turns) {
|
|
|
3586
3875
|
}
|
|
3587
3876
|
lines.push('');
|
|
3588
3877
|
lines.push(
|
|
3878
|
+
// Codex meters a share of an allowance and never quotes a price, so a
|
|
3879
|
+
// dollar figure here would be invented. The rate is still worth stating -
|
|
3880
|
+
// it is what makes the table above checkable and what a person carries to
|
|
3881
|
+
// the next job - so it is quoted in the unit Codex actually has: points of
|
|
3882
|
+
// the window a turn costs. The table's own figures divided by the turns
|
|
3883
|
+
// they were priced for, so the two can never disagree.
|
|
3589
3884
|
data.money === false
|
|
3590
|
-
? ' Priced from ' + data.rates.sample + ' recent turns
|
|
3885
|
+
? ' Priced from ' + data.rates.sample + ' recent turns: about ' +
|
|
3886
|
+
formatRatePercent(rows[0].percentLow / turns) + ' of the ' + rows[0].label +
|
|
3887
|
+
' window per turn typical, ' + formatRatePercent(rows[0].percentHigh / turns) +
|
|
3888
|
+
' at the expensive end.'
|
|
3591
3889
|
: ' Priced from ' + data.rates.sample + ' recent turns: ' +
|
|
3592
3890
|
formatUSD(data.rates.median) + ' typical, ' + formatUSD(data.rates.high) +
|
|
3593
3891
|
' at the expensive end.'
|
|
@@ -3788,12 +4086,53 @@ async function main(argv) {
|
|
|
3788
4086
|
.filter((window) => window.applies !== false)
|
|
3789
4087
|
.map((window) => forecastWindow(window, turns, data.rates))
|
|
3790
4088
|
.filter(Boolean);
|
|
3791
|
-
|
|
4089
|
+
// Same host split as the text renderer: without this a Codex reading's
|
|
4090
|
+
// rates would print as bare numbers and a consumer could not tell they
|
|
4091
|
+
// are window points rather than dollars.
|
|
4092
|
+
process.stdout.write(
|
|
4093
|
+
JSON.stringify({ turns, money: data.money !== false, rates: data.rates, windows: rows }, null, 2) + '\n'
|
|
4094
|
+
);
|
|
3792
4095
|
} else {
|
|
3793
4096
|
process.stdout.write(renderForecast(data, turns) + '\n');
|
|
3794
4097
|
}
|
|
3795
4098
|
return 0;
|
|
3796
4099
|
}
|
|
4100
|
+
// `--effort ultracode` records what nothing on the machine can see: the
|
|
4101
|
+
// picker's choice is session-only and touches neither disk nor environment.
|
|
4102
|
+
// `--effort clear` forgets it again.
|
|
4103
|
+
const effortAt = argv.indexOf('--effort');
|
|
4104
|
+
if (effortAt !== -1) {
|
|
4105
|
+
const wanted = String(argv[effortAt + 1] || '').trim().toLowerCase();
|
|
4106
|
+
const say = (text) => {
|
|
4107
|
+
process.stdout.write(text + '\n');
|
|
4108
|
+
return 0;
|
|
4109
|
+
};
|
|
4110
|
+
if (!wanted) {
|
|
4111
|
+
const found = effortNow();
|
|
4112
|
+
if (!found) return say('No effort reading available.');
|
|
4113
|
+
const guess = found.source === 'settings' || found.source === 'transcript';
|
|
4114
|
+
return say(
|
|
4115
|
+
'Effort reads as ' + found.effort + ' (' + found.source + ').' +
|
|
4116
|
+
(guess
|
|
4117
|
+
? '\nIf you picked ultracode in the session, nothing on disk says so - Claude Code calls it' +
|
|
4118
|
+
' "xhigh + dynamic workflow orchestration (this session only)".' +
|
|
4119
|
+
' Tell this: usage.js --effort ultracode'
|
|
4120
|
+
: '')
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
if (wanted === 'clear' || wanted === 'off') {
|
|
4124
|
+
writeEffortOverride(null);
|
|
4125
|
+
return say('Effort override cleared; back to the environment, then settings.');
|
|
4126
|
+
}
|
|
4127
|
+
const done = writeEffortOverride(wanted, Date.now());
|
|
4128
|
+
return say(
|
|
4129
|
+
done.ok
|
|
4130
|
+
? 'Effort recorded as ' + done.effort +
|
|
4131
|
+
'. It expires after twelve hours, and CLAUDE_CODE_EFFORT_LEVEL still wins.'
|
|
4132
|
+
: done.error
|
|
4133
|
+
);
|
|
4134
|
+
}
|
|
4135
|
+
|
|
3797
4136
|
const recommendAt = argv.indexOf('--recommend');
|
|
3798
4137
|
if (recommendAt !== -1) {
|
|
3799
4138
|
// The turn count is optional: with one the verdict is about that job,
|
|
@@ -3881,11 +4220,19 @@ module.exports = {
|
|
|
3881
4220
|
accountFile,
|
|
3882
4221
|
buildWindows,
|
|
3883
4222
|
limitWindows,
|
|
4223
|
+
snapshotWindows,
|
|
3884
4224
|
lastRejections,
|
|
3885
4225
|
bindingWindow,
|
|
3886
4226
|
criticalOthers,
|
|
3887
4227
|
escapeRoute,
|
|
3888
4228
|
settingFit,
|
|
4229
|
+
fitFromRates,
|
|
4230
|
+
formatRatePercent,
|
|
4231
|
+
effortNow,
|
|
4232
|
+
readEffortOverride,
|
|
4233
|
+
writeEffortOverride,
|
|
4234
|
+
effortOverrideFile,
|
|
4235
|
+
EFFORT_LEVELS,
|
|
3889
4236
|
levers,
|
|
3890
4237
|
betterCalibration,
|
|
3891
4238
|
calibrationForPlan,
|
|
@@ -352,6 +352,10 @@ function build(input) {
|
|
|
352
352
|
rows,
|
|
353
353
|
fable,
|
|
354
354
|
hidden,
|
|
355
|
+
// Which budget mode the hooks are in, when it is not the one they have
|
|
356
|
+
// always been in. Passed in rather than read here: this module builds a
|
|
357
|
+
// display out of what it is given and touches no files.
|
|
358
|
+
budget: opts.budget && opts.budget.name ? { name: opts.budget.name, label: opts.budget.label || opts.budget.name } : null,
|
|
355
359
|
// The agents underneath this session. They spend the same window and had
|
|
356
360
|
// no voice on any display until now.
|
|
357
361
|
agents: opts.agents && Number.isFinite(opts.agents.running) ? opts.agents : { running: 0, runs: 0 },
|
|
@@ -31,6 +31,9 @@ const fs = require('fs');
|
|
|
31
31
|
const os = require('os');
|
|
32
32
|
const path = require('path');
|
|
33
33
|
|
|
34
|
+
const host = require('./host.js');
|
|
35
|
+
const codex = require('./codex.js');
|
|
36
|
+
|
|
34
37
|
// Enough for the stable signals - message length, punctuation, openers - to
|
|
35
38
|
// stop moving. Short-message authorship work stops gaining accuracy at around
|
|
36
39
|
// a hundred and twenty messages, and says very little below a dozen.
|
|
@@ -48,8 +51,14 @@ const PROMPT_MAX_WORDS = 400;
|
|
|
48
51
|
|
|
49
52
|
const OPENERS_KEPT = 6;
|
|
50
53
|
|
|
54
|
+
// Every other state-writing script splits on the host so a Codex session
|
|
55
|
+
// learns into ~/.codex and a Claude Code one into ~/.claude; this one used to
|
|
56
|
+
// hardcode the Claude side, so under Codex the voice profile was written
|
|
57
|
+
// somewhere Codex never reads and learned nothing across sessions.
|
|
51
58
|
function configDir() {
|
|
52
|
-
return process.
|
|
59
|
+
return host.detect(process.argv.slice(2), process.env) === host.CODEX
|
|
60
|
+
? codex.homeDir()
|
|
61
|
+
: process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
function voiceFile() {
|