quotacap 0.0.50 → 0.0.52
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/README.md +2 -2
- package/dist/adapters/agy.js +3 -0
- package/dist/adapters/claude.js +3 -0
- package/dist/adapters/codex.js +4 -2
- package/dist/adapters/kimi.js +14 -0
- package/dist/adapters/manual.js +4 -0
- package/dist/adapters/muse.js +19 -5
- package/dist/adapters/opencode-go.d.ts +12 -2
- package/dist/adapters/opencode-go.js +156 -31
- package/dist/adapters/types.d.ts +2 -0
- package/dist/advisory/snapshot.d.ts +1 -0
- package/dist/advisory/snapshot.js +7 -1
- package/dist/advisory/types.d.ts +3 -0
- package/dist/cli/models.js +5 -4
- package/dist/format/rows.d.ts +30 -1
- package/dist/format/rows.js +109 -24
- package/dist/format/terminal.js +5 -3
- package/dist/http/server.d.ts +3 -0
- package/dist/http/server.js +7 -3
- package/dist/runtime/service.js +14 -3
- package/dist/service/macos.d.ts +5 -0
- package/dist/service/macos.js +6 -2
- package/dist/store/db.js +2 -1
- package/dist/store/quotas.d.ts +1 -1
- package/dist/store/quotas.js +65 -2
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +10 -4
- package/dist/web/src/components/AdviceDrawer.d.ts +2 -1
- package/dist/web/src/components/AdviceDrawer.js +27 -16
- package/dist/web/src/components/BurnDown.d.ts +37 -0
- package/dist/web/src/components/BurnDown.js +182 -0
- package/dist/web/src/components/ClosedWeeks.d.ts +6 -5
- package/dist/web/src/components/ClosedWeeks.js +25 -37
- package/dist/web/src/components/CopyButton.d.ts +4 -0
- package/dist/web/src/components/CopyButton.js +27 -0
- package/dist/web/src/components/ErrorBoundary.d.ts +37 -0
- package/dist/web/src/components/ErrorBoundary.js +32 -0
- package/dist/web/src/components/FaultBanner.d.ts +3 -2
- package/dist/web/src/components/FaultBanner.js +7 -4
- package/dist/web/src/components/OpencodeGoBanner.d.ts +3 -1
- package/dist/web/src/components/OpencodeGoBanner.js +16 -10
- package/dist/web/src/components/PaceBar.d.ts +80 -3
- package/dist/web/src/components/PaceBar.js +225 -34
- package/dist/web/src/components/PacingLegend.js +1 -1
- package/dist/web/src/components/ProviderCard.js +6 -9
- package/dist/web/src/components/ProviderDrawer.d.ts +56 -10
- package/dist/web/src/components/ProviderDrawer.js +263 -138
- package/dist/web/src/components/ProviderRow.js +3 -3
- package/dist/web/src/components/Recommendation.js +11 -7
- package/dist/web/src/components/ResetRail.js +13 -9
- package/dist/web/src/components/SubscriptionList.js +11 -4
- package/dist/web/src/components/WindowsRow.d.ts +13 -3
- package/dist/web/src/components/WindowsRow.js +27 -5
- package/dist/web/src/components/settings/CliIntegrationPanel.js +7 -45
- package/dist/web/src/components/settings/DaemonPanel.js +1 -28
- package/dist/web/src/components/settings/ProvidersPanel.d.ts +2 -2
- package/dist/web/src/components/settings/ProvidersPanel.js +10 -24
- package/dist/web/src/components/settings/SettingsDrawer.d.ts +1 -2
- package/dist/web/src/components/settings/SettingsDrawer.js +2 -2
- package/dist/web/src/state.d.ts +6 -1
- package/dist/web/src/state.js +19 -10
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-Co33DTl5.css +1 -0
- package/web/dist/assets/index-DqK6QVaw.js +13 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BSQW0C_t.css +0 -1
- package/web/dist/assets/index-Dq9ZvTUe.js +0 -13
package/dist/format/rows.js
CHANGED
|
@@ -9,6 +9,85 @@ function msOf(iso) {
|
|
|
9
9
|
const ms = new Date(iso).getTime();
|
|
10
10
|
return Number.isNaN(ms) ? null : ms;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Quota-window resolution, mirroring the dashboard helpers: the advisory
|
|
14
|
+
* binding window rations the account, so standalone resets, usage,
|
|
15
|
+
* elapsed, and pace read it. Weekly-bound rows fall back to the weekly
|
|
16
|
+
* fields unchanged; rate-limit windows never drive these figures.
|
|
17
|
+
*/
|
|
18
|
+
export function isMonthlyBound(ps) {
|
|
19
|
+
return (ps.advisory?.bindingWindow === "monthly" &&
|
|
20
|
+
ps.quota?.monthlyKind === "included" &&
|
|
21
|
+
typeof ps.quota?.monthlyPct === "number" &&
|
|
22
|
+
!!ps.quota?.monthlyResetsAt);
|
|
23
|
+
}
|
|
24
|
+
/** Reset instant of the quota window, or null without a quota reading. */
|
|
25
|
+
export function bindingResetsAt(ps) {
|
|
26
|
+
if (!ps.quota)
|
|
27
|
+
return null;
|
|
28
|
+
if (isMonthlyBound(ps))
|
|
29
|
+
return ps.quota.monthlyResetsAt ?? null;
|
|
30
|
+
return ps.quota.resetsAt ?? null;
|
|
31
|
+
}
|
|
32
|
+
/** Used percent of the quota window. Null without a quota reading. */
|
|
33
|
+
export function bindingUsedPct(ps) {
|
|
34
|
+
if (!ps.quota)
|
|
35
|
+
return null;
|
|
36
|
+
if (isMonthlyBound(ps)) {
|
|
37
|
+
const m = ps.quota.monthlyPct;
|
|
38
|
+
return typeof m === "number" && Number.isFinite(m) ? Math.round(m) : null;
|
|
39
|
+
}
|
|
40
|
+
const u = weeklyPctOf(ps.quota);
|
|
41
|
+
return typeof u === "number" && Number.isFinite(u) ? Math.round(u) : null;
|
|
42
|
+
}
|
|
43
|
+
/** Start of the monthly window: one calendar month before its reported reset. */
|
|
44
|
+
function monthlyStartMs(resetMs) {
|
|
45
|
+
const start = new Date(resetMs);
|
|
46
|
+
start.setMonth(start.getMonth() - 1);
|
|
47
|
+
return start.getTime();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Binding leftover for a raw quota without an advisory (model catalogs):
|
|
51
|
+
* the same scarcer-headroom rule the engine applies, so the catalog
|
|
52
|
+
* leftover and reset name the quota window, not always the week.
|
|
53
|
+
*/
|
|
54
|
+
export function bindingLeftover(quota, now) {
|
|
55
|
+
if (!quota || typeof quota.weeklyPct !== "number") {
|
|
56
|
+
return { leftoverPct: null, resetsAt: quota?.resetsAt ?? null };
|
|
57
|
+
}
|
|
58
|
+
const weeklyRemaining = 100 - quota.weeklyPct;
|
|
59
|
+
const weeklyDays = Math.max(0.1, (new Date(quota.resetsAt ?? "").getTime() - now.getTime()) / DAY_MS);
|
|
60
|
+
if (quota.monthlyKind === "included" &&
|
|
61
|
+
typeof quota.monthlyPct === "number" &&
|
|
62
|
+
quota.monthlyResetsAt) {
|
|
63
|
+
const monthlyRemaining = quota.monthlyStatus === "exhausted" ? 0 : Math.max(0, 100 - quota.monthlyPct);
|
|
64
|
+
const monthlyDays = Math.max(0.1, (new Date(quota.monthlyResetsAt).getTime() - now.getTime()) / DAY_MS);
|
|
65
|
+
if (monthlyRemaining / monthlyDays < weeklyRemaining / weeklyDays || monthlyRemaining === 0) {
|
|
66
|
+
return {
|
|
67
|
+
leftoverPct: Math.round(monthlyRemaining * 10) / 10,
|
|
68
|
+
resetsAt: quota.monthlyResetsAt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
leftoverPct: Math.round(weeklyRemaining * 10) / 10,
|
|
74
|
+
resetsAt: quota.resetsAt ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Window-average pace of the quota window; monthly divides monthly used by elapsed monthly days. */
|
|
78
|
+
export function bindingAvgPace(ps, now) {
|
|
79
|
+
if (!isMonthlyBound(ps))
|
|
80
|
+
return ps.advisory?.avgPace ?? null;
|
|
81
|
+
const used = ps.quota?.monthlyPct;
|
|
82
|
+
const resetMs = msOf(ps.quota?.monthlyResetsAt);
|
|
83
|
+
if (typeof used !== "number" || !Number.isFinite(used) || resetMs === null)
|
|
84
|
+
return null;
|
|
85
|
+
const elapsedDays = (now.getTime() - monthlyStartMs(resetMs)) / DAY_MS;
|
|
86
|
+
if (elapsedDays < 0.25)
|
|
87
|
+
return null;
|
|
88
|
+
const pace = used / elapsedDays;
|
|
89
|
+
return Number.isFinite(pace) ? Math.max(0, pace) : null;
|
|
90
|
+
}
|
|
12
91
|
// The CLI talks HTTP to a daemon that may predate the rename, which sent only
|
|
13
92
|
// `usedPct`. Read both for one release; drop the fallback with the aliases.
|
|
14
93
|
export function weeklyPctOf(q) {
|
|
@@ -33,7 +112,7 @@ export function dayOf(iso) {
|
|
|
33
112
|
return `${when.toLocaleDateString("en-US", { weekday: "short" })} ${when.toLocaleDateString("en-US", { day: "numeric", month: "short" })}`;
|
|
34
113
|
}
|
|
35
114
|
export function countdownText(ps, now) {
|
|
36
|
-
const resetsMs = msOf(ps
|
|
115
|
+
const resetsMs = msOf(bindingResetsAt(ps));
|
|
37
116
|
let text;
|
|
38
117
|
if (resetsMs === null)
|
|
39
118
|
text = "—";
|
|
@@ -41,27 +120,30 @@ export function countdownText(ps, now) {
|
|
|
41
120
|
text = "passed";
|
|
42
121
|
else
|
|
43
122
|
text = fmtDuration(resetsMs - now.getTime());
|
|
44
|
-
if (ps.quota?.resetsAtEstimated)
|
|
123
|
+
if (!isMonthlyBound(ps) && ps.quota?.resetsAtEstimated)
|
|
45
124
|
text += " (est.)";
|
|
46
125
|
return text;
|
|
47
126
|
}
|
|
48
127
|
export function elapsedPct(ps, now) {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
128
|
+
const resetsMs = msOf(bindingResetsAt(ps));
|
|
129
|
+
const startMs = isMonthlyBound(ps)
|
|
130
|
+
? resetsMs === null
|
|
131
|
+
? null
|
|
132
|
+
: monthlyStartMs(resetsMs)
|
|
133
|
+
: msOf(ps.quota?.periodStart);
|
|
51
134
|
if (startMs === null || resetsMs === null || resetsMs <= startMs)
|
|
52
135
|
return null;
|
|
53
136
|
const pct = ((now.getTime() - startMs) / (resetsMs - startMs)) * 100;
|
|
54
137
|
return Math.round(Math.min(100, Math.max(0, pct)));
|
|
55
138
|
}
|
|
56
139
|
export function usedPctOf(ps) {
|
|
57
|
-
|
|
58
|
-
return typeof u === "number" && Number.isFinite(u) ? Math.round(u) : null;
|
|
140
|
+
return bindingUsedPct(ps);
|
|
59
141
|
}
|
|
60
142
|
export function railCells(ps, now) {
|
|
61
143
|
const cells = new Array(RAIL_CELLS).fill("unused");
|
|
62
144
|
if (ps.exclusionReason !== null)
|
|
63
145
|
return cells;
|
|
64
|
-
const u =
|
|
146
|
+
const u = bindingUsedPct(ps);
|
|
65
147
|
const used = typeof u === "number" && Number.isFinite(u)
|
|
66
148
|
? Math.min(RAIL_CELLS, Math.max(0, Math.round((u / 100) * RAIL_CELLS)))
|
|
67
149
|
: 0;
|
|
@@ -167,7 +249,8 @@ export function forecastText(ps, now) {
|
|
|
167
249
|
text = "Exhausts before reset";
|
|
168
250
|
}
|
|
169
251
|
// Engine estimate vocabulary, mirroring the snapshot reason transform.
|
|
170
|
-
|
|
252
|
+
// A monthly quota forecast never carries the weekly estimate flag.
|
|
253
|
+
if (!isMonthlyBound(ps) && ps.quota?.resetsAtEstimated) {
|
|
171
254
|
if (text.includes("waste in"))
|
|
172
255
|
text = text.replace("waste in", "estimated waste in");
|
|
173
256
|
else if (text.includes("until reset"))
|
|
@@ -189,7 +272,7 @@ export function compactSuffix(ps) {
|
|
|
189
272
|
return "!";
|
|
190
273
|
if (adv.status === "watch")
|
|
191
274
|
return "~";
|
|
192
|
-
if (adv.urgency !== "on track" || ps.quota?.resetsAtEstimated)
|
|
275
|
+
if (adv.urgency !== "on track" || (!isMonthlyBound(ps) && ps.quota?.resetsAtEstimated))
|
|
193
276
|
return "~";
|
|
194
277
|
return "";
|
|
195
278
|
}
|
|
@@ -203,8 +286,7 @@ function wasteOf(p) {
|
|
|
203
286
|
return typeof w === "number" && Number.isFinite(w) ? w : null;
|
|
204
287
|
}
|
|
205
288
|
function finiteUsed(p) {
|
|
206
|
-
|
|
207
|
-
return typeof u === "number" && Number.isFinite(u) ? u : null;
|
|
289
|
+
return bindingUsedPct(p);
|
|
208
290
|
}
|
|
209
291
|
export function sortProviders(providers, key, use) {
|
|
210
292
|
assertValidSortKey(key);
|
|
@@ -241,8 +323,8 @@ export function sortProviders(providers, key, use) {
|
|
|
241
323
|
case "reset-desc": {
|
|
242
324
|
const dir = key === "reset-asc" ? 1 : -1;
|
|
243
325
|
indexed.sort((a, b) => {
|
|
244
|
-
const ra = msOf(a.p
|
|
245
|
-
const rb = msOf(b.p
|
|
326
|
+
const ra = msOf(bindingResetsAt(a.p));
|
|
327
|
+
const rb = msOf(bindingResetsAt(b.p));
|
|
246
328
|
if (ra === null && rb === null)
|
|
247
329
|
return a.i - b.i;
|
|
248
330
|
if (ra === null)
|
|
@@ -275,21 +357,24 @@ export function sortProviders(providers, key, use) {
|
|
|
275
357
|
}
|
|
276
358
|
}
|
|
277
359
|
}
|
|
278
|
-
// Compact pace pair: window average first, 24h rate alongside. A pair
|
|
279
|
-
// renders identically at display precision collapses to one figure.
|
|
280
|
-
|
|
360
|
+
// Compact pace pair: quota-window average first, 24h rate alongside. A pair
|
|
361
|
+
// that renders identically at display precision collapses to one figure.
|
|
362
|
+
// The 24h rate is weekly-only: a monthly-bound row shows its monthly
|
|
363
|
+
// average alone.
|
|
364
|
+
export function paceText(ps, now = new Date()) {
|
|
281
365
|
const adv = ps.advisory;
|
|
282
366
|
if (!adv)
|
|
283
367
|
return "—";
|
|
284
|
-
|
|
285
|
-
const avg = adv.avgPace ?? null;
|
|
368
|
+
const avg = bindingAvgPace(ps, now);
|
|
286
369
|
// The 24h figure is the measured recentRate, never the blended forecast
|
|
287
370
|
// input; an older daemon omits the field and burnRate was the raw rate then.
|
|
288
|
-
const recent =
|
|
289
|
-
?
|
|
290
|
-
: adv.
|
|
291
|
-
? adv.
|
|
292
|
-
:
|
|
371
|
+
const recent = isMonthlyBound(ps)
|
|
372
|
+
? null
|
|
373
|
+
: adv.recentRate !== undefined
|
|
374
|
+
? adv.recentRate
|
|
375
|
+
: adv.paceSource === "recent"
|
|
376
|
+
? adv.burnRate
|
|
377
|
+
: null;
|
|
293
378
|
if (avg !== null && recent !== null && avg.toFixed(1) !== recent.toFixed(1)) {
|
|
294
379
|
return `${avg.toFixed(1)} avg · ${recent.toFixed(1)} 24h`;
|
|
295
380
|
}
|
|
@@ -308,6 +393,6 @@ export function buildRow(ps, now) {
|
|
|
308
393
|
elapsedPct: elapsedPct(ps, now),
|
|
309
394
|
usedPct: usedPctOf(ps),
|
|
310
395
|
railCells: railCells(ps, now),
|
|
311
|
-
pace: paceText(ps),
|
|
396
|
+
pace: paceText(ps, now),
|
|
312
397
|
};
|
|
313
398
|
}
|
package/dist/format/terminal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRow, compactSuffix, sortProviders, weeklyPctOf, } from "./rows.js";
|
|
1
|
+
import { buildRow, compactSuffix, isMonthlyBound, sortProviders, weeklyPctOf, } from "./rows.js";
|
|
2
2
|
const GAP = " ";
|
|
3
3
|
const MIN_PROV_WIDTH = 12;
|
|
4
4
|
const MAX_PROV_WIDTH = 22;
|
|
@@ -83,7 +83,8 @@ export function renderWide(snapshot, opts) {
|
|
|
83
83
|
const close = p.lastCloses?.[0];
|
|
84
84
|
if (!close)
|
|
85
85
|
return null;
|
|
86
|
-
const
|
|
86
|
+
const unit = isMonthlyBound(p) ? "month" : "week";
|
|
87
|
+
const tail = `last ${unit} ${close.leftoverPct}% leftover${close.resetsAtEstimated ? " (est.)" : ""}`;
|
|
87
88
|
return `${resetsIndent}${paint(tail, DIM)}`;
|
|
88
89
|
};
|
|
89
90
|
const header = [
|
|
@@ -144,7 +145,8 @@ export function renderNarrow(snapshot, opts) {
|
|
|
144
145
|
const close = p.lastCloses?.[0];
|
|
145
146
|
if (close) {
|
|
146
147
|
const indent = " ".repeat(row.railCells.length + " resets ".length);
|
|
147
|
-
const
|
|
148
|
+
const unit = isMonthlyBound(p) ? "month" : "week";
|
|
149
|
+
const tail = `last ${unit} ${close.leftoverPct}% leftover${close.resetsAtEstimated ? " (est.)" : ""}`;
|
|
148
150
|
lines.push(`${indent}${color ? `${DIM}${tail}${RESET}` : tail}`);
|
|
149
151
|
}
|
|
150
152
|
}
|
package/dist/http/server.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface RuntimeContext {
|
|
|
12
12
|
providerNames?: Record<string, string>;
|
|
13
13
|
/** Detected-but-not-enabled opt-in providers, computed once at start. */
|
|
14
14
|
detectedProviders?: string[];
|
|
15
|
+
/** Login command for the detected OpenCode CLI version, filled in
|
|
16
|
+
* asynchronously after start; null until known. */
|
|
17
|
+
opencodeLoginCommand?: string | null;
|
|
15
18
|
version: string;
|
|
16
19
|
exec: string;
|
|
17
20
|
now?: () => Date;
|
package/dist/http/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { providerIdentity } from "../advisory/provider-names.js";
|
|
|
19
19
|
import { setProviderNameOverride, setProviderEnabled } from "../config.js";
|
|
20
20
|
import { ensureCatalogs, catalogProviders, CATALOG_FAIL_COOLDOWN_MS } from "../catalog/index.js";
|
|
21
21
|
import { getAllLatest } from "../store/quotas.js";
|
|
22
|
+
import { bindingLeftover } from "../format/rows.js";
|
|
22
23
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
23
24
|
export function hostnameFromHostHeader(hostHeader) {
|
|
24
25
|
if (typeof hostHeader !== "string" || !hostHeader.trim())
|
|
@@ -122,6 +123,7 @@ function snapshotOf(ctx) {
|
|
|
122
123
|
checkedAt: cache?.checkedAt ?? null,
|
|
123
124
|
},
|
|
124
125
|
detectedProviders: ctx.detectedProviders ?? [],
|
|
126
|
+
opencodeLoginCommand: ctx.opencodeLoginCommand ?? null,
|
|
125
127
|
},
|
|
126
128
|
});
|
|
127
129
|
}
|
|
@@ -217,17 +219,18 @@ export function buildApp(ctx) {
|
|
|
217
219
|
const catalogTtl = ctx.catalogTtlHours ?? 6;
|
|
218
220
|
function projectModels(catalogs, quotaMap) {
|
|
219
221
|
const out = [];
|
|
222
|
+
const now = ctx.now?.() ?? new Date();
|
|
220
223
|
for (const [id, catalog] of catalogs) {
|
|
221
224
|
const identity = providerIdentity(id, ctx.providerNames);
|
|
222
225
|
const quota = quotaMap.get(id);
|
|
223
|
-
const
|
|
226
|
+
const binding = bindingLeftover(quota, now);
|
|
224
227
|
out.push({
|
|
225
228
|
id,
|
|
226
229
|
displayName: identity.displayName,
|
|
227
230
|
harness: identity.harness,
|
|
228
231
|
vendor: identity.vendor,
|
|
229
|
-
leftoverPct,
|
|
230
|
-
resetsAt:
|
|
232
|
+
leftoverPct: binding.leftoverPct,
|
|
233
|
+
resetsAt: binding.resetsAt,
|
|
231
234
|
catalog,
|
|
232
235
|
});
|
|
233
236
|
}
|
|
@@ -454,6 +457,7 @@ export function buildApp(ctx) {
|
|
|
454
457
|
plan: parsed.plan,
|
|
455
458
|
weeklyPct: parsed.weeklyPct,
|
|
456
459
|
fiveHourPct: parsed.fiveHourPct,
|
|
460
|
+
fiveHourResetsAt: parsed.fiveHourResetsAt,
|
|
457
461
|
resetsAt: parsed.resetsAt,
|
|
458
462
|
periodStart: parsed.periodStart,
|
|
459
463
|
source: "manual",
|
package/dist/runtime/service.js
CHANGED
|
@@ -16,7 +16,7 @@ import { buildApp } from "../http/server.js";
|
|
|
16
16
|
import { detectChannel, refreshUpdateCache } from "./updates.js";
|
|
17
17
|
import { VERSION } from "../version.js";
|
|
18
18
|
import { claudeAdapter } from "../adapters/claude.js";
|
|
19
|
-
import { opencodeGoDetected } from "../adapters/opencode-go.js";
|
|
19
|
+
import { detectOpenCodeLoginCommand, opencodeGoDetected } from "../adapters/opencode-go.js";
|
|
20
20
|
import { classifyFailure } from "../diagnostics/failure.js";
|
|
21
21
|
import { ensureCatalogs, catalogProviders } from "../catalog/index.js";
|
|
22
22
|
export function resolveClaudeExecPath() {
|
|
@@ -196,12 +196,13 @@ export async function startService(opts) {
|
|
|
196
196
|
}).catch(() => { });
|
|
197
197
|
},
|
|
198
198
|
});
|
|
199
|
-
|
|
199
|
+
const appCtx = {
|
|
200
200
|
db,
|
|
201
201
|
token,
|
|
202
202
|
coordinator,
|
|
203
203
|
enabledProviders: config.enabledProviders,
|
|
204
204
|
detectedProviders: opencodeGoDetected() ? ["opencode-go"] : [],
|
|
205
|
+
opencodeLoginCommand: null,
|
|
205
206
|
providerNames: config.providerNames,
|
|
206
207
|
catalogTtlHours: config.catalogTtlHours,
|
|
207
208
|
version: VERSION,
|
|
@@ -213,7 +214,17 @@ export async function startService(opts) {
|
|
|
213
214
|
// hoisted declaration below; it can only fire after listen, so the
|
|
214
215
|
// shutdown bindings it closes over are initialized by then).
|
|
215
216
|
onRestart: () => onSignal(),
|
|
216
|
-
}
|
|
217
|
+
};
|
|
218
|
+
app = buildApp(appCtx);
|
|
219
|
+
// Version-specific OpenCode login command for the fault banner:
|
|
220
|
+
// detected once, in the background, never blocking readiness.
|
|
221
|
+
if (config.enabledProviders.includes("opencode-go") || appCtx.detectedProviders.length > 0) {
|
|
222
|
+
void detectOpenCodeLoginCommand()
|
|
223
|
+
.then((cmd) => {
|
|
224
|
+
appCtx.opencodeLoginCommand = cmd;
|
|
225
|
+
})
|
|
226
|
+
.catch(() => { });
|
|
227
|
+
}
|
|
217
228
|
// 6. Bind; unwind on failure without polling.
|
|
218
229
|
await app.listen({ port, host: "127.0.0.1" });
|
|
219
230
|
const bound = app.server.address();
|
package/dist/service/macos.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface ServiceDeps {
|
|
|
16
16
|
lintPlist?: (plistFile: string) => void;
|
|
17
17
|
waitReady?: (port: number) => Promise<boolean>;
|
|
18
18
|
waitReleased?: (port: number) => Promise<boolean>;
|
|
19
|
+
/** Internal: the caller just stopped this job and is replacing it, so a
|
|
20
|
+
* still-loaded record at bootstrap time is the dying one — keep
|
|
21
|
+
* retrying until it clears instead of treating it as already loaded.
|
|
22
|
+
* Only restart sets this; a plain start keeps the idempotent shortcut. */
|
|
23
|
+
expectReplace?: boolean;
|
|
19
24
|
print?: (msg: string) => void;
|
|
20
25
|
error?: (msg: string) => void;
|
|
21
26
|
now?: () => Date;
|
package/dist/service/macos.js
CHANGED
|
@@ -422,7 +422,7 @@ export async function start(deps = {}) {
|
|
|
422
422
|
const { home, uid, dataDir, print, run, sleep, bootstrapTimeoutMs } = resolved(deps);
|
|
423
423
|
const plistFile = plistFileFor(home);
|
|
424
424
|
await bootstrapWithRetry(run, uid, plistFile, {
|
|
425
|
-
expectReplace: false,
|
|
425
|
+
expectReplace: deps.expectReplace ?? false,
|
|
426
426
|
sleep,
|
|
427
427
|
timeoutMs: bootstrapTimeoutMs,
|
|
428
428
|
});
|
|
@@ -452,7 +452,11 @@ export async function restart(deps = {}) {
|
|
|
452
452
|
if (!(await waitReleased(port))) {
|
|
453
453
|
(deps.print ?? console.log)(`port ${port} is still in use after stopping; starting anyway`);
|
|
454
454
|
}
|
|
455
|
-
|
|
455
|
+
// The record we just booted out can still be clearing when start
|
|
456
|
+
// bootstraps: error 5 plus a loaded record would read as an idempotent
|
|
457
|
+
// double-start and skip the bootstrap entirely, leaving nothing running
|
|
458
|
+
// once the old record finishes clearing. Replace, don't shortcut.
|
|
459
|
+
await start({ ...deps, expectReplace: true });
|
|
456
460
|
}
|
|
457
461
|
function parsePrintOutput(out) {
|
|
458
462
|
const pidMatch = out.match(/^\s*pid\s*=\s*(\d+)/m);
|
package/dist/store/db.js
CHANGED
|
@@ -36,7 +36,7 @@ export function openDb(p) {
|
|
|
36
36
|
return db;
|
|
37
37
|
}
|
|
38
38
|
export function migrate(db) {
|
|
39
|
-
db.exec(`CREATE TABLE IF NOT EXISTS quotas(id INTEGER PRIMARY KEY, provider TEXT, plan TEXT, used_pct REAL, resets_at TEXT, period_start TEXT, source TEXT, fetched_at TEXT, credits_usd REAL, resets_at_estimated INTEGER, session_pct REAL, monthly_pct REAL, monthly_resets_at TEXT, monthly_status TEXT, monthly_kind TEXT);
|
|
39
|
+
db.exec(`CREATE TABLE IF NOT EXISTS quotas(id INTEGER PRIMARY KEY, provider TEXT, plan TEXT, used_pct REAL, resets_at TEXT, period_start TEXT, source TEXT, fetched_at TEXT, credits_usd REAL, resets_at_estimated INTEGER, session_pct REAL, monthly_pct REAL, monthly_resets_at TEXT, monthly_status TEXT, monthly_kind TEXT, five_hour_resets_at TEXT);
|
|
40
40
|
CREATE TABLE IF NOT EXISTS snapshots(day TEXT, provider TEXT, used_pct REAL, burn_rate REAL, ideal_rate REAL, PRIMARY KEY(day, provider));
|
|
41
41
|
CREATE TABLE IF NOT EXISTS adapter_attempts(provider TEXT PRIMARY KEY, attempted_at TEXT NOT NULL, completed_at TEXT, succeeded_at TEXT, success INTEGER NOT NULL, failure_category TEXT, diagnostic_code TEXT, summary TEXT, action TEXT, error_detail TEXT);
|
|
42
42
|
CREATE INDEX IF NOT EXISTS idx_quotas_provider ON quotas(provider);
|
|
@@ -104,6 +104,7 @@ export function migrate(db) {
|
|
|
104
104
|
["monthly_resets_at", "TEXT"],
|
|
105
105
|
["monthly_status", "TEXT"],
|
|
106
106
|
["monthly_kind", "TEXT"],
|
|
107
|
+
["five_hour_resets_at", "TEXT"],
|
|
107
108
|
]) {
|
|
108
109
|
if (!quotaCols.has(column))
|
|
109
110
|
db.exec(`ALTER TABLE quotas ADD COLUMN ${column} ${type}`);
|
package/dist/store/quotas.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function isWeeklyCycleBreak(prev: {
|
|
|
8
8
|
resetsAt: string | null;
|
|
9
9
|
}): boolean;
|
|
10
10
|
export declare function upsertQuota(db: any, q: any): void;
|
|
11
|
-
export declare function getWindowCloses(db: any, provider: string, limit?: number): Array<{
|
|
11
|
+
export declare function getWindowCloses(db: any, provider: string, limit?: number, window?: "weekly" | "monthly"): Array<{
|
|
12
12
|
provider: string;
|
|
13
13
|
plan: string | null;
|
|
14
14
|
usedPct: number;
|
package/dist/store/quotas.js
CHANGED
|
@@ -30,6 +30,10 @@ function mapRow(row) {
|
|
|
30
30
|
delete out.fiveHourPct;
|
|
31
31
|
delete out.sessionPct;
|
|
32
32
|
}
|
|
33
|
+
out.fiveHourResetsAt = row.five_hour_resets_at ?? row.fiveHourResetsAt ?? undefined;
|
|
34
|
+
delete out.five_hour_resets_at;
|
|
35
|
+
if (out.fiveHourResetsAt == null)
|
|
36
|
+
delete out.fiveHourResetsAt;
|
|
33
37
|
// A junk monthly percent means no usable monthly reading. It never
|
|
34
38
|
// invalidates the provider; only a bad weekly percent does (snapshot.ts).
|
|
35
39
|
const m = row.monthly_pct;
|
|
@@ -85,7 +89,7 @@ export function upsertQuota(db, q) {
|
|
|
85
89
|
.run(prev.provider, prev.plan ?? null, prev.weeklyPct, Math.max(0, 100 - prev.weeklyPct), prev.fetchedAt, prev.id, prev.periodStart ?? null, prev.resetsAt ?? null, prev.resetsAtEstimated ? 1 : null, q.fetchedAt, reason);
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
|
-
db.prepare(`INSERT INTO quotas(provider, plan, used_pct, resets_at, period_start, source, fetched_at, credits_usd, resets_at_estimated, session_pct, monthly_pct, monthly_resets_at, monthly_status, monthly_kind) VALUES(
|
|
92
|
+
db.prepare(`INSERT INTO quotas(provider, plan, used_pct, resets_at, period_start, source, fetched_at, credits_usd, resets_at_estimated, session_pct, monthly_pct, monthly_resets_at, monthly_status, monthly_kind, five_hour_resets_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(q.provider, q.plan, weeklyPct, q.resetsAt, q.periodStart, q.source, q.fetchedAt, credits, estimated, fiveHourPct, q.monthlyPct ?? null, q.monthlyResetsAt ?? null, q.monthlyStatus ?? null, q.monthlyKind ?? null, q.fiveHourResetsAt ?? null);
|
|
89
93
|
const day = new Date().toISOString().slice(0, 10);
|
|
90
94
|
db.prepare(`INSERT INTO snapshots(day, provider, used_pct) VALUES(?,?,?) ON CONFLICT(day, provider) DO UPDATE SET used_pct=excluded.used_pct`).run(day, q.provider, weeklyPct);
|
|
91
95
|
db.exec("COMMIT");
|
|
@@ -98,7 +102,66 @@ export function upsertQuota(db, q) {
|
|
|
98
102
|
throw e;
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
|
-
|
|
105
|
+
function monthlyPeriodStart(resetIso) {
|
|
106
|
+
if (!resetIso)
|
|
107
|
+
return null;
|
|
108
|
+
const resetMs = Date.parse(resetIso);
|
|
109
|
+
if (!Number.isFinite(resetMs))
|
|
110
|
+
return null;
|
|
111
|
+
const start = new Date(resetMs);
|
|
112
|
+
start.setMonth(start.getMonth() - 1);
|
|
113
|
+
return start.toISOString();
|
|
114
|
+
}
|
|
115
|
+
export function getWindowCloses(db, provider, limit = 4, window = "weekly") {
|
|
116
|
+
if (window === "monthly") {
|
|
117
|
+
let qRows = [];
|
|
118
|
+
try {
|
|
119
|
+
qRows = db
|
|
120
|
+
.prepare(`SELECT id, provider, plan, monthly_pct, monthly_resets_at, monthly_kind, fetched_at FROM quotas WHERE provider=? ORDER BY fetched_at ASC, id ASC`)
|
|
121
|
+
.all(provider);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (!/no such table|no such column/i.test(String(e?.message ?? "")))
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
const valid = qRows.filter((r) => r.monthly_kind === "included" &&
|
|
128
|
+
typeof r.monthly_pct === "number" &&
|
|
129
|
+
Number.isFinite(r.monthly_pct) &&
|
|
130
|
+
r.monthly_pct >= 0 &&
|
|
131
|
+
r.monthly_pct <= 100 &&
|
|
132
|
+
typeof r.monthly_resets_at === "string" &&
|
|
133
|
+
Number.isFinite(Date.parse(r.monthly_resets_at)) &&
|
|
134
|
+
Number.isFinite(Date.parse(r.fetched_at)));
|
|
135
|
+
const closes = [];
|
|
136
|
+
for (let i = 1; i < valid.length; i++) {
|
|
137
|
+
const prev = valid[i - 1];
|
|
138
|
+
const cur = valid[i];
|
|
139
|
+
const reason = cycleBreakReason({
|
|
140
|
+
usedPct: prev.monthly_pct,
|
|
141
|
+
t: Date.parse(prev.fetched_at),
|
|
142
|
+
resetsAt: prev.monthly_resets_at ?? null,
|
|
143
|
+
}, {
|
|
144
|
+
usedPct: cur.monthly_pct,
|
|
145
|
+
t: Date.parse(cur.fetched_at),
|
|
146
|
+
resetsAt: cur.monthly_resets_at ?? null,
|
|
147
|
+
});
|
|
148
|
+
if (reason) {
|
|
149
|
+
closes.push({
|
|
150
|
+
provider: prev.provider,
|
|
151
|
+
plan: prev.plan ?? null,
|
|
152
|
+
usedPct: prev.monthly_pct,
|
|
153
|
+
leftoverPct: Math.max(0, 100 - prev.monthly_pct),
|
|
154
|
+
sampledAt: prev.fetched_at,
|
|
155
|
+
periodStart: monthlyPeriodStart(prev.monthly_resets_at),
|
|
156
|
+
resetsAt: prev.monthly_resets_at ?? null,
|
|
157
|
+
resetsAtEstimated: false,
|
|
158
|
+
detectedAt: cur.fetched_at,
|
|
159
|
+
reason,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return closes.reverse().slice(0, limit);
|
|
164
|
+
}
|
|
102
165
|
let rows = [];
|
|
103
166
|
try {
|
|
104
167
|
rows = db.prepare(`SELECT provider, plan, used_pct, leftover_pct, sampled_at, period_start, resets_at, resets_at_estimated, detected_at, reason FROM window_closes WHERE provider=? ORDER BY detected_at DESC, id DESC LIMIT ?`).all(provider, limit);
|
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.52";
|
package/dist/web/src/App.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Recommendation } from "./components/Recommendation.js";
|
|
|
8
8
|
import { SubscriptionList } from "./components/SubscriptionList.js";
|
|
9
9
|
import { ResetRail } from "./components/ResetRail.js";
|
|
10
10
|
import { ProviderDrawer } from "./components/ProviderDrawer.js";
|
|
11
|
+
import { DrawerErrorFallback, ErrorBoundary } from "./components/ErrorBoundary.js";
|
|
11
12
|
import { AdviceDrawer } from "./components/AdviceDrawer.js";
|
|
12
13
|
import { SettingsDrawer } from "./components/settings/SettingsDrawer.js";
|
|
13
14
|
import { navigate, routeFor, useRoute } from "./router.js";
|
|
@@ -127,7 +128,7 @@ function App() {
|
|
|
127
128
|
: null;
|
|
128
129
|
const header = (_jsx(Header, { runtime: snapshot?.runtime ?? null, unreachable: error?.kind === "service-unavailable", refreshing: refreshing, onRefresh: () => void refresh(), onSettings: () => setSettingsOpen(true) }));
|
|
129
130
|
if (snapshot && setupMode) {
|
|
130
|
-
return (_jsxs(_Fragment, { children: [header, _jsxs("main", { children: [notice && (_jsx("div", { "data-testid": "refresh-notice", role: "status", children: notice })), showOpenCodeGoBanner(snapshot.runtime, snapshot.providers, goSuppressed) && (_jsx(OpencodeGoBanner, { onEnable: () => setConsentOpen(true) })), _jsx(Onboarding, { snapshot: snapshot, onFirstPoll: () => void refresh(), polling: refreshing, pollError: pollErrorText })] }), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot.providers,
|
|
131
|
+
return (_jsxs(_Fragment, { children: [header, _jsxs("main", { children: [notice && (_jsx("div", { "data-testid": "refresh-notice", role: "status", children: notice })), showOpenCodeGoBanner(snapshot.runtime, snapshot.providers, goSuppressed) && (_jsx(OpencodeGoBanner, { onEnable: () => setConsentOpen(true) })), _jsx(Onboarding, { snapshot: snapshot, onFirstPoll: () => void refresh(), polling: refreshing, pollError: pollErrorText })] }), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot.providers, refreshing: refreshing, onSetProviderEnabled: handleSetProviderEnabled, onClose: () => setSettingsOpen(false) }), consentOpen && (_jsx(ConsentModal, { loginCommand: snapshot?.runtime.opencodeLoginCommand, busy: consentBusy, onCancel: () => setConsentOpen(false), onConfirm: () => void (async () => {
|
|
131
132
|
setConsentBusy(true);
|
|
132
133
|
try {
|
|
133
134
|
await setProviderEnabled("opencode-go", true, true);
|
|
@@ -149,13 +150,15 @@ function App() {
|
|
|
149
150
|
border: i === 0 ? "1px solid var(--rec)" : "1px solid var(--line)",
|
|
150
151
|
borderRadius: 8,
|
|
151
152
|
marginBottom: 8,
|
|
152
|
-
} }, i))) })), error?.kind === "service-unavailable" && !snapshot && (_jsx(UnavailablePanel, { endpoint: daemonEndpoint(), lastGood: snapshot, onRetry: () => void load() })), error?.kind === "http-error" && !snapshot && (_jsx(HttpErrorPanel, { status: error.status, message: error.message, lastGood: snapshot, onRetry: () => void load() })), error && snapshot && (_jsxs("div", { "data-testid": "state-stale-error", role: "alert", children: [error.kind === "http-error" ? `Request failed (HTTP ${error.status}): ` : "Service unavailable: ", error.message, " Showing last readings (", ageLabel(snapshot.asOf), ").", " ", _jsx("button", { type: "button", onClick: () => void load(), children: "Retry" })] })), snapshot && (_jsx(FaultBanner, { providers: snapshot.providers, onRepoll: () => void refresh(), repolling: refreshing, polling: snapshot.runtime.polling })), snapshot && showOpenCodeGoBanner(snapshot.runtime, snapshot.providers, goSuppressed) && (_jsx(OpencodeGoBanner, { onEnable: () => setConsentOpen(true) })), snapshot && (_jsx(ReadyView, { snapshot: snapshot, onSelectProvider: (id) => {
|
|
153
|
+
} }, i))) })), error?.kind === "service-unavailable" && !snapshot && (_jsx(UnavailablePanel, { endpoint: daemonEndpoint(), lastGood: snapshot, onRetry: () => void load() })), error?.kind === "http-error" && !snapshot && (_jsx(HttpErrorPanel, { status: error.status, message: error.message, lastGood: snapshot, onRetry: () => void load() })), error && snapshot && (_jsxs("div", { "data-testid": "state-stale-error", role: "alert", children: [error.kind === "http-error" ? `Request failed (HTTP ${error.status}): ` : "Service unavailable: ", error.message, " Showing last readings (", ageLabel(snapshot.asOf), ").", " ", _jsx("button", { type: "button", onClick: () => void load(), children: "Retry" })] })), snapshot && (_jsx(FaultBanner, { providers: snapshot.providers, onRepoll: () => void refresh(), repolling: refreshing, polling: snapshot.runtime.polling, opencodeLoginCommand: snapshot.runtime.opencodeLoginCommand })), snapshot && showOpenCodeGoBanner(snapshot.runtime, snapshot.providers, goSuppressed) && (_jsx(OpencodeGoBanner, { onEnable: () => setConsentOpen(true) })), snapshot && (_jsx(ReadyView, { snapshot: snapshot, onSelectProvider: (id) => {
|
|
153
154
|
setAdviceOpen(false);
|
|
154
155
|
setSelectedProvider(id);
|
|
155
156
|
}, onViewAdvice: () => {
|
|
156
157
|
setSelectedProvider(null);
|
|
157
158
|
setAdviceOpen(true);
|
|
158
|
-
} }))] }), _jsx(
|
|
159
|
+
} }))] }), _jsx(ErrorBoundary, { resetKey: selectedProvider, fallback: _jsx(DrawerErrorFallback, { name: snapshot?.providers.find((p) => p.id === selectedProvider)?.displayName ??
|
|
160
|
+
selectedProvider ??
|
|
161
|
+
"provider", onClose: () => setSelectedProvider(null) }), children: _jsx(ProviderDrawer, { provider: snapshot?.providers.find((p) => p.id === selectedProvider) ?? null, asOf: snapshot?.asOf ?? "", recommendation: snapshot?.recommendation ?? null, onClose: () => setSelectedProvider(null), onRenamed: () => void load() }) }), consentOpen && (_jsx(ConsentModal, { loginCommand: snapshot?.runtime.opencodeLoginCommand, busy: consentBusy, onCancel: () => setConsentOpen(false), onConfirm: () => void (async () => {
|
|
159
162
|
setConsentBusy(true);
|
|
160
163
|
try {
|
|
161
164
|
await setProviderEnabled("opencode-go", true, true);
|
|
@@ -169,7 +172,10 @@ function App() {
|
|
|
169
172
|
finally {
|
|
170
173
|
setConsentBusy(false);
|
|
171
174
|
}
|
|
172
|
-
})() })), snapshot && (_jsx(AdviceDrawer, { open: adviceOpen, recommendation: snapshot.recommendation, providers: snapshot.providers, asOf: snapshot.asOf, onClose: () => setAdviceOpen(false)
|
|
175
|
+
})() })), snapshot && (_jsx(AdviceDrawer, { open: adviceOpen, recommendation: snapshot.recommendation, providers: snapshot.providers, asOf: snapshot.asOf, onClose: () => setAdviceOpen(false), onSelectProvider: (id) => {
|
|
176
|
+
setAdviceOpen(false);
|
|
177
|
+
setSelectedProvider(id);
|
|
178
|
+
} })), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot?.providers ?? [], refreshing: refreshing, onSetProviderEnabled: handleSetProviderEnabled, onClose: () => setSettingsOpen(false) }), _jsx(SiteFooter, { version: snapshot?.runtime.version })] }));
|
|
173
179
|
}
|
|
174
180
|
const el = typeof document === "undefined" ? null : document.getElementById("app");
|
|
175
181
|
if (el)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { ProviderView, RecommendationView } from "../state.js";
|
|
3
|
-
export declare function AdviceDrawer({ open, recommendation, providers, asOf, onClose, }: {
|
|
3
|
+
export declare function AdviceDrawer({ open, recommendation, providers, asOf, onClose, onSelectProvider, }: {
|
|
4
4
|
open: boolean;
|
|
5
5
|
recommendation: RecommendationView;
|
|
6
6
|
providers: ProviderView[];
|
|
7
7
|
asOf: string;
|
|
8
8
|
onClose: () => void;
|
|
9
|
+
onSelectProvider?: (id: string) => void;
|
|
9
10
|
}): React.JSX.Element | null;
|