quotacap 0.0.28 → 0.0.30
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/dist/adapters/codex.js +16 -4
- package/dist/advisory/engine.d.ts +3 -1
- package/dist/advisory/engine.js +32 -8
- package/dist/advisory/snapshot.js +1 -0
- package/dist/advisory/types.d.ts +14 -0
- package/dist/advisory/types.js +7 -1
- package/dist/cli/runtime.d.ts +1 -0
- package/dist/cli/runtime.js +8 -1
- package/dist/diagnostics/failure.js +45 -0
- package/dist/format/markdown.js +3 -3
- package/dist/format/rows.d.ts +4 -1
- package/dist/format/rows.js +25 -1
- package/dist/format/terminal.js +10 -3
- package/dist/mcp/server.js +1 -1
- package/dist/store/quotas.js +2 -1
- package/dist/version.js +1 -1
- package/dist/web/src/components/AdviceDrawer.js +2 -1
- package/dist/web/src/components/FaultBanner.js +6 -0
- package/dist/web/src/components/PaceBar.d.ts +53 -3
- package/dist/web/src/components/PaceBar.js +76 -11
- package/dist/web/src/components/PacingLegend.js +3 -2
- package/dist/web/src/components/ProviderCard.js +3 -2
- package/dist/web/src/components/ProviderDrawer.js +16 -4
- package/dist/web/src/components/ProviderRow.js +3 -3
- package/dist/web/src/components/ResetRail.js +1 -1
- package/dist/web/src/components/SubscriptionList.js +1 -1
- package/dist/web/src/state.d.ts +7 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-TJk_KDbS.css → index-BRBtLIOV.css} +1 -1
- package/web/dist/assets/{index-hmABfkqB.js → index-DnWV59MD.js} +2 -2
- package/web/dist/index.html +2 -2
package/dist/adapters/codex.js
CHANGED
|
@@ -32,6 +32,11 @@ function parseWeeklyReset(raw, now) {
|
|
|
32
32
|
return dt.toISOString();
|
|
33
33
|
}
|
|
34
34
|
function parseFiveReset(raw, now) {
|
|
35
|
+
// The 5h reset is bare ("19:38") when it falls today and date-qualified
|
|
36
|
+
// ("02:43 on 15 Sep", weekly shape) otherwise; accept both.
|
|
37
|
+
const qualified = parseWeeklyReset(raw, now);
|
|
38
|
+
if (qualified)
|
|
39
|
+
return qualified;
|
|
35
40
|
const m = raw.trim().match(/^(\d{1,2}):(\d{2})$/);
|
|
36
41
|
if (!m)
|
|
37
42
|
return null;
|
|
@@ -51,8 +56,8 @@ export function parseCodexTui(text, now = new Date()) {
|
|
|
51
56
|
let fiveLeft = null;
|
|
52
57
|
let weeklyRaw = null;
|
|
53
58
|
let fiveRaw = null;
|
|
54
|
-
const weeklyRe = /Weekly limit:\s*(\d+)%\s*left\s*\(resets\s+([^)]+)\)/i;
|
|
55
|
-
const fiveRe = /5h limit:\s*(\d+)%\s*left\s*\(resets\s+([^)]+)\)/i;
|
|
59
|
+
const weeklyRe = /Weekly limit:\s*(?:\[[^\]]*\]\s*)?(\d+)%\s*left\s*\(resets\s+([^)]+)\)/i;
|
|
60
|
+
const fiveRe = /5h limit:\s*(?:\[[^\]]*\]\s*)?(\d+)%\s*left\s*\(resets\s+([^)]+)\)/i;
|
|
56
61
|
const w = cleaned.match(weeklyRe);
|
|
57
62
|
const f = cleaned.match(fiveRe);
|
|
58
63
|
if (w) {
|
|
@@ -127,8 +132,15 @@ export const codexAdapter = {
|
|
|
127
132
|
cols: 140,
|
|
128
133
|
rows: 50,
|
|
129
134
|
settleDelayMs: 2000,
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
// Two-phase submit: the slash-command autocomplete swallows a
|
|
136
|
+
// same-burst Enter, so the command never runs.
|
|
137
|
+
input: "/status",
|
|
138
|
+
submitInput: "\r",
|
|
139
|
+
submitAfterMs: 1500,
|
|
140
|
+
// Complete on the /status panel only. The startup statusline footer
|
|
141
|
+
// ("· 5h N% left · weekly N% left") carries no reset timestamps and
|
|
142
|
+
// would otherwise win the race, forcing a drifting now+7d estimate.
|
|
143
|
+
completionRegex: /Weekly limit:/i,
|
|
132
144
|
abortOn: /Do you trust|Trust.*folder|trust the files in this folder/i,
|
|
133
145
|
timeoutMs: 12000,
|
|
134
146
|
maxBytes: 256 * 1024,
|
|
@@ -5,7 +5,9 @@ import type { Advisory, PaceSource, RecommendationBasis } from "./types.js";
|
|
|
5
5
|
*
|
|
6
6
|
* `usedPct` is a running total since the window opened, and `periodStart` is
|
|
7
7
|
* a required field every adapter sets, so a pace is always computable from
|
|
8
|
-
* one poll. Returns null
|
|
8
|
+
* one poll. Returns null when `periodStart` is missing or unparseable, or
|
|
9
|
+
* when less than MIN_PACE_SPAN_DAYS has elapsed (annualizing a smaller
|
|
10
|
+
* denominator fabricates verdicts).
|
|
9
11
|
*/
|
|
10
12
|
export declare function averagePace(q: Quota, now?: Date): number | null;
|
|
11
13
|
export declare function computeAdvisory(q: Quota, burnRate: number | null, now?: Date, paceSourceOrMeasured?: PaceSource | boolean): Advisory;
|
package/dist/advisory/engine.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { MIN_PACE_SPAN_DAYS } from "./types.js";
|
|
1
2
|
const DAY_MS = 86400000;
|
|
2
3
|
/**
|
|
3
4
|
* Pace from a single reading.
|
|
4
5
|
*
|
|
5
6
|
* `usedPct` is a running total since the window opened, and `periodStart` is
|
|
6
7
|
* a required field every adapter sets, so a pace is always computable from
|
|
7
|
-
* one poll. Returns null
|
|
8
|
+
* one poll. Returns null when `periodStart` is missing or unparseable, or
|
|
9
|
+
* when less than MIN_PACE_SPAN_DAYS has elapsed (annualizing a smaller
|
|
10
|
+
* denominator fabricates verdicts).
|
|
8
11
|
*/
|
|
9
12
|
export function averagePace(q, now = new Date()) {
|
|
10
13
|
if (!q.periodStart)
|
|
@@ -13,8 +16,8 @@ export function averagePace(q, now = new Date()) {
|
|
|
13
16
|
if (Number.isNaN(start))
|
|
14
17
|
return null;
|
|
15
18
|
const elapsedDays = (now.getTime() - start) / DAY_MS;
|
|
16
|
-
if (elapsedDays <
|
|
17
|
-
return null;
|
|
19
|
+
if (elapsedDays < MIN_PACE_SPAN_DAYS)
|
|
20
|
+
return null;
|
|
18
21
|
const pace = q.usedPct / elapsedDays;
|
|
19
22
|
if (!Number.isFinite(pace))
|
|
20
23
|
return null;
|
|
@@ -25,6 +28,10 @@ export function computeAdvisory(q, burnRate, now = new Date(), paceSourceOrMeasu
|
|
|
25
28
|
const daysLeft = Math.max(0.1, (resets.getTime() - now.getTime()) / DAY_MS);
|
|
26
29
|
const remaining = 100 - q.usedPct;
|
|
27
30
|
const idealRate = remaining / daysLeft;
|
|
31
|
+
const avgPace = averagePace(q, now);
|
|
32
|
+
// Nothing left to burn: force the capped verdict even when the recent
|
|
33
|
+
// window is flat (burn 0 at 100% used must not read "on track").
|
|
34
|
+
const exhausted = remaining <= 0;
|
|
28
35
|
let paceSource;
|
|
29
36
|
let burnMeasured;
|
|
30
37
|
if (burnRate === null) {
|
|
@@ -46,12 +53,29 @@ export function computeAdvisory(q, burnRate, now = new Date(), paceSourceOrMeasu
|
|
|
46
53
|
remaining,
|
|
47
54
|
idealRate,
|
|
48
55
|
burnRate: null,
|
|
56
|
+
avgPace,
|
|
49
57
|
burnMeasured: false,
|
|
50
58
|
paceSource: "unknown",
|
|
51
|
-
daysToExhaust: null,
|
|
52
|
-
status: "unknown",
|
|
53
|
-
wastePct: null,
|
|
54
|
-
urgency: "on track",
|
|
59
|
+
daysToExhaust: exhausted ? 0 : null,
|
|
60
|
+
status: exhausted ? "at risk" : "unknown",
|
|
61
|
+
wastePct: exhausted ? 0 : null,
|
|
62
|
+
urgency: exhausted ? "slow down" : "on track",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (exhausted) {
|
|
66
|
+
return {
|
|
67
|
+
provider: q.provider,
|
|
68
|
+
daysLeft,
|
|
69
|
+
remaining,
|
|
70
|
+
idealRate,
|
|
71
|
+
burnRate,
|
|
72
|
+
avgPace,
|
|
73
|
+
burnMeasured,
|
|
74
|
+
paceSource,
|
|
75
|
+
daysToExhaust: 0,
|
|
76
|
+
status: "at risk",
|
|
77
|
+
wastePct: 0,
|
|
78
|
+
urgency: "slow down",
|
|
55
79
|
};
|
|
56
80
|
}
|
|
57
81
|
const wastePct = Math.max(0, remaining - burnRate * daysLeft);
|
|
@@ -67,7 +91,7 @@ export function computeAdvisory(q, burnRate, now = new Date(), paceSourceOrMeasu
|
|
|
67
91
|
urgency = "slow down";
|
|
68
92
|
else if (wastePct > 10)
|
|
69
93
|
urgency = "save";
|
|
70
|
-
return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency };
|
|
94
|
+
return { provider: q.provider, daysLeft, remaining, idealRate, burnRate, avgPace, burnMeasured, paceSource, daysToExhaust, status, wastePct, urgency };
|
|
71
95
|
}
|
|
72
96
|
export function recommend(quotas, _task, burnByProvider = new Map(), now = new Date()) {
|
|
73
97
|
if (!quotas.length) {
|
|
@@ -12,6 +12,7 @@ function normalizeAdvisory(adv) {
|
|
|
12
12
|
remaining: Number.isFinite(adv.remaining) ? adv.remaining : 0,
|
|
13
13
|
idealRate: Number.isFinite(adv.idealRate) ? adv.idealRate : 0,
|
|
14
14
|
burnRate: adv.burnRate !== null && Number.isFinite(adv.burnRate) ? adv.burnRate : null,
|
|
15
|
+
avgPace: adv.avgPace !== null && Number.isFinite(adv.avgPace) ? adv.avgPace : null,
|
|
15
16
|
daysToExhaust: adv.daysToExhaust !== null && Number.isFinite(adv.daysToExhaust) ? adv.daysToExhaust : null,
|
|
16
17
|
wastePct: adv.wastePct !== null && Number.isFinite(adv.wastePct) ? adv.wastePct : null,
|
|
17
18
|
};
|
package/dist/advisory/types.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Quota } from "../adapters/types.js";
|
|
2
2
|
import type { AttemptRecord } from "../store/attempts.js";
|
|
3
|
+
/**
|
|
4
|
+
* Minimum data span before a pace annualizes, in days. Under 6h a single
|
|
5
|
+
* 1% quantum annualizes to 4%/day of noise — enough to fabricate Cap-risk
|
|
6
|
+
* and burn-now verdicts on a fresh window. Applies to the window average
|
|
7
|
+
* (elapsed time) and the recent rate (reading span) alike.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MIN_PACE_SPAN_DAYS: number;
|
|
3
10
|
export type Urgency = "burn now" | "use soon" | "slow down" | "save" | "on track";
|
|
4
11
|
export type BurnStatus = "at risk" | "on track" | "unknown";
|
|
5
12
|
export type PaceSource = "recent" | "window-average" | "unknown";
|
|
@@ -12,6 +19,13 @@ export interface Advisory {
|
|
|
12
19
|
remaining: number;
|
|
13
20
|
idealRate: number;
|
|
14
21
|
burnRate: number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Window-average pace (used % ÷ days elapsed), always computed when the
|
|
24
|
+
* window start is known — independent of `burnRate`, which is the forecast
|
|
25
|
+
* input (recent 24h rolling burn when available, else this same average).
|
|
26
|
+
* Surfaces show both so a flat 24h (0.0) never hides real window usage.
|
|
27
|
+
*/
|
|
28
|
+
avgPace: number | null;
|
|
15
29
|
burnMeasured: boolean;
|
|
16
30
|
paceSource: PaceSource;
|
|
17
31
|
daysToExhaust: number | null;
|
package/dist/advisory/types.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Minimum data span before a pace annualizes, in days. Under 6h a single
|
|
3
|
+
* 1% quantum annualizes to 4%/day of noise — enough to fabricate Cap-risk
|
|
4
|
+
* and burn-now verdicts on a fresh window. Applies to the window average
|
|
5
|
+
* (elapsed time) and the recent rate (reading span) alike.
|
|
6
|
+
*/
|
|
7
|
+
export const MIN_PACE_SPAN_DAYS = 6 / 24;
|
package/dist/cli/runtime.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface RuntimeCommandDeps {
|
|
|
28
28
|
export interface WebLaunchOptions {
|
|
29
29
|
port?: string;
|
|
30
30
|
foreground?: boolean;
|
|
31
|
+
open?: boolean;
|
|
31
32
|
}
|
|
32
33
|
export declare function launchWeb(o: WebLaunchOptions, deps?: RuntimeCommandDeps): Promise<void>;
|
|
33
34
|
export declare function registerRuntimeCommands(program: Command, deps?: RuntimeCommandDeps): void;
|
package/dist/cli/runtime.js
CHANGED
|
@@ -15,7 +15,13 @@ import { checkSkew, execSkewWarning, formatWedged, olderCliWarning, takeoverMana
|
|
|
15
15
|
// overrides skip service management entirely.
|
|
16
16
|
export async function launchWeb(o, deps = {}) {
|
|
17
17
|
const start = deps.startService ?? startService;
|
|
18
|
-
|
|
18
|
+
// Tab-free mode for tests and automation: print the URL instead of
|
|
19
|
+
// opening the user's browser. The env form also covers bare `quotacap`,
|
|
20
|
+
// which takes no flags.
|
|
21
|
+
const noOpen = o.open === false || process.env.QUOTACAP_NO_OPEN === "1";
|
|
22
|
+
const openBrowser = noOpen
|
|
23
|
+
? async (url) => { console.log(url); }
|
|
24
|
+
: deps.openBrowser ?? defaultOpenBrowser;
|
|
19
25
|
const exit = deps.exit ?? process.exit;
|
|
20
26
|
const execService = deps.execService ?? ((args, opts) => runServiceCommand(args, opts ?? {}, {}));
|
|
21
27
|
const createClient = deps.createClient ?? ((c) => createServiceClient(c));
|
|
@@ -203,6 +209,7 @@ export function registerRuntimeCommands(program, deps) {
|
|
|
203
209
|
.command("web")
|
|
204
210
|
.option("--port <n>")
|
|
205
211
|
.option("--foreground", "hold the terminal with a foreground service instead of using the login service")
|
|
212
|
+
.option("--no-open", "print the dashboard URL instead of opening a browser")
|
|
206
213
|
.description("open the dashboard, starting the service if needed")
|
|
207
214
|
.action(async (o) => launchWeb(o, {
|
|
208
215
|
startService: start,
|
|
@@ -171,6 +171,51 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
|
171
171
|
if (/\bparse error\b/i.test(text) || /\bsyntax error\b/i.test(text) || /\bfailed to parse\b/i.test(text)) {
|
|
172
172
|
return { code: "parse_error", phrase: "parse error" };
|
|
173
173
|
}
|
|
174
|
+
// QuotaCap-authored adapter parse vocabulary (`src/adapters/*.ts` throws).
|
|
175
|
+
// Fixed subjects only: a quoted provider value after "bad … reset" is never
|
|
176
|
+
// echoed, per the sanitization policy.
|
|
177
|
+
if (/\bweekly limit not found\b/i.test(text)) {
|
|
178
|
+
return { code: "parse_error", phrase: "weekly limit not found" };
|
|
179
|
+
}
|
|
180
|
+
if (/\b5h limit not found\b/i.test(text)) {
|
|
181
|
+
return { code: "parse_error", phrase: "5h limit not found" };
|
|
182
|
+
}
|
|
183
|
+
if (/\bweekly percent not found\b/i.test(text)) {
|
|
184
|
+
return { code: "parse_error", phrase: "weekly percent not found" };
|
|
185
|
+
}
|
|
186
|
+
if (/\bweekly usage not found\b/i.test(text)) {
|
|
187
|
+
return { code: "parse_error", phrase: "weekly usage not found" };
|
|
188
|
+
}
|
|
189
|
+
if (/\bcurrent-window usage not found\b/i.test(text)) {
|
|
190
|
+
return { code: "parse_error", phrase: "current-window usage not found" };
|
|
191
|
+
}
|
|
192
|
+
if (/\bbad weekly pct\b/i.test(text)) {
|
|
193
|
+
return { code: "parse_error", phrase: "bad weekly pct" };
|
|
194
|
+
}
|
|
195
|
+
if (/\bbad 5h pct\b/i.test(text)) {
|
|
196
|
+
return { code: "parse_error", phrase: "bad 5h pct" };
|
|
197
|
+
}
|
|
198
|
+
if (/\bbad current pct\b/i.test(text)) {
|
|
199
|
+
return { code: "parse_error", phrase: "bad current pct" };
|
|
200
|
+
}
|
|
201
|
+
if (/\bbad weekly reset\b/i.test(text)) {
|
|
202
|
+
return { code: "parse_error", phrase: "bad weekly reset" };
|
|
203
|
+
}
|
|
204
|
+
if (/\bbad 5h reset\b/i.test(text)) {
|
|
205
|
+
return { code: "parse_error", phrase: "bad 5h reset" };
|
|
206
|
+
}
|
|
207
|
+
if (/\bbad weekly reset_time\b/i.test(text)) {
|
|
208
|
+
return { code: "parse_error", phrase: "bad weekly reset_time" };
|
|
209
|
+
}
|
|
210
|
+
if (/\bstatus is not SUCCESS\b/i.test(text)) {
|
|
211
|
+
return { code: "parse_error", phrase: "status is not SUCCESS" };
|
|
212
|
+
}
|
|
213
|
+
if (/\bno usage groups\b/i.test(text)) {
|
|
214
|
+
return { code: "parse_error", phrase: "no usage groups" };
|
|
215
|
+
}
|
|
216
|
+
if (/\bno weekly bucket\b/i.test(text)) {
|
|
217
|
+
return { code: "parse_error", phrase: "no weekly bucket" };
|
|
218
|
+
}
|
|
174
219
|
// Order 9: timeout — Remaining explicit timeout or timed out errors
|
|
175
220
|
if (/\btimed out\b/i.test(text) || /\btimeout\b/i.test(text)) {
|
|
176
221
|
return { code: "timeout", phrase: "timed out" };
|
package/dist/format/markdown.js
CHANGED
|
@@ -10,11 +10,11 @@ export function renderMarkdownTable(snapshot, now) {
|
|
|
10
10
|
const name = p.id === use && use !== "none" ? `★ ${p.displayName}` : p.displayName;
|
|
11
11
|
const used = row.usedPct === null ? "—" : `${row.usedPct}%`;
|
|
12
12
|
const elapsed = row.elapsedPct === null ? "—" : `${row.elapsedPct}%`;
|
|
13
|
-
return `| ${esc(name)} | ${used} | ${elapsed} | ${esc(row.countdown)} | ${esc(row.state)} | ${esc(row.forecast)} |`;
|
|
13
|
+
return `| ${esc(name)} | ${used} | ${elapsed} | ${esc(row.pace)} | ${esc(row.countdown)} | ${esc(row.state)} | ${esc(row.forecast)} |`;
|
|
14
14
|
});
|
|
15
15
|
return [
|
|
16
|
-
"| Provider | Used | Elapsed | Resets | State | Forecast |",
|
|
17
|
-
"
|
|
16
|
+
"| Provider | Used | Elapsed | Pace (%/day avg · 24h) | Resets | State | Forecast |",
|
|
17
|
+
"|---|---|---|---|---|---|---|",
|
|
18
18
|
...lines,
|
|
19
19
|
].join("\n");
|
|
20
20
|
}
|
package/dist/format/rows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderSnapshot } from "../advisory/types.js";
|
|
2
|
-
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Not reporting";
|
|
2
|
+
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Measuring" | "Not reporting";
|
|
3
3
|
export type RailCell = "used" | "tick" | "unused";
|
|
4
4
|
export type SortKey = "recommended" | "reset-asc" | "reset-desc" | "used-desc" | "used-asc";
|
|
5
5
|
export type CompactSuffix = "" | "!" | "~" | "?";
|
|
@@ -11,6 +11,8 @@ export interface Row {
|
|
|
11
11
|
elapsedPct: number | null;
|
|
12
12
|
usedPct: number | null;
|
|
13
13
|
railCells: RailCell[];
|
|
14
|
+
/** Compact pace pair without units, e.g. "10.6 avg · 0.0 24h". Surfaces label %/day once. */
|
|
15
|
+
pace: string;
|
|
14
16
|
}
|
|
15
17
|
export declare const RAIL_CELLS = 20;
|
|
16
18
|
export declare const SORT_KEYS: SortKey[];
|
|
@@ -24,4 +26,5 @@ export declare function forecastText(ps: ProviderSnapshot, now: Date): string;
|
|
|
24
26
|
export declare function compactSuffix(ps: ProviderSnapshot): CompactSuffix;
|
|
25
27
|
export declare function assertValidSortKey(key: string): asserts key is SortKey;
|
|
26
28
|
export declare function sortProviders(providers: ProviderSnapshot[], key: string, use: string): ProviderSnapshot[];
|
|
29
|
+
export declare function paceText(ps: ProviderSnapshot): string;
|
|
27
30
|
export declare function buildRow(ps: ProviderSnapshot, now: Date): Row;
|
package/dist/format/rows.js
CHANGED
|
@@ -70,6 +70,8 @@ export function stateWord(ps) {
|
|
|
70
70
|
return "Not reporting";
|
|
71
71
|
if (adv.status === "at risk")
|
|
72
72
|
return "Cap risk";
|
|
73
|
+
if (adv.status === "unknown")
|
|
74
|
+
return "Measuring";
|
|
73
75
|
switch (adv.urgency) {
|
|
74
76
|
case "burn now":
|
|
75
77
|
case "use soon":
|
|
@@ -116,7 +118,10 @@ export function forecastText(ps, now) {
|
|
|
116
118
|
if (!adv)
|
|
117
119
|
return "no readings yet";
|
|
118
120
|
let text;
|
|
119
|
-
if (adv.
|
|
121
|
+
if (adv.remaining <= 0) {
|
|
122
|
+
text = "Exhausted";
|
|
123
|
+
}
|
|
124
|
+
else if (adv.paceSource === "unknown" || adv.burnRate === null) {
|
|
120
125
|
text = `Measuring pace; ${Math.round(adv.remaining)}% remains with ${adv.daysLeft.toFixed(1)}d until reset`;
|
|
121
126
|
}
|
|
122
127
|
else if (adv.wastePct !== null && adv.wastePct > 0) {
|
|
@@ -230,6 +235,24 @@ export function sortProviders(providers, key, use) {
|
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
}
|
|
238
|
+
// Compact pace pair: window average first, 24h rate alongside. A pair that
|
|
239
|
+
// renders identically at display precision collapses to one figure.
|
|
240
|
+
export function paceText(ps) {
|
|
241
|
+
const adv = ps.advisory;
|
|
242
|
+
if (!adv)
|
|
243
|
+
return "—";
|
|
244
|
+
// avgPace is absent on advisories from a daemon older than the split.
|
|
245
|
+
const avg = adv.avgPace ?? null;
|
|
246
|
+
const recent = adv.paceSource === "recent" ? adv.burnRate : null;
|
|
247
|
+
if (avg !== null && recent !== null && avg.toFixed(1) !== recent.toFixed(1)) {
|
|
248
|
+
return `${avg.toFixed(1)} avg · ${recent.toFixed(1)} 24h`;
|
|
249
|
+
}
|
|
250
|
+
if (avg !== null)
|
|
251
|
+
return `${avg.toFixed(1)} avg`;
|
|
252
|
+
if (recent !== null)
|
|
253
|
+
return `${recent.toFixed(1)} 24h`;
|
|
254
|
+
return "—";
|
|
255
|
+
}
|
|
233
256
|
export function buildRow(ps, now) {
|
|
234
257
|
return {
|
|
235
258
|
id: ps.id,
|
|
@@ -239,5 +262,6 @@ export function buildRow(ps, now) {
|
|
|
239
262
|
elapsedPct: elapsedPct(ps, now),
|
|
240
263
|
usedPct: usedPctOf(ps),
|
|
241
264
|
railCells: railCells(ps, now),
|
|
265
|
+
pace: paceText(ps),
|
|
242
266
|
};
|
|
243
267
|
}
|
package/dist/format/terminal.js
CHANGED
|
@@ -29,6 +29,7 @@ const STATE_COLORS = {
|
|
|
29
29
|
"Behind pace": "\x1b[33m",
|
|
30
30
|
"Ahead of pace": "\x1b[93m",
|
|
31
31
|
"Cap risk": "\x1b[31m",
|
|
32
|
+
"Measuring": "\x1b[90m",
|
|
32
33
|
"Not reporting": "\x1b[90m",
|
|
33
34
|
};
|
|
34
35
|
// provWidth covers the 2-char prefix plus name, measured before ANSI.
|
|
@@ -72,22 +73,25 @@ export function renderWide(snapshot, opts) {
|
|
|
72
73
|
const use = snapshot.recommendation.use;
|
|
73
74
|
const sorted = sortProviders(snapshot.providers, opts.sort ?? "recommended", use);
|
|
74
75
|
const width = provWidthFor(snapshot.providers.map((p) => p.displayName));
|
|
76
|
+
const rows = sorted.map((p) => ({ p, row: buildRow(p, opts.now) }));
|
|
77
|
+
const paceWidth = Math.max("PACE (%/DAY)".length, ...rows.map(({ row }) => row.pace.length));
|
|
75
78
|
const paint = (s, code) => (color ? `${code}${s}${RESET}` : s);
|
|
76
79
|
const header = [
|
|
77
80
|
"PROVIDER".padEnd(width),
|
|
78
81
|
"USED".padStart(6),
|
|
79
82
|
"ELAPSED".padStart(8),
|
|
83
|
+
"PACE (%/DAY)".padEnd(paceWidth),
|
|
80
84
|
"USED VS TIME".padEnd(20),
|
|
81
85
|
"RESETS".padEnd(9),
|
|
82
86
|
"STATE".padEnd(15),
|
|
83
87
|
"FORECAST",
|
|
84
88
|
].join(GAP);
|
|
85
|
-
const lines =
|
|
86
|
-
const row = buildRow(p, opts.now);
|
|
89
|
+
const lines = rows.map(({ p, row }) => {
|
|
87
90
|
const recommended = p.id === use && use !== "none";
|
|
88
91
|
const name = rawProviderField(p.displayName, recommended, width, g);
|
|
89
92
|
const used = (row.usedPct === null ? g.dash : `${row.usedPct}%`).padStart(6);
|
|
90
93
|
const elapsed = (row.elapsedPct === null ? g.dash : `${row.elapsedPct}%`).padStart(8);
|
|
94
|
+
const pace = (opts.ascii ? asciiText(row.pace) : row.pace).padEnd(paceWidth);
|
|
91
95
|
const rail = renderRail(row.railCells, g, row.state, color);
|
|
92
96
|
const countdown = (opts.ascii ? asciiText(row.countdown) : row.countdown).padEnd(9);
|
|
93
97
|
const state = row.state.padEnd(15);
|
|
@@ -96,6 +100,7 @@ export function renderWide(snapshot, opts) {
|
|
|
96
100
|
color ? colorProviderField(name, recommended, row.state) : name,
|
|
97
101
|
used,
|
|
98
102
|
elapsed,
|
|
103
|
+
pace,
|
|
99
104
|
rail,
|
|
100
105
|
countdown,
|
|
101
106
|
paint(state, STATE_COLORS[row.state]),
|
|
@@ -120,8 +125,10 @@ export function renderNarrow(snapshot, opts) {
|
|
|
120
125
|
const rail = renderRail(row.railCells, g, row.state, color);
|
|
121
126
|
const countdown = opts.ascii ? asciiText(row.countdown) : row.countdown;
|
|
122
127
|
const forecast = opts.ascii ? asciiText(row.forecast) : row.forecast;
|
|
128
|
+
const pace = opts.ascii ? asciiText(row.pace) : row.pace;
|
|
129
|
+
const paceBits = row.pace === "—" ? pace : `${pace} %/day`;
|
|
123
130
|
const state = color ? `${STATE_COLORS[row.state]}${row.state}${RESET}` : row.state;
|
|
124
|
-
lines.push(`${color ? colorProviderField(name, recommended, row.state) : name} ${used} used ${g.sep} ${elapsed} elapsed`, `${rail} resets ${countdown} ${g.sep} ${state} ${g.sep} ${forecast}`);
|
|
131
|
+
lines.push(`${color ? colorProviderField(name, recommended, row.state) : name} ${used} used ${g.sep} ${elapsed} elapsed ${g.sep} ${paceBits}`, `${rail} resets ${countdown} ${g.sep} ${state} ${g.sep} ${forecast}`);
|
|
125
132
|
}
|
|
126
133
|
return lines.join("\n");
|
|
127
134
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ClientError, OFFLINE_LABEL, resolveSnapshot, } from "../cli/snapshot-so
|
|
|
10
10
|
export const tools = [
|
|
11
11
|
{ name: "get_quotas", description: "All quotas with resets and health", inputSchema: { type: "object", properties: {}, required: [] } },
|
|
12
12
|
{ name: "get_recommendation", description: "Which provider to use next (same advice for every task)", inputSchema: { type: "object", properties: { task: { type: "string", enum: ["any", "heavy", "light"] } } } },
|
|
13
|
-
{ name: "forecast", description: "Burn vs ideal + waste for a provider", inputSchema: { type: "object", properties: { provider: { type: "string" } }, required: ["provider"] } },
|
|
13
|
+
{ name: "forecast", description: "Burn (24h + window avg) vs ideal + waste for a provider", inputSchema: { type: "object", properties: { provider: { type: "string" } }, required: ["provider"] } },
|
|
14
14
|
];
|
|
15
15
|
// QUOTACAP_URL is used as configured: scheme, authority, effective port and
|
|
16
16
|
// path prefix all survive, so HTTPS and reverse-proxied services still work.
|
package/dist/store/quotas.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MIN_PACE_SPAN_DAYS } from "../advisory/types.js";
|
|
1
2
|
function mapRow(row) {
|
|
2
3
|
if (!row)
|
|
3
4
|
return row;
|
|
@@ -88,7 +89,7 @@ export function getBurnRates(db, now = Date.now()) {
|
|
|
88
89
|
const cutoff = latest.t - 86400000;
|
|
89
90
|
const windowStart = sorted.find((p) => p.t >= cutoff) ?? sorted[0];
|
|
90
91
|
const days = (latest.t - windowStart.t) / 86400000;
|
|
91
|
-
if (sorted.length < 2 || days <
|
|
92
|
+
if (sorted.length < 2 || days < MIN_PACE_SPAN_DAYS)
|
|
92
93
|
continue;
|
|
93
94
|
const burn = (latest.usedPct - windowStart.usedPct) / days;
|
|
94
95
|
if (burn >= 0 && Number.isFinite(burn))
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.30";
|
|
@@ -56,9 +56,10 @@ export function AdviceDrawer({ open, recommendation, providers, asOf, onClose, }
|
|
|
56
56
|
? Math.round(pick.advisory.wastePct)
|
|
57
57
|
: null;
|
|
58
58
|
const left = pick?.quota ? timeLeft(pick.quota.resetsAt, asOfMs) : null;
|
|
59
|
+
const basis = pick?.advisory?.paceSource === "window-average" ? "window-average pace" : "current 24h pace";
|
|
59
60
|
const othersMore = useMore.filter((a) => a.provider !== pickId);
|
|
60
61
|
const titleId = "advice-drawer-title";
|
|
61
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "advice-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "advice-drawer", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, className: "drawer is-open", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "drawer-head", children: [_jsx("span", { className: "picon rec-picon", "aria-hidden": "true", children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }) }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("h2", { id: titleId, children: "Quota Advice & Pacing" }), _jsx("span", { className: "sub", children: "Actionable recommendations based on quota pacing & reset schedules" })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close advice", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Recommended for next session" }), pick && pickName ? (_jsxs("div", { className: "advice-card", style: { marginTop: 8 }, children: [_jsxs("div", { className: "advice-card-main", children: [_jsxs("div", { className: "advice-card-lead", children: [_jsx("span", { className: "sq", style: providerTint(pick.id), children: _jsx(ProviderIcon, { id: pick.id, title: pickName, size: 16 }) }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { className: "name", children: pickName }), _jsxs("span", { className: "recommended-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }), "Recommended"] })] })] }), waste !== null && left && (_jsxs("div", { className: "advice-card-stats", children: [_jsxs("b", { children: [waste, "%"] }), " unused quota expires in ", _jsx("b", { children: left })] }))] }), _jsx("p", { className: "advice-card-body", children: remaining !== null && left && waste !== null ? (_jsxs(_Fragment, { children: [_jsx("b", { children: pickName }), " has ", remaining, "% quota remaining with its reset window closing in ", left, ".
|
|
62
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "advice-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "advice-drawer", role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, className: "drawer is-open", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "drawer-head", children: [_jsx("span", { className: "picon rec-picon", "aria-hidden": "true", children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }) }), _jsxs("div", { style: { flex: 1 }, children: [_jsx("h2", { id: titleId, children: "Quota Advice & Pacing" }), _jsx("span", { className: "sub", children: "Actionable recommendations based on quota pacing & reset schedules" })] }), _jsx("button", { className: "drawer-close", type: "button", "aria-label": "Close advice", onClick: onClose, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Recommended for next session" }), pick && pickName ? (_jsxs("div", { className: "advice-card", style: { marginTop: 8 }, children: [_jsxs("div", { className: "advice-card-main", children: [_jsxs("div", { className: "advice-card-lead", children: [_jsx("span", { className: "sq", style: providerTint(pick.id), children: _jsx(ProviderIcon, { id: pick.id, title: pickName, size: 16 }) }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { className: "name", children: pickName }), _jsxs("span", { className: "recommended-badge", children: [_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z" }) }), "Recommended"] })] })] }), waste !== null && left && (_jsxs("div", { className: "advice-card-stats", children: [_jsxs("b", { children: [waste, "%"] }), " unused quota expires in ", _jsx("b", { children: left })] }))] }), _jsx("p", { className: "advice-card-body", children: remaining !== null && left && waste !== null ? (_jsxs(_Fragment, { children: [_jsx("b", { children: pickName }), " has ", remaining, "% quota remaining with its reset window closing in ", left, ". At the ", basis, ", ", waste, "% unused quota is projected at reset. Using this subscription now avoids unused quota expiration."] })) : (recommendation.reason) })] })) : (_jsx("p", { className: "advice-card-body", children: recommendation.reason }))] }), _jsxs("div", { className: "dsec", children: [_jsx("h3", { children: "Fleet Pacing Breakdown" }), _jsxs("dl", { className: "dkv", children: [_jsx("dt", { children: "Use more" }), _jsx("dd", { children: othersMore.length
|
|
62
63
|
? othersMore.map((a, i) => {
|
|
63
64
|
const p = providers.find((x) => x.id === a.provider);
|
|
64
65
|
const w = a.wastePct !== null ? `${Math.round(a.wastePct)}% unused` : null;
|
|
@@ -22,6 +22,12 @@ export function repairFor(provider) {
|
|
|
22
22
|
case "invalid":
|
|
23
23
|
return { text: `Check the ${name} CLI output, then re-poll.` };
|
|
24
24
|
case "provider-failed": {
|
|
25
|
+
const attempt = provider.lastAttempt;
|
|
26
|
+
if (attempt?.summary) {
|
|
27
|
+
const detail = attempt.errorDetail ? `: ${attempt.errorDetail}` : "";
|
|
28
|
+
const action = attempt.action ?? `Check the ${name} CLI, then re-poll.`;
|
|
29
|
+
return { text: `${attempt.summary}${detail}. ${action}` };
|
|
30
|
+
}
|
|
25
31
|
const failure = failureWords(provider.lastAttempt?.failureCategory ?? null);
|
|
26
32
|
return { text: `${failure ?? "Provider failed"} — check the ${name} CLI, then re-poll.` };
|
|
27
33
|
}
|
|
@@ -1,15 +1,58 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ExclusionReason, ProviderView } from "../state.js";
|
|
3
|
-
export type PaceBadge = "Not reporting" | "Cap risk" | "Behind pace" | "Ahead of pace" | "On track";
|
|
3
|
+
export type PaceBadge = "Not reporting" | "Cap risk" | "Behind pace" | "Ahead of pace" | "On track" | "Measuring";
|
|
4
4
|
/**
|
|
5
5
|
* Pace-state badge from the mapping table. Exclusion always wins; otherwise
|
|
6
6
|
* status "at risk" wins; otherwise urgency decides. No client thresholds.
|
|
7
7
|
*/
|
|
8
8
|
export declare function paceBadge(provider: ProviderView): PaceBadge;
|
|
9
|
+
export interface PaceFigures {
|
|
10
|
+
/** Window average: used % ÷ days elapsed. Null before the window start is known. */
|
|
11
|
+
avg: number | null;
|
|
12
|
+
/** Recent burn: last-24h rolling rate. Null unless paceSource is "recent". */
|
|
13
|
+
recent: number | null;
|
|
14
|
+
}
|
|
15
|
+
/** Split the advisory into its two displayed paces. `burnRate` doubles as the
|
|
16
|
+
* forecast input, so the recent figure only exists when it was measured. */
|
|
17
|
+
export declare function paceFigures(advisory: {
|
|
18
|
+
avgPace?: number | null;
|
|
19
|
+
burnRate: number | null;
|
|
20
|
+
paceSource: string;
|
|
21
|
+
} | null | undefined): PaceFigures;
|
|
22
|
+
export interface PaceCells {
|
|
23
|
+
avg: string;
|
|
24
|
+
recent: string;
|
|
25
|
+
}
|
|
26
|
+
/** Card pace cells: average and 24h rate side by side, each "—" when unknown.
|
|
27
|
+
* Unlike the collapsed summaries, both cells always render so the pair
|
|
28
|
+
* reads as two peer figures. */
|
|
29
|
+
export declare function paceCells(advisory: {
|
|
30
|
+
avgPace?: number | null;
|
|
31
|
+
burnRate: number | null;
|
|
32
|
+
paceSource: string;
|
|
33
|
+
} | null | undefined): PaceCells;
|
|
34
|
+
/** Hover explanations shared by cards and rows. Plain words, no jargon. */
|
|
35
|
+
export declare const PACE_TIPS: {
|
|
36
|
+
readonly avg: "Window average — quota used ÷ days elapsed. Your overall pace this window.";
|
|
37
|
+
readonly recent: "Burn over the last 24 hours. Forecasts use this rate when recent readings exist.";
|
|
38
|
+
readonly ideal: "The even pace that lands exactly on 100% at reset: remaining ÷ days left.";
|
|
39
|
+
};
|
|
40
|
+
export interface PaceLine {
|
|
41
|
+
label: string;
|
|
42
|
+
value: string;
|
|
43
|
+
tip: string;
|
|
44
|
+
}
|
|
45
|
+
/** Table-row pace stack: one labeled line per known pace plus the needed
|
|
46
|
+
* rate. Both paces always show when known (no collapsing) so rows read
|
|
47
|
+
* uniformly. Empty when no advisory is present. */
|
|
48
|
+
export declare function paceLines(advisory: {
|
|
49
|
+
avgPace?: number | null;
|
|
50
|
+
burnRate: number | null;
|
|
51
|
+
paceSource: string;
|
|
52
|
+
idealRate: number;
|
|
53
|
+
} | null | undefined): PaceLine[];
|
|
9
54
|
/** Plain-words reason shown beside "Not reporting". Null when the badge says it all. */
|
|
10
55
|
export declare function exclusionDetail(reason: ExclusionReason): string | null;
|
|
11
|
-
/** Coordinator-locked evidence labels. Unknown tokens pass through verbatim. */
|
|
12
|
-
export declare function evidenceLabels(evidence: string[]): string[];
|
|
13
56
|
export declare function isEstimated(provider: ProviderView): boolean;
|
|
14
57
|
/** Weekday plus 24h clock, e.g. "Thu 21:00". Null when unparseable. */
|
|
15
58
|
export declare function resetClock(resetsAt: string, locale?: string): string | null;
|
|
@@ -20,6 +63,13 @@ export declare function timeLeft(resetsAt: string, asOfMs: number): string | nul
|
|
|
20
63
|
* rows await a fresh window; invalid rows carry a row-level note.
|
|
21
64
|
*/
|
|
22
65
|
export declare function resetCountdown(provider: ProviderView, asOfMs: number): string;
|
|
66
|
+
/**
|
|
67
|
+
* Reset cell for the table view: clock time plus countdown, e.g.
|
|
68
|
+
* "Tue 10:25 · in 20h 30m". Estimated resets mark the timestamp, per the
|
|
69
|
+
* 0.0.25 convention. Falls back to the countdown-only wording for
|
|
70
|
+
* reset-passed, missing, and unparseable rows.
|
|
71
|
+
*/
|
|
72
|
+
export declare function resetCell(provider: ProviderView, asOfMs: number): string;
|
|
23
73
|
export interface BarGeometry {
|
|
24
74
|
usedPct: number;
|
|
25
75
|
elapsedPct: number | null;
|