claude-usage-limits 1.19.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 +244 -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;
|
|
@@ -2259,16 +2263,30 @@ function settingFit(events, effortNow, which) {
|
|
|
2259
2263
|
// Guarded here rather than trusted from the caller: a cache hit has no event
|
|
2260
2264
|
// list to hand over, and dominantEffort iterates without checking.
|
|
2261
2265
|
const list = Array.isArray(events) ? events : [];
|
|
2262
|
-
|
|
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;
|
|
2263
2281
|
if (!current) return null;
|
|
2264
|
-
const
|
|
2265
|
-
const here =
|
|
2282
|
+
const table = Array.isArray(rates) ? rates : [];
|
|
2283
|
+
const here = table.find((row) => row.effort === current);
|
|
2266
2284
|
if (!here || here.turns < MIN_EFFORT_SAMPLE) return null;
|
|
2267
2285
|
const measure = (row) => (Number.isFinite(row.outputPerTurn) && row.outputPerTurn > 0 ? row.outputPerTurn : row.perTurn);
|
|
2268
2286
|
const mine = measure(here);
|
|
2269
2287
|
if (!Number.isFinite(mine) || mine <= 0) return null;
|
|
2270
2288
|
let best = null;
|
|
2271
|
-
for (const row of
|
|
2289
|
+
for (const row of table) {
|
|
2272
2290
|
if (row.effort === current || row.turns < MIN_EFFORT_SAMPLE) continue;
|
|
2273
2291
|
const theirs = measure(row);
|
|
2274
2292
|
if (!Number.isFinite(theirs) || theirs <= 0) continue;
|
|
@@ -2309,23 +2327,46 @@ function levers(which) {
|
|
|
2309
2327
|
// `host` is the required module here, not a parameter - shadowing it was a
|
|
2310
2328
|
// ReferenceError on the constant it owns.
|
|
2311
2329
|
const codex = which === host.CODEX;
|
|
2330
|
+
const gemini = which === host.GEMINI;
|
|
2312
2331
|
return {
|
|
2313
2332
|
model: (family) =>
|
|
2314
|
-
|
|
2315
|
-
?
|
|
2316
|
-
|
|
2317
|
-
|
|
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>)',
|
|
2318
2339
|
effort: (level) =>
|
|
2319
|
-
|
|
2320
|
-
?
|
|
2321
|
-
|
|
2322
|
-
|
|
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>'),
|
|
2323
2346
|
};
|
|
2324
2347
|
}
|
|
2325
2348
|
|
|
2326
|
-
|
|
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) {
|
|
2327
2366
|
if (!binding || binding.percentUsed === null || binding.percentUsed === undefined) return null;
|
|
2328
2367
|
const lever = levers(host || currentHost());
|
|
2368
|
+
const gapPoints = policy && Number.isFinite(policy.switchGapPoints) ? policy.switchGapPoints : 10;
|
|
2369
|
+
const pinned = Boolean(bounds && bounds.pin);
|
|
2329
2370
|
const live = (windows || []).filter(
|
|
2330
2371
|
(w) => w && w.percentUsed !== null && w.percentUsed !== undefined && !w.stale && w.key !== binding.key
|
|
2331
2372
|
);
|
|
@@ -2342,12 +2383,13 @@ function escapeRoute(windows, binding, effortNote, host) {
|
|
|
2342
2383
|
// no other readable window there is no evidence a switch helps at all: it
|
|
2343
2384
|
// is true that the scoped window would retire, but "you have room" is a
|
|
2344
2385
|
// claim, and a claim with nothing behind it is the thing not to make.
|
|
2345
|
-
if (!next || next.percentUsed >= binding.percentUsed -
|
|
2386
|
+
if (!next || next.percentUsed >= binding.percentUsed - gapPoints) return null;
|
|
2346
2387
|
const roomier = after
|
|
2347
2388
|
.filter((w) => w.family && w.family !== binding.family)
|
|
2348
2389
|
.sort((a, b) => a.percentUsed - b.percentUsed)[0] || null;
|
|
2349
2390
|
return {
|
|
2350
2391
|
kind: 'model',
|
|
2392
|
+
report: pinned,
|
|
2351
2393
|
frees: binding.label || binding.key,
|
|
2352
2394
|
family: binding.family,
|
|
2353
2395
|
nextLabel: next ? next.label || next.key : null,
|
|
@@ -2362,6 +2404,7 @@ function escapeRoute(windows, binding, effortNote, host) {
|
|
|
2362
2404
|
if (effortNote && effortNote.cheaper && effortNote.cheaper.effort) {
|
|
2363
2405
|
return {
|
|
2364
2406
|
kind: 'effort',
|
|
2407
|
+
report: pinned,
|
|
2365
2408
|
from: effortNote.effort || null,
|
|
2366
2409
|
to: effortNote.cheaper.effort,
|
|
2367
2410
|
multiple: Number.isFinite(effortNote.cheaper.multiple) ? effortNote.cheaper.multiple : null,
|
|
@@ -2573,7 +2616,142 @@ function limitWindows(utilization) {
|
|
|
2573
2616
|
return rows;
|
|
2574
2617
|
}
|
|
2575
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
|
+
|
|
2576
2753
|
function collect(now) {
|
|
2754
|
+
if (isGemini()) return collectGemini(now);
|
|
2577
2755
|
if (isCodex()) return codex.collect(now);
|
|
2578
2756
|
return collectClaude(now);
|
|
2579
2757
|
}
|
|
@@ -3029,6 +3207,11 @@ async function report(now, options) {
|
|
|
3029
3207
|
return Object.assign({}, base, {
|
|
3030
3208
|
windows,
|
|
3031
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,
|
|
3032
3215
|
otherLimits: otherLimits(base.utilization),
|
|
3033
3216
|
// The plan moved since anything was last learned about it, so the cached
|
|
3034
3217
|
// percentage was measured against a different allowance and everything
|
|
@@ -3215,7 +3398,7 @@ function render(data) {
|
|
|
3215
3398
|
// honest money column. Everything else in the table means the same thing on
|
|
3216
3399
|
// both hosts.
|
|
3217
3400
|
const money = data.money !== false;
|
|
3218
|
-
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'));
|
|
3219
3402
|
lines.push('');
|
|
3220
3403
|
lines.push(' Plan ' + data.plan);
|
|
3221
3404
|
lines.push(
|
|
@@ -3225,6 +3408,16 @@ function render(data) {
|
|
|
3225
3408
|
: formatDuration(data.snapshotAgeMs) + ' old' + (data.snapshotSource === 'live' ? ' (live reading)' : ''))
|
|
3226
3409
|
);
|
|
3227
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
|
+
}
|
|
3228
3421
|
if (data.planChanged) {
|
|
3229
3422
|
lines.push(' Plan change this is a different plan from the one the figures below');
|
|
3230
3423
|
lines.push(' were learned on, so what a point of a window is worth has');
|
|
@@ -3619,6 +3812,23 @@ function formatPercent(value) {
|
|
|
3619
3812
|
return value.toFixed(1) + '%';
|
|
3620
3813
|
}
|
|
3621
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
|
+
|
|
3622
3832
|
function renderForecast(data, turns) {
|
|
3623
3833
|
const lines = [];
|
|
3624
3834
|
|
|
@@ -3665,8 +3875,17 @@ function renderForecast(data, turns) {
|
|
|
3665
3875
|
}
|
|
3666
3876
|
lines.push('');
|
|
3667
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.
|
|
3668
3884
|
data.money === false
|
|
3669
|
-
? ' 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.'
|
|
3670
3889
|
: ' Priced from ' + data.rates.sample + ' recent turns: ' +
|
|
3671
3890
|
formatUSD(data.rates.median) + ' typical, ' + formatUSD(data.rates.high) +
|
|
3672
3891
|
' at the expensive end.'
|
|
@@ -3867,7 +4086,12 @@ async function main(argv) {
|
|
|
3867
4086
|
.filter((window) => window.applies !== false)
|
|
3868
4087
|
.map((window) => forecastWindow(window, turns, data.rates))
|
|
3869
4088
|
.filter(Boolean);
|
|
3870
|
-
|
|
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
|
+
);
|
|
3871
4095
|
} else {
|
|
3872
4096
|
process.stdout.write(renderForecast(data, turns) + '\n');
|
|
3873
4097
|
}
|
|
@@ -3996,11 +4220,14 @@ module.exports = {
|
|
|
3996
4220
|
accountFile,
|
|
3997
4221
|
buildWindows,
|
|
3998
4222
|
limitWindows,
|
|
4223
|
+
snapshotWindows,
|
|
3999
4224
|
lastRejections,
|
|
4000
4225
|
bindingWindow,
|
|
4001
4226
|
criticalOthers,
|
|
4002
4227
|
escapeRoute,
|
|
4003
4228
|
settingFit,
|
|
4229
|
+
fitFromRates,
|
|
4230
|
+
formatRatePercent,
|
|
4004
4231
|
effortNow,
|
|
4005
4232
|
readEffortOverride,
|
|
4006
4233
|
writeEffortOverride,
|
|
@@ -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() {
|