claude-usage-limits 1.7.1 → 1.9.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 +118 -12
- package/bin/cli.js +3 -0
- package/commands/session.md +13 -0
- package/hooks/hooks.json +23 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +69 -1
- package/skills/usage-limits/references/how-it-works.md +15 -0
- package/skills/usage-limits/references/tactics.md +8 -1
- package/skills/usage-limits/scripts/brief.js +147 -11
- package/skills/usage-limits/scripts/lowpower.js +9 -0
- package/skills/usage-limits/scripts/pulse.js +6 -4
- package/skills/usage-limits/scripts/recommend.js +295 -0
- package/skills/usage-limits/scripts/sessionend.js +47 -0
- package/skills/usage-limits/scripts/stop.js +55 -0
- package/skills/usage-limits/scripts/tally.js +378 -0
- package/skills/usage-limits/scripts/usage.js +488 -70
|
@@ -55,7 +55,7 @@ const RATES = {
|
|
|
55
55
|
'claude-opus-4-8': { input: 5, output: 25 },
|
|
56
56
|
'claude-opus-4-7': { input: 5, output: 25 },
|
|
57
57
|
'claude-opus-4-6': { input: 5, output: 25 },
|
|
58
|
-
'claude-sonnet-5': { input:
|
|
58
|
+
'claude-sonnet-5': { input: 2, output: 10 },
|
|
59
59
|
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
60
60
|
'claude-haiku-4-5': { input: 1, output: 5 },
|
|
61
61
|
};
|
|
@@ -296,6 +296,10 @@ function eventFrom(line, seen, project) {
|
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// Interrupts and client-side errors are written as assistant messages from
|
|
300
|
+
// a model called <synthetic>, with a usage block of zeros. Not a call.
|
|
301
|
+
if (entry.message.model === '<synthetic>') return null;
|
|
302
|
+
|
|
299
303
|
// A resumed or forked session repeats earlier turns in a new file.
|
|
300
304
|
const id = (entry.message.id || '') + '|' + (entry.requestId || '');
|
|
301
305
|
if (id !== '|' && seen) {
|
|
@@ -303,23 +307,76 @@ function eventFrom(line, seen, project) {
|
|
|
303
307
|
seen.add(id);
|
|
304
308
|
}
|
|
305
309
|
|
|
310
|
+
const parts = tokenParts(entry.message.usage);
|
|
306
311
|
return {
|
|
307
312
|
at,
|
|
308
313
|
model: entry.message.model || '',
|
|
309
314
|
effort: entry.effort || null,
|
|
310
315
|
cost: costOf(entry.message.usage, entry.message.model),
|
|
311
316
|
tokens: tokensOf(entry.message.usage),
|
|
312
|
-
parts
|
|
317
|
+
parts,
|
|
318
|
+
// What the model was shown on this call: everything except what it wrote.
|
|
319
|
+
// It is re-sent on every later call, so it is the recurring cost of the
|
|
320
|
+
// session, and the one number that says how bloated the context has got.
|
|
321
|
+
context: parts.input + parts.cacheRead + parts.cacheWrite,
|
|
313
322
|
project: project || null,
|
|
314
323
|
sessionId: entry.sessionId || null,
|
|
324
|
+
// A subagent's turns are written under the parent session with these set.
|
|
325
|
+
sidechain: Boolean(entry.isSidechain || entry.agentId),
|
|
315
326
|
};
|
|
316
327
|
}
|
|
317
328
|
|
|
329
|
+
// Whether a transcript line is a prompt the user typed. Tool results are also
|
|
330
|
+
// written as user messages, and so are internal notes marked isMeta; neither
|
|
331
|
+
// is something a person asked for.
|
|
332
|
+
function promptFrom(line) {
|
|
333
|
+
if (line.indexOf('"user"') === -1) return false;
|
|
334
|
+
let entry;
|
|
335
|
+
try {
|
|
336
|
+
entry = JSON.parse(line);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (!entry || entry.type !== 'user' || entry.isMeta || !entry.message) return false;
|
|
341
|
+
const content = entry.message.content;
|
|
342
|
+
if (typeof content === 'string') return content.length > 0;
|
|
343
|
+
if (!Array.isArray(content)) return false;
|
|
344
|
+
let typed = false;
|
|
345
|
+
for (const block of content) {
|
|
346
|
+
if (!block) continue;
|
|
347
|
+
if (block.type === 'tool_result') return false;
|
|
348
|
+
if (block.type === 'text') typed = true;
|
|
349
|
+
}
|
|
350
|
+
return typed;
|
|
351
|
+
}
|
|
352
|
+
|
|
318
353
|
async function readEvents(since) {
|
|
319
354
|
if (isCodex()) return codex.readEvents(since);
|
|
320
355
|
return readClaudeEvents(since);
|
|
321
356
|
}
|
|
322
357
|
|
|
358
|
+
// A file last touched before the window opened holds nothing useful.
|
|
359
|
+
function fresh(file, since) {
|
|
360
|
+
try {
|
|
361
|
+
return fs.statSync(file).mtimeMs >= since;
|
|
362
|
+
} catch (err) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function freshFiles(dir, since) {
|
|
368
|
+
let names;
|
|
369
|
+
try {
|
|
370
|
+
names = fs.readdirSync(dir);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
return names
|
|
375
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
376
|
+
.map((name) => path.join(dir, name))
|
|
377
|
+
.filter((file) => fresh(file, since));
|
|
378
|
+
}
|
|
379
|
+
|
|
323
380
|
async function readClaudeEvents(since) {
|
|
324
381
|
const root = path.join(configDir(), 'projects');
|
|
325
382
|
let dirs = [];
|
|
@@ -333,21 +390,25 @@ async function readClaudeEvents(since) {
|
|
|
333
390
|
for (const dir of dirs) {
|
|
334
391
|
if (!dir.isDirectory()) continue;
|
|
335
392
|
const full = path.join(root, dir.name);
|
|
336
|
-
let
|
|
393
|
+
let entries = [];
|
|
337
394
|
try {
|
|
338
|
-
|
|
395
|
+
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
339
396
|
} catch (err) {
|
|
340
397
|
continue;
|
|
341
398
|
}
|
|
342
|
-
for (const
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
if (entry.isDirectory()) {
|
|
401
|
+
// A session's subagents write their transcripts under
|
|
402
|
+
// <project>/<session id>/subagents/. Same budget, different file, and
|
|
403
|
+
// for a long time an Explore or Plan agent's whole spend went unseen.
|
|
404
|
+
for (const file of freshFiles(path.join(full, entry.name, 'subagents'), since)) {
|
|
405
|
+
files.push({ file, project: dir.name });
|
|
406
|
+
}
|
|
349
407
|
continue;
|
|
350
408
|
}
|
|
409
|
+
if (!entry.name.endsWith('.jsonl')) continue;
|
|
410
|
+
const file = path.join(full, entry.name);
|
|
411
|
+
if (!fresh(file, since)) continue;
|
|
351
412
|
files.push({ file, project: dir.name });
|
|
352
413
|
}
|
|
353
414
|
}
|
|
@@ -374,13 +435,20 @@ async function readClaudeEvents(since) {
|
|
|
374
435
|
return events;
|
|
375
436
|
}
|
|
376
437
|
|
|
438
|
+
// A turn is one main-thread call, the unit the headroom is planned in. A
|
|
439
|
+
// subagent's calls spend the same budget, so they count in the money and the
|
|
440
|
+
// tokens, and are counted apart so they never inflate the turn figures.
|
|
377
441
|
function totals(events) {
|
|
378
442
|
let cost = 0;
|
|
379
443
|
let tokens = 0;
|
|
444
|
+
let turns = 0;
|
|
445
|
+
let subagentTurns = 0;
|
|
380
446
|
const parts = { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, reasoning: 0 };
|
|
381
447
|
for (const event of events) {
|
|
382
448
|
cost += event.cost;
|
|
383
449
|
tokens += event.tokens;
|
|
450
|
+
if (event.sidechain) subagentTurns += 1;
|
|
451
|
+
else turns += 1;
|
|
384
452
|
if (event.parts) {
|
|
385
453
|
parts.input += event.parts.input;
|
|
386
454
|
parts.cacheWrite += event.parts.cacheWrite;
|
|
@@ -389,7 +457,11 @@ function totals(events) {
|
|
|
389
457
|
parts.reasoning += event.parts.reasoning || 0;
|
|
390
458
|
}
|
|
391
459
|
}
|
|
392
|
-
return { cost, tokens, turns
|
|
460
|
+
return { cost, tokens, turns, subagentTurns, parts };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function mainThread(events) {
|
|
464
|
+
return (events || []).filter((event) => !event.sidechain);
|
|
393
465
|
}
|
|
394
466
|
|
|
395
467
|
// What each model actually cost, dearest first.
|
|
@@ -437,8 +509,9 @@ function typicalTurnCost(recentEvents, windowEvents, allEvents, minSample) {
|
|
|
437
509
|
|
|
438
510
|
// What a turn costs is a fact about how you work, not about which budget it
|
|
439
511
|
// is being measured against, so a thin window borrows from a wider sample
|
|
440
|
-
// rather than inventing a figure from two turns.
|
|
441
|
-
|
|
512
|
+
// rather than inventing a figure from two turns. Subagent calls are left
|
|
513
|
+
// out: they are small and many, and would make a turn look cheap.
|
|
514
|
+
const tiers = [mainThread(recentEvents), mainThread(windowEvents), mainThread(allEvents)];
|
|
442
515
|
let pool = [];
|
|
443
516
|
for (const tier of tiers) {
|
|
444
517
|
if (tier && tier.length >= floor) {
|
|
@@ -578,7 +651,7 @@ function creditsFrom(utilization) {
|
|
|
578
651
|
// files costs many times one that answers from context. A median alone
|
|
579
652
|
// under-promises on the expensive half, so carry a high end too.
|
|
580
653
|
function costPercentiles(events) {
|
|
581
|
-
const costs = (events
|
|
654
|
+
const costs = mainThread(events)
|
|
582
655
|
.map((event) => event.cost)
|
|
583
656
|
.filter((cost) => Number.isFinite(cost) && cost > 0)
|
|
584
657
|
.sort((a, b) => a - b);
|
|
@@ -640,11 +713,25 @@ function activeSessions(events, now, windowMs) {
|
|
|
640
713
|
|
|
641
714
|
// The slice of the shared budget this session is actually getting. With
|
|
642
715
|
// another session spending half of it, only half those turns are yours.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
716
|
+
//
|
|
717
|
+
// Past spend says how the budget has been going, not how it will go. A
|
|
718
|
+
// session that has only just started has almost none of it, and dividing its
|
|
719
|
+
// headroom by that share once turned 208 turns into "about 6 turns left" two
|
|
720
|
+
// tool calls into a session at 13 per cent used. So an equal split is the
|
|
721
|
+
// floor: a measured share can raise it, never lower it. `activeCount` is how
|
|
722
|
+
// many sessions are open, which can exceed how many have spent yet; the split
|
|
723
|
+
// is among all of them.
|
|
724
|
+
function shareOf(sessions, sessionId, activeCount) {
|
|
725
|
+
const spent = sessions || [];
|
|
726
|
+
const n = Math.max(spent.length, Number.isFinite(activeCount) ? activeCount : 0);
|
|
727
|
+
if (n < 2) return 1;
|
|
728
|
+
const equal = 1 / n;
|
|
729
|
+
// One session's spend is not a comparison. Until a second one has spent,
|
|
730
|
+
// being the only one on record says nothing about who owns the budget.
|
|
731
|
+
if (spent.length < 2) return equal;
|
|
732
|
+
const mine = spent.find((row) => row.sessionId === sessionId);
|
|
733
|
+
if (!mine || !(mine.share > 0)) return equal;
|
|
734
|
+
return Math.max(mine.share, equal);
|
|
648
735
|
}
|
|
649
736
|
|
|
650
737
|
// What a point of a window costs is a property of the plan, not of the moment,
|
|
@@ -725,13 +812,24 @@ function stampPlan(all, planId) {
|
|
|
725
812
|
return stamped;
|
|
726
813
|
}
|
|
727
814
|
|
|
728
|
-
// A sample is better when it rests on more
|
|
729
|
-
// numbers, so a
|
|
815
|
+
// A sample is better when it rests on more of the meter. Percentages read in
|
|
816
|
+
// whole numbers, so a reading at 1% prices a point against a bracket that is
|
|
817
|
+
// mostly rounding, while one at 60% divides by a number that means something.
|
|
818
|
+
// Turn count only breaks the tie: it says how much local spend sat behind the
|
|
819
|
+
// reading, not how precise the denominator was, and preferring it outright is
|
|
820
|
+
// how a 44-turn baseline read at 1% once beat every honest sample after it.
|
|
730
821
|
function betterCalibration(current, candidate) {
|
|
731
822
|
if (!candidate || !Number.isFinite(candidate.usdPerPercent) || candidate.usdPerPercent <= 0) {
|
|
732
823
|
return current || null;
|
|
733
824
|
}
|
|
734
825
|
if (!current || !Number.isFinite(current.turns)) return candidate;
|
|
826
|
+
if (
|
|
827
|
+
Number.isFinite(candidate.percent) &&
|
|
828
|
+
Number.isFinite(current.percent) &&
|
|
829
|
+
candidate.percent !== current.percent
|
|
830
|
+
) {
|
|
831
|
+
return candidate.percent > current.percent ? candidate : current;
|
|
832
|
+
}
|
|
735
833
|
return candidate.turns > current.turns ? candidate : current;
|
|
736
834
|
}
|
|
737
835
|
|
|
@@ -768,6 +866,7 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
768
866
|
spentUSD: spent.cost,
|
|
769
867
|
spentTokens: spent.tokens,
|
|
770
868
|
turns: spent.turns,
|
|
869
|
+
subagentTurns: spent.subagentTurns,
|
|
771
870
|
recentTurns: recent.turns,
|
|
772
871
|
recentUSDPerHour: recent.cost / recentHours,
|
|
773
872
|
recentUSDPerTurn: recent.turns ? recent.cost / recent.turns : null,
|
|
@@ -789,6 +888,14 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
789
888
|
correctionUnreliable: false,
|
|
790
889
|
// The price-per-point this window derived from its own baseline.
|
|
791
890
|
calibration: null,
|
|
891
|
+
// True when the bucket quoted its limit in dollars, so the price of a
|
|
892
|
+
// point is known rather than learned.
|
|
893
|
+
metered: false,
|
|
894
|
+
// What the account's own list of limits says about this one.
|
|
895
|
+
severity: null,
|
|
896
|
+
isActive: false,
|
|
897
|
+
scoped: false,
|
|
898
|
+
family: null,
|
|
792
899
|
verdict: 'unknown',
|
|
793
900
|
};
|
|
794
901
|
|
|
@@ -837,19 +944,31 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
837
944
|
// holding. The learned price covers it: what a point costs is a property
|
|
838
945
|
// of the plan, not of this reading.
|
|
839
946
|
const selfPriced =
|
|
840
|
-
rawPercent
|
|
947
|
+
rawPercent >= MIN_BASELINE_PERCENT && upTo.cost > 0 && upTo.turns >= MIN_BASELINE_TURNS
|
|
841
948
|
? { usdPerPercent: upTo.cost / rawPercent, turns: upTo.turns, percent: rawPercent }
|
|
842
949
|
: null;
|
|
843
|
-
|
|
950
|
+
// A metered window has nothing to learn: its price per point is stated.
|
|
951
|
+
if (selfPriced && !extra.metered) window.calibration = selfPriced;
|
|
844
952
|
|
|
845
953
|
const known = extra.knownCalibration;
|
|
954
|
+
// A remembered price read off a near-empty meter is the same rounding
|
|
955
|
+
// bracket in disguise, so it is no more usable than measuring one now.
|
|
846
956
|
const usable =
|
|
847
|
-
known &&
|
|
848
|
-
|
|
957
|
+
known &&
|
|
958
|
+
Number.isFinite(known.usdPerPercent) &&
|
|
959
|
+
known.usdPerPercent > 0 &&
|
|
960
|
+
!(Number.isFinite(known.percent) && known.percent < MIN_BASELINE_PERCENT)
|
|
961
|
+
? known
|
|
962
|
+
: null;
|
|
963
|
+
// Trust the better-measured of the two, whichever that is; a stated price
|
|
964
|
+
// beats both.
|
|
965
|
+
const stated =
|
|
966
|
+
extra.metered && Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
967
|
+
? { usdPerPercent: extra.usdPerPercent, turns: Infinity }
|
|
968
|
+
: null;
|
|
849
969
|
const chosen =
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
: selfPriced || usable;
|
|
970
|
+
stated ||
|
|
971
|
+
(selfPriced && usable ? betterCalibration(usable, selfPriced) : selfPriced || usable);
|
|
853
972
|
|
|
854
973
|
if (after.cost > 0 && chosen) {
|
|
855
974
|
const pricePerPoint = chosen.usdPerPercent;
|
|
@@ -859,6 +978,10 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
859
978
|
// not the window. Better to leave the reading uncorrected and say the
|
|
860
979
|
// snapshot is old than to assert a budget that is gone.
|
|
861
980
|
if (rawPercent + sinceSnapshot > SATURATION_LIMIT) {
|
|
981
|
+
// Keep the size of the overshoot: the brief needs it to say how far
|
|
982
|
+
// past the snapshot the spending has gone, instead of repeating the
|
|
983
|
+
// snapshot figure for hours as if it were current.
|
|
984
|
+
window.pointsBeyondSnapshot = Math.round(sinceSnapshot);
|
|
862
985
|
sinceSnapshot = 0;
|
|
863
986
|
window.correctionUnreliable = true;
|
|
864
987
|
} else if (sinceSnapshot >= 1) {
|
|
@@ -881,23 +1004,35 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
881
1004
|
// so when the two disagree that badly it is the local history that is
|
|
882
1005
|
// incomplete, not the meter.
|
|
883
1006
|
const thin = spent.turns < MIN_BASELINE_TURNS && percent >= UNEXPLAINED_PERCENT;
|
|
884
|
-
|
|
1007
|
+
// The same rounding bracket again, in the optimistic direction this time: a
|
|
1008
|
+
// window reading 1% divided a full hour of spend by one and priced the
|
|
1009
|
+
// remaining 99 points at thousands of dollars. Below the floor the learned
|
|
1010
|
+
// price takes over through the fallback chain.
|
|
1011
|
+
const measured =
|
|
1012
|
+
percent !== null && percent >= MIN_BASELINE_PERCENT && spent.cost > 0 && !thin;
|
|
885
1013
|
const derived = measured ? spent.cost / percent : null;
|
|
886
1014
|
const known = extra.knownCalibration;
|
|
887
|
-
const
|
|
888
|
-
|
|
1015
|
+
const metered =
|
|
1016
|
+
Boolean(extra.metered) && Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0;
|
|
1017
|
+
const priced = metered
|
|
1018
|
+
? extra.usdPerPercent
|
|
1019
|
+
: derived !== null
|
|
889
1020
|
? derived
|
|
890
1021
|
: Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
891
1022
|
? extra.usdPerPercent
|
|
892
1023
|
: known && Number.isFinite(known.usdPerPercent) && known.usdPerPercent > 0
|
|
893
1024
|
? known.usdPerPercent
|
|
894
1025
|
: null;
|
|
1026
|
+
window.metered = metered;
|
|
1027
|
+
|
|
1028
|
+
// The API reports whole numbers, so a low reading is a wide bracket. A fact
|
|
1029
|
+
// about the reading, not the pricing, so it is set whether or not a price
|
|
1030
|
+
// per point could be found.
|
|
1031
|
+
window.coarse = percent !== null && percent < MIN_BASELINE_PERCENT;
|
|
895
1032
|
|
|
896
1033
|
if (percent !== null && priced !== null) {
|
|
897
1034
|
window.usdPerPercent = priced;
|
|
898
1035
|
window.remainingUSD = window.usdPerPercent * window.percentLeft;
|
|
899
|
-
// The API reports whole numbers, so a low reading is a wide bracket.
|
|
900
|
-
window.coarse = percent < 5;
|
|
901
1036
|
|
|
902
1037
|
const perTurn = typicalTurnCost(recentEvents, inWindow, events, MIN_PACE_SAMPLE);
|
|
903
1038
|
window.percentPerTurn = perTurn === null ? null : perTurn / window.usdPerPercent;
|
|
@@ -989,6 +1124,10 @@ function bindingWindow(windows) {
|
|
|
989
1124
|
const theirs = soonest(best);
|
|
990
1125
|
if (mine !== theirs) return mine < theirs ? w : best;
|
|
991
1126
|
|
|
1127
|
+
// Equally urgent by our own measure: the account says which limit it is
|
|
1128
|
+
// enforcing, and that is worth more than a rule of thumb.
|
|
1129
|
+
if (Boolean(w.isActive) !== Boolean(best.isActive)) return w.isActive ? w : best;
|
|
1130
|
+
|
|
992
1131
|
// Equally urgent: the shorter window is the one hit first in practice, so
|
|
993
1132
|
// the 5-hour limit wins a tie against the weekly one.
|
|
994
1133
|
const myspan = Number.isFinite(w.spanMs) ? w.spanMs : Infinity;
|
|
@@ -1023,6 +1162,14 @@ function formatUSD(value) {
|
|
|
1023
1162
|
return '$' + value.toFixed(3);
|
|
1024
1163
|
}
|
|
1025
1164
|
|
|
1165
|
+
// Money to two places. Three below a dollar is right for a per-turn price and
|
|
1166
|
+
// wrong for a total someone reads after every reply.
|
|
1167
|
+
function formatMoney(value) {
|
|
1168
|
+
if (value === null || !Number.isFinite(value)) return '-';
|
|
1169
|
+
if (value >= 100) return '$' + Math.round(value);
|
|
1170
|
+
return '$' + value.toFixed(2);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1026
1173
|
function formatTokens(value) {
|
|
1027
1174
|
if (!Number.isFinite(value)) return '-';
|
|
1028
1175
|
if (value >= 1e9) return (value / 1e9).toFixed(1) + 'B';
|
|
@@ -1099,6 +1246,49 @@ function otherLimits(utilization, threshold) {
|
|
|
1099
1246
|
return rows.sort((a, b) => b.percentUsed - a.percentUsed);
|
|
1100
1247
|
}
|
|
1101
1248
|
|
|
1249
|
+
// Beside the per-window buckets, the snapshot carries `limits`: one entry per
|
|
1250
|
+
// limit the account enforces, with a severity, whether it is the active one,
|
|
1251
|
+
// and for the per-model weeklies which model it scopes to. It is the account's
|
|
1252
|
+
// own description of its limits, and it can name a window the bucket table
|
|
1253
|
+
// does not: on a Max plan the Fable weekly sat at 17% while the shared weekly
|
|
1254
|
+
// read 11%, and nothing reported the higher of the two.
|
|
1255
|
+
const LIMIT_KINDS = { session: 'five_hour', weekly_all: 'seven_day' };
|
|
1256
|
+
|
|
1257
|
+
function limitWindows(utilization) {
|
|
1258
|
+
const list = utilization && Array.isArray(utilization.limits) ? utilization.limits : [];
|
|
1259
|
+
const rows = [];
|
|
1260
|
+
for (const limit of list) {
|
|
1261
|
+
if (!limit || typeof limit !== 'object' || typeof limit.percent !== 'number') continue;
|
|
1262
|
+
const resetsAt = limit.resets_at ? Date.parse(limit.resets_at) : null;
|
|
1263
|
+
const base = {
|
|
1264
|
+
kind: limit.kind,
|
|
1265
|
+
percent: limit.percent,
|
|
1266
|
+
severity: typeof limit.severity === 'string' ? limit.severity : null,
|
|
1267
|
+
isActive: Boolean(limit.is_active),
|
|
1268
|
+
resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
|
|
1269
|
+
family: null,
|
|
1270
|
+
};
|
|
1271
|
+
if (LIMIT_KINDS[limit.kind]) {
|
|
1272
|
+
rows.push(Object.assign(base, { key: LIMIT_KINDS[limit.kind] }));
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (limit.kind === 'weekly_scoped' && limit.scope && limit.scope.model) {
|
|
1276
|
+
const model = limit.scope.model;
|
|
1277
|
+
const name = model.display_name || model.id || 'model';
|
|
1278
|
+
const family = familyOf(name) || familyOf(model.id) || String(name).toLowerCase();
|
|
1279
|
+
rows.push(
|
|
1280
|
+
Object.assign(base, {
|
|
1281
|
+
key: 'seven_day_scoped:' + family,
|
|
1282
|
+
label: 'weekly (' + name + ')',
|
|
1283
|
+
family,
|
|
1284
|
+
spanMs: 7 * DAY,
|
|
1285
|
+
})
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return rows;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1102
1292
|
function collect(now) {
|
|
1103
1293
|
if (isCodex()) return codex.collect(now);
|
|
1104
1294
|
return collectClaude(now);
|
|
@@ -1147,6 +1337,15 @@ const SATURATION_LIMIT = 105;
|
|
|
1147
1337
|
// Fewer turns than this before the snapshot and a point cannot be priced.
|
|
1148
1338
|
const MIN_BASELINE_TURNS = 5;
|
|
1149
1339
|
|
|
1340
|
+
// A reading below this cannot price a point either. The API reports whole
|
|
1341
|
+
// numbers, so at 1% the denominator is mostly rounding: the true figure is
|
|
1342
|
+
// anywhere in a bracket as wide as the reading itself, and a point priced
|
|
1343
|
+
// against it converts later spend into several times the points it really
|
|
1344
|
+
// moved. A snapshot taken just after a reset is the common case - one sat at
|
|
1345
|
+
// 1% while the local spend divided by it asserted 97% of a window that was
|
|
1346
|
+
// truly at 35.
|
|
1347
|
+
const MIN_BASELINE_PERCENT = 5;
|
|
1348
|
+
|
|
1150
1349
|
// Past this much of a window, a handful of local turns is not what spent it,
|
|
1151
1350
|
// so their total is not a fair price for a point.
|
|
1152
1351
|
const UNEXPLAINED_PERCENT = 20;
|
|
@@ -1162,6 +1361,10 @@ function reconstructWindow(spec, snapshot, events, now) {
|
|
|
1162
1361
|
const past = totals(events.filter((e) => e.at >= pastStart && e.at <= resetsAt));
|
|
1163
1362
|
if (past.cost <= 0) return null;
|
|
1164
1363
|
|
|
1364
|
+
// The same rounding bracket that poisons the live correction poisons a
|
|
1365
|
+
// rebuild: a closed window that read 1% prices a point off almost nothing.
|
|
1366
|
+
if (snapshot.utilization < MIN_BASELINE_PERCENT) return null;
|
|
1367
|
+
|
|
1165
1368
|
const usdPerPercent = past.cost / snapshot.utilization;
|
|
1166
1369
|
|
|
1167
1370
|
// The window running now began when the old one reset, not five hours ago.
|
|
@@ -1236,11 +1439,10 @@ function buildWindows(utilization, events, now, fetchedAt, learned, specs, rejec
|
|
|
1236
1439
|
// of its two windows in the payload, so it hands its own spans in rather than
|
|
1237
1440
|
// having them assumed.
|
|
1238
1441
|
const table = specs && specs.length ? specs : WINDOWS;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// The per-model weekly windows only exist on some plans.
|
|
1242
|
-
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
1442
|
+
const limits = limitWindows(utilization);
|
|
1443
|
+
const limitByKey = new Map(limits.map((limit) => [limit.key, limit]));
|
|
1243
1444
|
|
|
1445
|
+
const one = (spec, snapshot, own, limit) => {
|
|
1244
1446
|
const known = learned ? learned[spec.key] : null;
|
|
1245
1447
|
const refusal = refused.get(spec.key);
|
|
1246
1448
|
|
|
@@ -1261,34 +1463,91 @@ function buildWindows(utilization, events, now, fetchedAt, learned, specs, rejec
|
|
|
1261
1463
|
}
|
|
1262
1464
|
}
|
|
1263
1465
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1466
|
+
// A bucket that quotes its limit in dollars needs no calibration: a
|
|
1467
|
+
// hundred dollars is a hundred points. Null on the plans seen so far, but
|
|
1468
|
+
// the field is there, and when it fills in it beats any estimate.
|
|
1469
|
+
const dollars =
|
|
1470
|
+
snapshot && Number.isFinite(snapshot.limit_dollars) && snapshot.limit_dollars > 0
|
|
1471
|
+
? snapshot.limit_dollars / 100
|
|
1472
|
+
: null;
|
|
1473
|
+
|
|
1474
|
+
const window = buildWindow(
|
|
1475
|
+
spec,
|
|
1476
|
+
anchored,
|
|
1477
|
+
own,
|
|
1478
|
+
now,
|
|
1479
|
+
Object.assign(
|
|
1480
|
+
{ fetchedAt, knownCalibration: known },
|
|
1481
|
+
windowStart === undefined ? {} : { windowStart },
|
|
1482
|
+
dollars === null ? {} : { metered: true, usdPerPercent: dollars }
|
|
1483
|
+
)
|
|
1484
|
+
);
|
|
1269
1485
|
if (refusal) {
|
|
1270
1486
|
window.refusedAt = refusal.at;
|
|
1271
1487
|
window.refusedResetsAt = refusal.resetsAt;
|
|
1272
1488
|
}
|
|
1489
|
+
|
|
1490
|
+
let result = window;
|
|
1273
1491
|
if (!window.stale) {
|
|
1274
|
-
|
|
1492
|
+
result = reconstructUnanchored(spec, window, own, now, known) || window;
|
|
1493
|
+
} else {
|
|
1494
|
+
// Rolled over. Rebuild from local history rather than going blind on it.
|
|
1495
|
+
const rebuilt = reconstructWindow(spec, snapshot, own, now);
|
|
1496
|
+
if (rebuilt) {
|
|
1497
|
+
result = buildWindow(
|
|
1498
|
+
spec,
|
|
1499
|
+
{ utilization: rebuilt.percentUsed, resets_at: null },
|
|
1500
|
+
own,
|
|
1501
|
+
now,
|
|
1502
|
+
{
|
|
1503
|
+
estimated: true,
|
|
1504
|
+
windowStart: rebuilt.windowStart,
|
|
1505
|
+
usdPerPercent: rebuilt.usdPerPercent,
|
|
1506
|
+
}
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1275
1509
|
}
|
|
1510
|
+
// What the account itself says about this limit rides along.
|
|
1511
|
+
if (limit) {
|
|
1512
|
+
result.severity = limit.severity;
|
|
1513
|
+
result.isActive = limit.isActive;
|
|
1514
|
+
}
|
|
1515
|
+
return result;
|
|
1516
|
+
};
|
|
1276
1517
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
windowStart: rebuilt.windowStart,
|
|
1288
|
-
usdPerPercent: rebuilt.usdPerPercent,
|
|
1518
|
+
const windows = table
|
|
1519
|
+
.map((spec) => {
|
|
1520
|
+
let snapshot = utilization[spec.key];
|
|
1521
|
+
const limit = limitByKey.get(spec.key);
|
|
1522
|
+
// The account's own list can carry a window the bucket table does not.
|
|
1523
|
+
if ((!snapshot || typeof snapshot.utilization !== 'number') && limit) {
|
|
1524
|
+
snapshot = {
|
|
1525
|
+
utilization: limit.percent,
|
|
1526
|
+
resets_at: limit.resetsAt ? new Date(limit.resetsAt).toISOString() : null,
|
|
1527
|
+
};
|
|
1289
1528
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1529
|
+
// The per-model weekly windows only exist on some plans.
|
|
1530
|
+
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
1531
|
+
return one(spec, snapshot, events, limit);
|
|
1532
|
+
})
|
|
1533
|
+
.filter(Boolean);
|
|
1534
|
+
|
|
1535
|
+
// A per-model weekly is a limit on one model's spend, so it is priced from
|
|
1536
|
+
// that model's calls alone; the shared windows still see everything.
|
|
1537
|
+
for (const limit of limits) {
|
|
1538
|
+
if (!limit.family) continue;
|
|
1539
|
+
const spec = { key: limit.key, label: limit.label, span: limit.spanMs };
|
|
1540
|
+
const own = events.filter((event) => familyOf(event.model) === limit.family);
|
|
1541
|
+
const snapshot = {
|
|
1542
|
+
utilization: limit.percent,
|
|
1543
|
+
resets_at: limit.resetsAt ? new Date(limit.resetsAt).toISOString() : null,
|
|
1544
|
+
};
|
|
1545
|
+
const window = one(spec, snapshot, own, limit);
|
|
1546
|
+
window.scoped = true;
|
|
1547
|
+
window.family = limit.family;
|
|
1548
|
+
windows.push(window);
|
|
1549
|
+
}
|
|
1550
|
+
return windows;
|
|
1292
1551
|
}
|
|
1293
1552
|
|
|
1294
1553
|
// What one session has spent, out of everything on record.
|
|
@@ -1296,12 +1555,14 @@ function sessionSpend(events, sessionId) {
|
|
|
1296
1555
|
if (!sessionId) return null;
|
|
1297
1556
|
let cost = 0;
|
|
1298
1557
|
let turns = 0;
|
|
1558
|
+
let tokens = 0;
|
|
1299
1559
|
for (const event of events) {
|
|
1300
1560
|
if (event.sessionId !== sessionId) continue;
|
|
1301
1561
|
cost += event.cost;
|
|
1302
|
-
|
|
1562
|
+
tokens += event.tokens || 0;
|
|
1563
|
+
if (!event.sidechain) turns += 1;
|
|
1303
1564
|
}
|
|
1304
|
-
return turns ? { turns, cost } : null;
|
|
1565
|
+
return turns ? { turns, cost, tokens } : null;
|
|
1305
1566
|
}
|
|
1306
1567
|
|
|
1307
1568
|
async function report(now, options) {
|
|
@@ -1413,7 +1674,8 @@ async function report(now, options) {
|
|
|
1413
1674
|
tokens: recent.tokens,
|
|
1414
1675
|
effort: dominantEffort(recentEvents),
|
|
1415
1676
|
},
|
|
1416
|
-
measuredTurns: events.length,
|
|
1677
|
+
measuredTurns: mainThread(events).length,
|
|
1678
|
+
subagentTurns: events.length - mainThread(events).length,
|
|
1417
1679
|
});
|
|
1418
1680
|
}
|
|
1419
1681
|
|
|
@@ -1578,7 +1840,14 @@ function render(data) {
|
|
|
1578
1840
|
(money ? padLeft('Left', 10) : '') + padLeft('Turns left', 12)
|
|
1579
1841
|
);
|
|
1580
1842
|
for (const window of data.windows) {
|
|
1581
|
-
const
|
|
1843
|
+
const bound = data.binding && window.key === data.binding.key;
|
|
1844
|
+
// The account's own severity, when it says critical, is worth a word.
|
|
1845
|
+
const critical = window.severity === 'critical';
|
|
1846
|
+
const marker = bound
|
|
1847
|
+
? ' <- binding' + (critical ? ', critical' : '')
|
|
1848
|
+
: critical
|
|
1849
|
+
? ' critical'
|
|
1850
|
+
: '';
|
|
1582
1851
|
lines.push(
|
|
1583
1852
|
' ' + pad(window.label, 15) +
|
|
1584
1853
|
padLeft(
|
|
@@ -1693,7 +1962,10 @@ function render(data) {
|
|
|
1693
1962
|
} else {
|
|
1694
1963
|
lines.push(' Recent pace no turns in the last hour');
|
|
1695
1964
|
}
|
|
1696
|
-
lines.push(
|
|
1965
|
+
lines.push(
|
|
1966
|
+
' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript' +
|
|
1967
|
+
(data.subagentTurns > 0 ? ' (+' + formatCount(data.subagentTurns) + ' subagent calls)' : '')
|
|
1968
|
+
);
|
|
1697
1969
|
|
|
1698
1970
|
if (data.windows.some((window) => window.adjusted)) {
|
|
1699
1971
|
lines.push(
|
|
@@ -1861,6 +2133,102 @@ function renderForecast(data, turns) {
|
|
|
1861
2133
|
return lines.join('\n');
|
|
1862
2134
|
}
|
|
1863
2135
|
|
|
2136
|
+
// The session history kept by the Stop hook, one row per session.
|
|
2137
|
+
function sessionTokens(session) {
|
|
2138
|
+
const t = (session && session.tokens) || {};
|
|
2139
|
+
return (t.input || 0) + (t.cacheWrite || 0) + (t.cacheRead || 0) + (t.output || 0);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
function shortId(sessionId) {
|
|
2143
|
+
return String(sessionId || '').slice(0, 8);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function renderSessions(list, now) {
|
|
2147
|
+
const lines = [];
|
|
2148
|
+
lines.push('Sessions on this machine, newest first');
|
|
2149
|
+
lines.push('');
|
|
2150
|
+
if (!list || !list.length) {
|
|
2151
|
+
lines.push(' No sessions on record yet. The Stop hook writes one after each reply, so');
|
|
2152
|
+
lines.push(' this fills in as soon as a session with the hook installed has run.');
|
|
2153
|
+
return lines.join('\n');
|
|
2154
|
+
}
|
|
2155
|
+
lines.push(
|
|
2156
|
+
' ' + pad('Id', 10) + pad('When', 12) + pad('Project', 24) + padLeft('Prompts', 7) +
|
|
2157
|
+
padLeft('Turns', 9) + padLeft('Tokens', 9) + padLeft('Cost', 9)
|
|
2158
|
+
);
|
|
2159
|
+
for (const session of list) {
|
|
2160
|
+
const turns =
|
|
2161
|
+
String(session.turns || 0) + (session.subagentTurns > 0 ? '+' + session.subagentTurns : '');
|
|
2162
|
+
lines.push(
|
|
2163
|
+
' ' + pad(shortId(session.sessionId), 10) +
|
|
2164
|
+
pad(Number.isFinite(session.lastAt) ? formatDuration(now - session.lastAt) + ' ago' : '-', 12) +
|
|
2165
|
+
pad(shortenProject(session.project || '-', 22), 24) +
|
|
2166
|
+
padLeft(session.prompts || 0, 7) +
|
|
2167
|
+
padLeft(turns, 9) +
|
|
2168
|
+
padLeft(formatTokens(sessionTokens(session)), 9) +
|
|
2169
|
+
padLeft(formatMoney(session.cost || 0), 9) +
|
|
2170
|
+
(Number.isFinite(session.endedAt) ? '' : ' open')
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
lines.push('');
|
|
2174
|
+
lines.push(' Turns are main-thread calls; +N is what subagents made on top.');
|
|
2175
|
+
return lines.join('\n');
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function renderSession(session, now) {
|
|
2179
|
+
const lines = [];
|
|
2180
|
+
lines.push('Session ' + shortId(session.sessionId) + (session.project ? ' (' + session.project + ')' : ''));
|
|
2181
|
+
lines.push('');
|
|
2182
|
+
const started = Number.isFinite(session.firstAt)
|
|
2183
|
+
? formatClock(session.firstAt) + ', ' + formatDuration(now - session.firstAt) + ' ago, '
|
|
2184
|
+
: '';
|
|
2185
|
+
const ended = Number.isFinite(session.endedAt)
|
|
2186
|
+
? 'closed ' + formatDuration(now - session.endedAt) + ' ago' + (session.reason ? ' (' + session.reason + ')' : '')
|
|
2187
|
+
: 'still open';
|
|
2188
|
+
lines.push(' Started ' + started + ended);
|
|
2189
|
+
lines.push(' Prompts ' + (session.prompts || 0));
|
|
2190
|
+
lines.push(
|
|
2191
|
+
' Turns ' + (session.turns || 0) +
|
|
2192
|
+
(session.subagentTurns > 0 ? ', plus ' + session.subagentTurns + ' by subagents' : '')
|
|
2193
|
+
);
|
|
2194
|
+
const t = session.tokens || {};
|
|
2195
|
+
lines.push(
|
|
2196
|
+
' Tokens ' + formatTokens(sessionTokens(session)) + ': input ' + formatTokens(t.input || 0) +
|
|
2197
|
+
', cache write ' + formatTokens(t.cacheWrite || 0) + ', cache read ' + formatTokens(t.cacheRead || 0) +
|
|
2198
|
+
', output ' + formatTokens(t.output || 0) +
|
|
2199
|
+
(t.reasoning > 0 ? ' (' + formatTokens(t.reasoning) + ' of it reasoning)' : '')
|
|
2200
|
+
);
|
|
2201
|
+
lines.push(' Cost ' + formatMoney(session.cost || 0));
|
|
2202
|
+
if (Number.isFinite(session.context) && session.context > 0) {
|
|
2203
|
+
lines.push(' Context ' + formatTokens(session.context) + ' tokens at the last call');
|
|
2204
|
+
}
|
|
2205
|
+
const models = Object.keys(session.models || {}).sort(
|
|
2206
|
+
(a, b) => (session.models[b].cost || 0) - (session.models[a].cost || 0)
|
|
2207
|
+
);
|
|
2208
|
+
models.forEach((id, index) => {
|
|
2209
|
+
const row = session.models[id];
|
|
2210
|
+
lines.push(
|
|
2211
|
+
(index === 0 ? ' Models ' : ' ') + pad(id, 24) +
|
|
2212
|
+
padLeft((row.turns || 0) + ' turns', 12) + padLeft(formatMoney(row.cost || 0), 10)
|
|
2213
|
+
);
|
|
2214
|
+
});
|
|
2215
|
+
return lines.join('\n');
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// "last", an exact id, or an unambiguous prefix of one.
|
|
2219
|
+
function pickSession(list, which) {
|
|
2220
|
+
const rows = list || [];
|
|
2221
|
+
if (!rows.length) return null;
|
|
2222
|
+
const want = String(which || 'last');
|
|
2223
|
+
if (want === 'last') {
|
|
2224
|
+
return rows.slice().sort((a, b) => (b.lastAt || 0) - (a.lastAt || 0))[0];
|
|
2225
|
+
}
|
|
2226
|
+
const exact = rows.find((row) => row.sessionId === want);
|
|
2227
|
+
if (exact) return exact;
|
|
2228
|
+
const prefixed = rows.filter((row) => String(row.sessionId || '').startsWith(want));
|
|
2229
|
+
return prefixed.length === 1 ? prefixed[0] : null;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1864
2232
|
async function main(argv) {
|
|
1865
2233
|
// Settle the host before anything reads a file, so one run never mixes one
|
|
1866
2234
|
// agent's percentages with the other's turns.
|
|
@@ -1872,6 +2240,29 @@ async function main(argv) {
|
|
|
1872
2240
|
return 0;
|
|
1873
2241
|
}
|
|
1874
2242
|
|
|
2243
|
+
// The session history is a file the Stop hook keeps, so neither of these
|
|
2244
|
+
// opens a transcript. Required lazily: tally.js depends on this module.
|
|
2245
|
+
const sessionAt = argv.indexOf('--session');
|
|
2246
|
+
if (sessionAt !== -1 || argv.indexOf('--sessions') !== -1) {
|
|
2247
|
+
const tally = require('./tally.js');
|
|
2248
|
+
const list = tally.sessions(tally.readState());
|
|
2249
|
+
const now = Date.now();
|
|
2250
|
+
const json = argv.indexOf('--json') !== -1;
|
|
2251
|
+
if (sessionAt !== -1) {
|
|
2252
|
+
const next = argv[sessionAt + 1];
|
|
2253
|
+
const which = next && next.indexOf('--') !== 0 ? next : 'last';
|
|
2254
|
+
const picked = pickSession(list, which);
|
|
2255
|
+
if (!picked) {
|
|
2256
|
+
process.stderr.write('usage: no session matches "' + which + '". Run --sessions to list them.\n');
|
|
2257
|
+
return 2;
|
|
2258
|
+
}
|
|
2259
|
+
process.stdout.write((json ? JSON.stringify(picked, null, 2) : renderSession(picked, now)) + '\n');
|
|
2260
|
+
return 0;
|
|
2261
|
+
}
|
|
2262
|
+
process.stdout.write((json ? JSON.stringify(list, null, 2) : renderSessions(list, now)) + '\n');
|
|
2263
|
+
return 0;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
1875
2266
|
// Codex writes its meter into the session rollouts, so the cached reading is
|
|
1876
2267
|
// only as fresh as the last request it made. Asking Codex itself is a second
|
|
1877
2268
|
// and a child process, which is why it is opt-in rather than the default.
|
|
@@ -1907,6 +2298,23 @@ async function main(argv) {
|
|
|
1907
2298
|
}
|
|
1908
2299
|
return 0;
|
|
1909
2300
|
}
|
|
2301
|
+
const recommendAt = argv.indexOf('--recommend');
|
|
2302
|
+
if (recommendAt !== -1) {
|
|
2303
|
+
// The turn count is optional: with one the verdict is about that job,
|
|
2304
|
+
// without one it is about the headroom in general.
|
|
2305
|
+
const next = argv[recommendAt + 1];
|
|
2306
|
+
const turns = next && next.indexOf('--') !== 0 ? Number(next) : null;
|
|
2307
|
+
const recommend = require('./recommend.js');
|
|
2308
|
+
if (wantsJson) {
|
|
2309
|
+
process.stdout.write(
|
|
2310
|
+
JSON.stringify(recommend.decide(recommend.fromReport(data, turns)), null, 2) + '\n'
|
|
2311
|
+
);
|
|
2312
|
+
} else {
|
|
2313
|
+
process.stdout.write(recommend.renderRecommend(data, turns) + '\n');
|
|
2314
|
+
}
|
|
2315
|
+
return 0;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
1910
2318
|
if (wantsJson) {
|
|
1911
2319
|
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
1912
2320
|
} else {
|
|
@@ -1915,13 +2323,8 @@ async function main(argv) {
|
|
|
1915
2323
|
return 0;
|
|
1916
2324
|
}
|
|
1917
2325
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
process.stderr.write('usage: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
1921
|
-
process.exitCode = 1;
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
|
|
2326
|
+
// Assigned before main() can run: tally.js requires this module back, and a
|
|
2327
|
+
// lazy require from inside main() would otherwise see an empty exports object.
|
|
1925
2328
|
module.exports = {
|
|
1926
2329
|
main,
|
|
1927
2330
|
setHost,
|
|
@@ -1939,12 +2342,16 @@ module.exports = {
|
|
|
1939
2342
|
costOf,
|
|
1940
2343
|
tokensOf,
|
|
1941
2344
|
eventFrom,
|
|
2345
|
+
promptFrom,
|
|
1942
2346
|
readEvents,
|
|
2347
|
+
readCalibration,
|
|
1943
2348
|
buildWindow,
|
|
1944
2349
|
reconstructWindow,
|
|
1945
2350
|
SATURATION_LIMIT,
|
|
1946
2351
|
MIN_BASELINE_TURNS,
|
|
2352
|
+
MIN_BASELINE_PERCENT,
|
|
1947
2353
|
buildWindows,
|
|
2354
|
+
limitWindows,
|
|
1948
2355
|
lastRejections,
|
|
1949
2356
|
bindingWindow,
|
|
1950
2357
|
criticalOthers,
|
|
@@ -1962,7 +2369,11 @@ module.exports = {
|
|
|
1962
2369
|
MIN_PACE_SAMPLE,
|
|
1963
2370
|
formatDuration,
|
|
1964
2371
|
formatUSD,
|
|
2372
|
+
formatMoney,
|
|
1965
2373
|
formatCount,
|
|
2374
|
+
renderSessions,
|
|
2375
|
+
renderSession,
|
|
2376
|
+
pickSession,
|
|
1966
2377
|
verdictLine,
|
|
1967
2378
|
render,
|
|
1968
2379
|
report,
|
|
@@ -1983,3 +2394,10 @@ module.exports = {
|
|
|
1983
2394
|
formatTokens,
|
|
1984
2395
|
PLANS,
|
|
1985
2396
|
};
|
|
2397
|
+
|
|
2398
|
+
if (require.main === module) {
|
|
2399
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
2400
|
+
process.stderr.write('usage: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
2401
|
+
process.exitCode = 1;
|
|
2402
|
+
});
|
|
2403
|
+
}
|