quotacap 0.0.52 → 0.0.53
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/muse.d.ts +2 -0
- package/dist/adapters/muse.js +41 -5
- package/dist/diagnostics/failure.js +4 -4
- package/dist/format/rows.js +3 -2
- package/dist/version.js +1 -1
- package/dist/web/src/components/PaceBar.d.ts +2 -1
- package/dist/web/src/components/PaceBar.js +32 -4
- package/dist/web/src/components/ProviderDrawer.js +21 -7
- package/dist/web/src/components/WindowsRow.js +3 -2
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-DHHYR0xZ.js +13 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-DqK6QVaw.js +0 -13
package/dist/adapters/muse.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface RecoveryDeps {
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function recoverSubscription(deps: RecoveryDeps): Promise<ParsedQuota>;
|
|
31
31
|
export declare function museProbeDir(): string;
|
|
32
|
+
export declare function parseMusePlan(cleaned: string): string;
|
|
33
|
+
export declare function parseMuseQuotaExhausted(text: string, plan?: string, now?: Date): ParsedQuota | null;
|
|
32
34
|
export declare function parseMuseTui(text: string, now?: Date, dims?: Partial<GridDims>): ParsedQuota;
|
|
33
35
|
export declare const museAdapter: {
|
|
34
36
|
id: string;
|
package/dist/adapters/muse.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseResetText } from "./parse.js";
|
|
|
5
5
|
import { runPty } from "./pty.js";
|
|
6
6
|
import { parseScreens } from "./vt.js";
|
|
7
7
|
import { adapterSignal, trackedExecFile } from "../runtime/spawn.js";
|
|
8
|
-
import { attachEvidence } from "../diagnostics/failure.js";
|
|
8
|
+
import { attachEvidence, getErrorEvidence } from "../diagnostics/failure.js";
|
|
9
9
|
/** Canonical unavailability error: parseMuseTui throws it, poll() recognises it
|
|
10
10
|
* to start recovery, and failure.ts classifies its text as service_unavailable. */
|
|
11
11
|
export const MUSE_UNAVAILABLE_MESSAGE = "muse: subscription currently unavailable in TUI output";
|
|
@@ -48,6 +48,19 @@ export async function recoverSubscription(deps) {
|
|
|
48
48
|
catch (warmError) {
|
|
49
49
|
if (aborted())
|
|
50
50
|
throw warmError;
|
|
51
|
+
const ev = getErrorEvidence(warmError);
|
|
52
|
+
const text = [
|
|
53
|
+
warmError?.message,
|
|
54
|
+
ev?.stdout,
|
|
55
|
+
ev?.stderr,
|
|
56
|
+
].filter(Boolean).join("\n");
|
|
57
|
+
const unavailEv = getErrorEvidence(unavailable);
|
|
58
|
+
const plan = parseMusePlan(unavailEv?.stdout ?? "");
|
|
59
|
+
const quota = parseMuseQuotaExhausted(text, plan, new Date(now()));
|
|
60
|
+
if (quota) {
|
|
61
|
+
log(`subscription exhausted; parsed 100% quota from warm turn (resets ${quota.resetsAt})`);
|
|
62
|
+
return quota;
|
|
63
|
+
}
|
|
51
64
|
log(`warm attempt ${attempt} failed: ${warmError?.message}`);
|
|
52
65
|
continue;
|
|
53
66
|
}
|
|
@@ -76,6 +89,32 @@ export async function recoverSubscription(deps) {
|
|
|
76
89
|
export function museProbeDir() {
|
|
77
90
|
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "muse-probe");
|
|
78
91
|
}
|
|
92
|
+
export function parseMusePlan(cleaned) {
|
|
93
|
+
const planMatch = cleaned.match(/Subscription\s*·\s*Muse Code\s+(.+?)(?:\s{2,}|\r?\n|$)/i);
|
|
94
|
+
return planMatch ? planMatch[1].trim() : "unknown";
|
|
95
|
+
}
|
|
96
|
+
export function parseMuseQuotaExhausted(text, plan = "unknown", now = new Date()) {
|
|
97
|
+
const match = text.match(/Subscription quota exhausted\.?\s+Your usage window resets at\s+(\S+?)(?:\.|\s|$)/i);
|
|
98
|
+
if (!match)
|
|
99
|
+
return null;
|
|
100
|
+
const resetIso = match[1];
|
|
101
|
+
const resetMs = Date.parse(resetIso);
|
|
102
|
+
if (!Number.isFinite(resetMs) || resetMs <= now.getTime())
|
|
103
|
+
return null;
|
|
104
|
+
const resetsAt = new Date(resetMs).toISOString();
|
|
105
|
+
const periodStart = new Date(resetMs - 7 * 86400000).toISOString();
|
|
106
|
+
return {
|
|
107
|
+
provider: "muse",
|
|
108
|
+
plan,
|
|
109
|
+
weeklyPct: 100,
|
|
110
|
+
fiveHourPct: 0,
|
|
111
|
+
resetsAt,
|
|
112
|
+
periodStart,
|
|
113
|
+
source: "tui",
|
|
114
|
+
fetchedAt: now.toISOString(),
|
|
115
|
+
raw: text.slice(0, 4096),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
79
118
|
export function parseMuseTui(text, now = new Date(), dims) {
|
|
80
119
|
return parseScreens(text, dims, (cleaned) => parseMuseCleaned(cleaned, now));
|
|
81
120
|
}
|
|
@@ -101,10 +140,7 @@ function parseMuseCleaned(cleaned, now) {
|
|
|
101
140
|
// First match wins; overdrawn transcripts carry two identical copies.
|
|
102
141
|
// Vendor casing preserved (grok precedent: SuperGrok); the Muse Code
|
|
103
142
|
// product prefix is stripped because the product is already the row label.
|
|
104
|
-
|
|
105
|
-
const planMatch = cleaned.match(/Subscription\s*·\s*Muse Code\s+(.+?)\s{2,}/i);
|
|
106
|
-
if (planMatch)
|
|
107
|
-
plan = planMatch[1].trim();
|
|
143
|
+
const plan = parseMusePlan(cleaned);
|
|
108
144
|
// The Current-window reset ("Resets at 3:51 PM") is a bare clock time
|
|
109
145
|
// with no date: resolve it against the panel's Brisbane date, rolling to
|
|
110
146
|
// tomorrow once today's time has passed. Display-only; a missing or
|
|
@@ -157,18 +157,18 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
|
157
157
|
if (/\btrust this folder\b/i.test(text)) {
|
|
158
158
|
return { code: "trust_prompt", phrase: "trust prompt" };
|
|
159
159
|
}
|
|
160
|
-
// Order 5: rate_limit — rate limit, rate_limit, too many requests, quota exceeded, or explicit HTTP status 429
|
|
160
|
+
// Order 5: rate_limit — rate limit, rate_limit, rate_limit_error, too many requests, quota exceeded/exhausted, or explicit HTTP/API status 429
|
|
161
161
|
// A bare number 429 is insufficient
|
|
162
|
-
if (/\brate[ _]limit
|
|
162
|
+
if (/\brate[ _]limit(?:_error)?\b/i.test(text)) {
|
|
163
163
|
return { code: "rate_limit", phrase: "rate limit" };
|
|
164
164
|
}
|
|
165
165
|
if (/\btoo many requests\b/i.test(text)) {
|
|
166
166
|
return { code: "rate_limit", phrase: "too many requests" };
|
|
167
167
|
}
|
|
168
|
-
if (/\bquota exceeded\b/i.test(text)) {
|
|
168
|
+
if (/\bquota (?:exceeded|exhausted)\b/i.test(text)) {
|
|
169
169
|
return { code: "rate_limit", phrase: "quota exceeded" };
|
|
170
170
|
}
|
|
171
|
-
if (/\b(?:status|code|http)\s*:?\s*429\b/i.test(text) || /\b429\s+too many requests\b/i.test(text)) {
|
|
171
|
+
if (/\b(?:status|code|http|api error)\s*:?\s*429\b/i.test(text) || /\b429\s+too many requests\b/i.test(text)) {
|
|
172
172
|
return { code: "rate_limit", phrase: "HTTP 429" };
|
|
173
173
|
}
|
|
174
174
|
// Order 6: auth — login required, not logged in, logged out, unauthorized, unauthorised, forbidden,
|
package/dist/format/rows.js
CHANGED
|
@@ -141,9 +141,10 @@ export function usedPctOf(ps) {
|
|
|
141
141
|
}
|
|
142
142
|
export function railCells(ps, now) {
|
|
143
143
|
const cells = new Array(RAIL_CELLS).fill("unused");
|
|
144
|
-
if (ps.exclusionReason !== null)
|
|
145
|
-
return cells;
|
|
146
144
|
const u = bindingUsedPct(ps);
|
|
145
|
+
const is100Exhausted = typeof u === "number" && u >= 100 && !ps.resetPassed;
|
|
146
|
+
if (ps.exclusionReason !== null && !is100Exhausted)
|
|
147
|
+
return cells;
|
|
147
148
|
const used = typeof u === "number" && Number.isFinite(u)
|
|
148
149
|
? Math.min(RAIL_CELLS, Math.max(0, Math.round((u / 100) * RAIL_CELLS)))
|
|
149
150
|
: 0;
|
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.53";
|
|
@@ -190,12 +190,13 @@ export interface PaceHeadline {
|
|
|
190
190
|
/** True when the projected-unused hatch belongs on the bar. */
|
|
191
191
|
hatch: boolean;
|
|
192
192
|
}
|
|
193
|
+
export declare function isQuotaExhaustedBeforeReset(provider: ProviderView, asOfMs?: number): boolean;
|
|
193
194
|
/**
|
|
194
195
|
* Headline forecast shared by the card bar and the drawer quota row:
|
|
195
196
|
* "expires unused" wording when a waste hatch applies, otherwise the
|
|
196
197
|
* one-line forecast. Null when there is nothing to forecast.
|
|
197
198
|
*/
|
|
198
|
-
export declare function paceHeadline(provider: ProviderView): PaceHeadline | null;
|
|
199
|
+
export declare function paceHeadline(provider: ProviderView, asOfMs?: number): PaceHeadline | null;
|
|
199
200
|
/**
|
|
200
201
|
* Two-tier pacing rail: used fill, elapsed tick, projected-unused hatch.
|
|
201
202
|
* Unknown pace shows fill and tick but no hatch and no numeric rate.
|
|
@@ -503,16 +503,41 @@ export function Badge({ provider, compact = false, }) {
|
|
|
503
503
|
: badgeTitle(badge, quotaResetEstimated(provider));
|
|
504
504
|
return (_jsxs("span", { "data-testid": "pace-badge", className: `pace ${badgeClass}`, title: title, children: [badge, !compact && detail ? ` · ${detail}` : ""] }));
|
|
505
505
|
}
|
|
506
|
+
export function isQuotaExhaustedBeforeReset(provider, asOfMs) {
|
|
507
|
+
if (provider.resetPassed)
|
|
508
|
+
return false;
|
|
509
|
+
const resetIso = quotaResetsAt(provider);
|
|
510
|
+
if (!resetIso)
|
|
511
|
+
return false;
|
|
512
|
+
const resetMs = Date.parse(resetIso);
|
|
513
|
+
if (Number.isFinite(resetMs) && asOfMs !== undefined && resetMs <= asOfMs)
|
|
514
|
+
return false;
|
|
515
|
+
const used = quotaUsedPct(provider);
|
|
516
|
+
if (used !== null && used >= 100)
|
|
517
|
+
return true;
|
|
518
|
+
if (isMonthlyBound(provider) && provider.quota?.monthlyStatus === "exhausted")
|
|
519
|
+
return true;
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
506
522
|
/**
|
|
507
523
|
* Headline forecast shared by the card bar and the drawer quota row:
|
|
508
524
|
* "expires unused" wording when a waste hatch applies, otherwise the
|
|
509
525
|
* one-line forecast. Null when there is nothing to forecast.
|
|
510
526
|
*/
|
|
511
|
-
export function paceHeadline(provider) {
|
|
527
|
+
export function paceHeadline(provider, asOfMs) {
|
|
512
528
|
const badge = paceBadge(provider);
|
|
513
529
|
const monthlyBinds = provider.advisory?.bindingWindow === "monthly";
|
|
514
530
|
const wastePct = provider.advisory?.wastePct;
|
|
515
531
|
const hatch = !monthlyBinds && wastePct !== null && wastePct !== undefined && Number.isFinite(wastePct) && wastePct >= 3;
|
|
532
|
+
const is100Exhausted = isQuotaExhaustedBeforeReset(provider, asOfMs);
|
|
533
|
+
if (provider.exclusionReason !== null && is100Exhausted) {
|
|
534
|
+
const line = forecastLine(provider);
|
|
535
|
+
return {
|
|
536
|
+
text: line ? `Exhausted · ${line}` : "Exhausted · quota fully used",
|
|
537
|
+
kind: "cap",
|
|
538
|
+
hatch: false,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
516
541
|
const text = hatch && badge === "Behind pace" ? `${Math.round(wastePct)}% expires unused` : forecastLine(provider);
|
|
517
542
|
if (!text)
|
|
518
543
|
return null;
|
|
@@ -532,17 +557,20 @@ export function PaceBar({ provider, asOf, showHeadline = true, }) {
|
|
|
532
557
|
const asOfMs = Date.parse(asOf);
|
|
533
558
|
const { usedPct, elapsedPct } = barGeometry(provider, asOfMs);
|
|
534
559
|
const wastePct = provider.advisory?.wastePct;
|
|
535
|
-
const headline = paceHeadline(provider);
|
|
560
|
+
const headline = paceHeadline(provider, asOfMs);
|
|
536
561
|
// A weekly-sized hatch under a month-bound card restates issue 109.
|
|
537
562
|
const showHatch = headline?.hatch ?? false;
|
|
538
563
|
const quotaUsed = quotaUsedPct(provider);
|
|
539
564
|
const label = provider.quota
|
|
540
565
|
? `${provider.id}: ${quotaUsed ?? provider.quota.weeklyPct}% used, ${badge}`
|
|
541
566
|
: `${provider.id}: no readings, ${badge}`;
|
|
567
|
+
const is100Exhausted = isQuotaExhaustedBeforeReset(provider, asOfMs);
|
|
568
|
+
const fillBg = is100Exhausted ? "var(--fill-cap)" : fillToken(badge);
|
|
569
|
+
const edgeBg = is100Exhausted ? "var(--edge-cap)" : edgeToken(badge);
|
|
542
570
|
return (_jsxs("div", { "data-testid": "pace-bar", role: "img", "aria-label": label, className: "trackwrap", children: [showHeadline && provider.quota && (_jsxs("div", { className: "tline", children: [_jsxs("span", { className: "used", children: [quotaUsed ?? provider.quota.weeklyPct, "% used"] }), headline && headline.kind !== "none" ? (_jsxs("span", { className: `out out-${headline.kind}`, children: ["\u2192 ", headline.text] })) : headline ? (_jsxs("span", { className: "out out-none", children: ["\u2192 ", headline.text] })) : null] })), _jsxs("div", { className: "track", children: [_jsx("div", { "aria-hidden": "true", className: "fill", style: {
|
|
543
571
|
width: `${usedPct}%`,
|
|
544
|
-
background:
|
|
545
|
-
["--fill-edge"]:
|
|
572
|
+
background: fillBg,
|
|
573
|
+
["--fill-edge"]: edgeBg,
|
|
546
574
|
} }), showHatch && (_jsx("div", { "aria-hidden": "true", "data-testid": "pace-hatch", className: "band band-waste", style: {
|
|
547
575
|
left: `${100 - wastePct}%`,
|
|
548
576
|
width: `${wastePct}%`,
|
|
@@ -67,7 +67,7 @@ export function drawerModel(provider) {
|
|
|
67
67
|
lastSuccessAt: provider.lastSuccessAt,
|
|
68
68
|
ageMs: provider.ageMs,
|
|
69
69
|
failureCategory: provider.lastAttempt?.failureCategory ?? null,
|
|
70
|
-
failureWords: failureWords(provider.lastAttempt?.failureCategory ?? null),
|
|
70
|
+
failureWords: provider.lastAttempt?.summary ?? failureWords(provider.lastAttempt?.failureCategory ?? null),
|
|
71
71
|
evidence: [...provider.evidence],
|
|
72
72
|
exclusionReason: provider.exclusionReason,
|
|
73
73
|
exclusionWords: exclusionWords(provider.exclusionReason),
|
|
@@ -484,21 +484,35 @@ export function ProviderDrawer({ provider, asOf, recommendation, onClose, onRena
|
|
|
484
484
|
const limitForecast = compactLimitForecast(provider);
|
|
485
485
|
const wastePct = provider.advisory?.wastePct ?? null;
|
|
486
486
|
const weeklyHatchPct = !monthlyPrimary && headline?.hatch ? wastePct : null;
|
|
487
|
+
const isWeekly100 = provider.quota &&
|
|
488
|
+
!provider.resetPassed &&
|
|
489
|
+
provider.quota.weeklyPct >= 100 &&
|
|
490
|
+
(!provider.quota.resetsAt || Date.parse(provider.quota.resetsAt) > asOfMs);
|
|
487
491
|
const weeklyRow = provider.quota ? (_jsx(LimitRow, { name: "Weekly limit", pct: provider.quota.weeklyPct, quota: !monthlyPrimary, resetLine: weeklyResetLine, forecast: monthlyPrimary ? null : limitForecast, hatchPct: weeklyHatchPct, fillStyle: monthlyPrimary
|
|
488
492
|
? limitFill(provider.quota.weeklyPct)
|
|
489
|
-
:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
493
|
+
: isWeekly100
|
|
494
|
+
? {
|
|
495
|
+
width: `${Math.min(100, Math.max(0, provider.quota.weeklyPct))}%`,
|
|
496
|
+
background: "var(--fill-cap)",
|
|
497
|
+
["--fill-edge"]: "var(--edge-cap)",
|
|
498
|
+
}
|
|
499
|
+
: {
|
|
500
|
+
width: `${Math.min(100, Math.max(0, provider.quota.weeklyPct))}%`,
|
|
501
|
+
background: fillToken(model.badge),
|
|
502
|
+
["--fill-edge"]: edgeToken(model.badge),
|
|
503
|
+
}, elapsedPct: weeklyElapsed })) : null;
|
|
494
504
|
const monthlyRow = model.monthlyKind === "included" &&
|
|
495
505
|
model.monthlyPct !== undefined &&
|
|
496
506
|
Number.isFinite(model.monthlyPct) ? (_jsx(LimitRow, { name: "Monthly limit", pct: model.monthlyPct, quota: monthlyPrimary, resetLine: monthlyResetLine, forecast: monthlyPrimary ? limitForecast : null, fillStyle: monthlyPrimary
|
|
497
507
|
? trackFillForPct(model.monthlyPct, model.monthlyStatus === "exhausted")
|
|
498
508
|
: limitFill(model.monthlyPct, model.monthlyStatus === "exhausted"), elapsedPct: monthlyElapsed, testId: "drawer-monthly" })) : null;
|
|
509
|
+
const fiveResetPassed = model.fiveHourResetsAt !== undefined &&
|
|
510
|
+
Number.isFinite(Date.parse(model.fiveHourResetsAt)) &&
|
|
511
|
+
Date.parse(model.fiveHourResetsAt) <= asOfMs;
|
|
512
|
+
const effectiveFivePct = fiveResetPassed ? 0 : model.fiveHourPct;
|
|
499
513
|
const fiveRow = model.fiveHourPct !== undefined &&
|
|
500
514
|
model.fiveHourPct !== null &&
|
|
501
|
-
Number.isFinite(model.fiveHourPct) ? (_jsx(LimitRow, { name: shortWindowLabel(provider.id) ?? "5h limit", pct:
|
|
515
|
+
Number.isFinite(model.fiveHourPct) ? (_jsx(LimitRow, { name: shortWindowLabel(provider.id) ?? "5h limit", pct: effectiveFivePct ?? 0, resetLine: fiveResetLineNode, fillStyle: limitFill(effectiveFivePct ?? 0), elapsedPct: fiveResetPassed ? null : fiveElapsed })) : null;
|
|
502
516
|
const anyElapsed = [weeklyElapsed, monthlyElapsed, fiveElapsed].some((v) => v !== null);
|
|
503
517
|
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "drawer-scrim", className: "scrim is-open", "aria-hidden": "true", onClick: onClose }), _jsxs("aside", { ref: dialogRef, tabIndex: -1, "data-testid": "provider-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", style: providerTint(provider.id), children: _jsx(ProviderIcon, { id: provider.id, title: model.name }) }), _jsxs("div", { className: "who", style: { flex: 1, minWidth: 0 }, children: [_jsxs("h2", { id: titleId, style: isEditing
|
|
504
518
|
? {
|
|
@@ -43,9 +43,10 @@ export function windowUnits(provider, asOfMs) {
|
|
|
43
43
|
reset: left ? `in ${left}` : null });
|
|
44
44
|
}
|
|
45
45
|
if (typeof q.fiveHourPct === "number" && Number.isFinite(q.fiveHourPct)) {
|
|
46
|
-
|
|
46
|
+
const passed = q.fiveHourResetsAt ? Date.parse(q.fiveHourResetsAt) <= asOfMs : false;
|
|
47
|
+
const effectivePct = passed ? 0 : q.fiveHourPct;
|
|
47
48
|
units.push({ win: "five", tag: "5h", cardLabel: "5h limit", rowLabel: "5h",
|
|
48
|
-
pct:
|
|
49
|
+
pct: effectivePct, heat: heatOf(effectivePct, false), time: null, reset: null });
|
|
49
50
|
}
|
|
50
51
|
if (!monthlyBound && q.monthlyKind === "included" && typeof q.monthlyPct === "number" && q.monthlyResetsAt) {
|
|
51
52
|
const exhausted = q.monthlyStatus === "exhausted";
|