@kunkun.sh/kuota 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +130 -2
- package/bin/kuota.mjs +3 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +1303 -0
- package/dist/dist-CIjerpSd.mjs +576 -0
- package/dist/history-G4o4HVvW.d.mts +36 -0
- package/dist/history-oOXsjdWR.mjs +236 -0
- package/dist/history.d.mts +3 -0
- package/dist/history.mjs +3 -0
- package/dist/index-2k6FkhpT.d.mts +81 -0
- package/dist/index-NHzh2TGn.d.mts +168 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/main-B-hZbKK1.mjs +340 -0
- package/dist/metrics-Bzmrfgn-.mjs +307 -0
- package/dist/metrics-D7oNul_9.d.mts +30 -0
- package/dist/metrics.d.mts +3 -0
- package/dist/metrics.mjs +3 -0
- package/dist/parsers.d.mts +13 -0
- package/dist/parsers.mjs +19 -0
- package/dist/providers.d.mts +2 -0
- package/dist/providers.mjs +1 -0
- package/dist/src-C48Ew_L9.mjs +707 -0
- package/dist/src-y_QKmTyj.mjs +309 -0
- package/dist/web/assets/index-ihoe7C_u.js +27 -0
- package/dist/web/index.html +30 -0
- package/dist/web-server-DkdA5Ixj.mjs +154 -0
- package/package.json +73 -4
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { a as createLocalJsonlJournal, o as defaultHistoryDirectory, r as createQuotaService, t as PROVIDER_METADATA } from "./bin.mjs";
|
|
2
|
+
import { c as windowProfile, n as calculatePace, t as buildForecastV1 } from "./dist-CIjerpSd.mjs";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { Box, Panel, Text, render, useInput } from "@uniview/tui-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
// ../tui/dist/main.mjs
|
|
8
|
+
const BLOCKS = "▁▂▃▄▅▆▇█";
|
|
9
|
+
const clampPct = (value) => Math.min(100, Math.max(0, value));
|
|
10
|
+
/** Absolute 0–100% scale so the shape never exaggerates small movements. Newest `width` points. */
|
|
11
|
+
function sparkline(values, width) {
|
|
12
|
+
return values.filter((value) => Number.isFinite(value)).slice(-Math.max(0, width)).map((value) => BLOCKS[Math.round(clampPct(value) / 100 * 7)]).join("");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One cell per column; the single `marker` cell is the white ideal-pace tick (time budget, not a forecast).
|
|
16
|
+
* Fill represents remaining quota (`% left`), matching physical battery / tank intuition.
|
|
17
|
+
*/
|
|
18
|
+
function barCells(remainingPercent, idealRemainingPercent, width) {
|
|
19
|
+
const filled = Math.round(clampPct(remainingPercent) / 100 * width);
|
|
20
|
+
const cells = Array.from({ length: width }, (_, i) => i < filled ? "fill" : "empty");
|
|
21
|
+
if (idealRemainingPercent !== null && width > 0) cells[Math.round(clampPct(idealRemainingPercent) / 100 * (width - 1))] = "marker";
|
|
22
|
+
return cells;
|
|
23
|
+
}
|
|
24
|
+
function describeExhaustion(forecast) {
|
|
25
|
+
if (!forecast) return "No forecast yet · run `kuota collect --once`";
|
|
26
|
+
if (forecast.quality === "unsupported") return forecast.reasons.includes("ROLLING_WINDOW_UNMODELED") ? "Forecast unavailable · rolling window is not modeled" : "Forecast unavailable · window semantics unknown";
|
|
27
|
+
if (forecast.reasons.includes("ALREADY_EXHAUSTED")) return "Quota exhausted";
|
|
28
|
+
if (forecast.quality === "insufficient") return "Collecting data for a forecast…";
|
|
29
|
+
if (forecast.exhaustedAt) return `Est. exhaustion ${forecast.exhaustedAt} · confidence ${forecast.quality}`;
|
|
30
|
+
if (forecast.reasons.includes("STABLE_AT_CURRENT_ACTIVITY")) return "Stable at current activity";
|
|
31
|
+
return `On pace · not expected to exhaust before reset · confidence ${forecast.quality}`;
|
|
32
|
+
}
|
|
33
|
+
/** Ideal pace is shown only for windows whose fixed cadence is known; never guessed. */
|
|
34
|
+
function windowPace(provider, window, now) {
|
|
35
|
+
const profile = windowProfile(provider, window.windowId);
|
|
36
|
+
const reset = window.resetAt ? new Date(window.resetAt) : null;
|
|
37
|
+
const start = reset && profile.durationMs !== null ? new Date(reset.getTime() - profile.durationMs) : null;
|
|
38
|
+
return calculatePace({
|
|
39
|
+
now,
|
|
40
|
+
startAt: start,
|
|
41
|
+
resetAt: reset,
|
|
42
|
+
usedPercent: window.usedPercent,
|
|
43
|
+
semantics: profile.semantics,
|
|
44
|
+
startSource: start ? "inferred" : "unknown"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Cycles through windows, then back to "all" (null). */
|
|
48
|
+
function nextWindow(windowIds, current) {
|
|
49
|
+
if (windowIds.length === 0) return null;
|
|
50
|
+
if (current === null) return windowIds[0];
|
|
51
|
+
const index = windowIds.indexOf(current);
|
|
52
|
+
return index < 0 || index === windowIds.length - 1 ? null : windowIds[index + 1];
|
|
53
|
+
}
|
|
54
|
+
const seriesKey = (provider, windowId) => `${provider}/${windowId}`;
|
|
55
|
+
const LABELS = {
|
|
56
|
+
codex: "Codex",
|
|
57
|
+
claude: "Claude Code",
|
|
58
|
+
antigravity: "Antigravity"
|
|
59
|
+
};
|
|
60
|
+
/** Strip control characters before potentially untrusted provider text reaches a terminal. */
|
|
61
|
+
function safeTerminalText(value) {
|
|
62
|
+
return value.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
63
|
+
}
|
|
64
|
+
const PROVIDER_ORDER = [
|
|
65
|
+
"codex",
|
|
66
|
+
"claude",
|
|
67
|
+
"antigravity"
|
|
68
|
+
];
|
|
69
|
+
const BAR_WIDTH = 24;
|
|
70
|
+
/** Color of remaining balance fill: red when quota is low/critical, yellow when cautious, green when healthy. */
|
|
71
|
+
function fillColor(remaining) {
|
|
72
|
+
return remaining <= 15 ? "red" : remaining <= 35 ? "yellow" : "green";
|
|
73
|
+
}
|
|
74
|
+
function bar(cells, remaining) {
|
|
75
|
+
const runs = [];
|
|
76
|
+
for (const kind of cells) {
|
|
77
|
+
const last = runs[runs.length - 1];
|
|
78
|
+
if (last && last.kind === kind) last.length += 1;
|
|
79
|
+
else runs.push({
|
|
80
|
+
kind,
|
|
81
|
+
length: 1
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
85
|
+
flexDirection: "row",
|
|
86
|
+
children: runs.map((run, index) => run.kind === "marker" ? /* @__PURE__ */ jsx(Text, {
|
|
87
|
+
color: "white",
|
|
88
|
+
bold: true,
|
|
89
|
+
children: "┃".repeat(run.length)
|
|
90
|
+
}, index) : run.kind === "fill" ? /* @__PURE__ */ jsx(Text, {
|
|
91
|
+
color: fillColor(remaining),
|
|
92
|
+
children: "█".repeat(run.length)
|
|
93
|
+
}, index) : /* @__PURE__ */ jsx(Text, {
|
|
94
|
+
dim: true,
|
|
95
|
+
children: "░".repeat(run.length)
|
|
96
|
+
}, index))
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function QuotaApp(props) {
|
|
100
|
+
const [inspectSnapshot, setInspectSnapshot] = React.useState(false);
|
|
101
|
+
useInput((input) => {
|
|
102
|
+
if (input === "q") props.onQuit();
|
|
103
|
+
else if (input === "r") props.onRefresh();
|
|
104
|
+
else if (input === "a") props.onSelect(null);
|
|
105
|
+
else if (input === "1") props.onSelect("codex");
|
|
106
|
+
else if (input === "2") props.onSelect("claude");
|
|
107
|
+
else if (input === "3") props.onSelect("antigravity");
|
|
108
|
+
else if (input === "s") setInspectSnapshot((prev) => !prev);
|
|
109
|
+
else if (input === "p") {
|
|
110
|
+
const index = props.selected ? PROVIDER_ORDER.indexOf(props.selected) : -1;
|
|
111
|
+
props.onSelect(index === PROVIDER_ORDER.length - 1 ? null : PROVIDER_ORDER[index + 1]);
|
|
112
|
+
} else if (input === "w") {
|
|
113
|
+
const ids = visible.flatMap((snapshot) => snapshot.windows.map((w) => w.windowId));
|
|
114
|
+
props.onSelectWindow?.(nextWindow([...new Set(ids)], props.selectedWindow ?? null));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const visible = props.selected ? props.snapshots.filter((snapshot) => snapshot.provider === props.selected) : props.snapshots;
|
|
118
|
+
const now = props.now ?? /* @__PURE__ */ new Date();
|
|
119
|
+
const showWindow = (w) => !props.selectedWindow || w.windowId === props.selectedWindow;
|
|
120
|
+
return /* @__PURE__ */ jsx(Panel, {
|
|
121
|
+
title: inspectSnapshot ? "Kuota · Snapshot Provenance Inspector" : "Kuota · AI subscription usage",
|
|
122
|
+
focused: true,
|
|
123
|
+
children: /* @__PURE__ */ jsxs(Box, {
|
|
124
|
+
flexDirection: "column",
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsx(Text, {
|
|
127
|
+
bold: true,
|
|
128
|
+
children: inspectSnapshot ? "Raw Snapshot Inspector" : "Kuota"
|
|
129
|
+
}),
|
|
130
|
+
/* @__PURE__ */ jsx(Text, { children: inspectSnapshot ? "Field provenance, data source freshness, and raw timestamps" : props.loading ? "Refreshing provider quotas…" : "Live local provider snapshots" }),
|
|
131
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
132
|
+
visible.length === 0 && /* @__PURE__ */ jsx(Text, { children: props.loading ? "Loading…" : "No provider results" }),
|
|
133
|
+
inspectSnapshot ? visible.map((snapshot) => /* @__PURE__ */ jsxs(Box, {
|
|
134
|
+
flexDirection: "column",
|
|
135
|
+
children: [
|
|
136
|
+
/* @__PURE__ */ jsx(Text, {
|
|
137
|
+
bold: true,
|
|
138
|
+
color: "cyan",
|
|
139
|
+
children: `${LABELS[snapshot.provider]} · status: ${snapshot.status}`
|
|
140
|
+
}),
|
|
141
|
+
/* @__PURE__ */ jsx(Text, { children: ` source: ${snapshot.source} · fetchedAt: ${snapshot.fetchedAt}` }),
|
|
142
|
+
snapshot.errorCode && /* @__PURE__ */ jsx(Text, {
|
|
143
|
+
color: "red",
|
|
144
|
+
children: ` errorCode: ${safeTerminalText(snapshot.errorCode)}`
|
|
145
|
+
}),
|
|
146
|
+
snapshot.windows.filter(showWindow).map((window) => {
|
|
147
|
+
const profile = windowProfile(snapshot.provider, window.windowId);
|
|
148
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
149
|
+
flexDirection: "column",
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ jsx(Text, { children: ` [window] ${safeTerminalText(window.windowId)}` }),
|
|
152
|
+
/* @__PURE__ */ jsx(Text, {
|
|
153
|
+
dim: true,
|
|
154
|
+
children: ` used: ${window.usedPercent ?? "null"}% | remaining: ${window.remainingPercent ?? "null"}%`
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ jsx(Text, {
|
|
157
|
+
dim: true,
|
|
158
|
+
children: ` resetAt: ${window.resetAt ?? "none"} | semantics: ${profile.semantics}`
|
|
159
|
+
})
|
|
160
|
+
]
|
|
161
|
+
}, window.windowId);
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ jsx(Text, { children: " " })
|
|
164
|
+
]
|
|
165
|
+
}, snapshot.provider)) : visible.map((snapshot) => /* @__PURE__ */ jsxs(Box, {
|
|
166
|
+
flexDirection: "column",
|
|
167
|
+
children: [
|
|
168
|
+
/* @__PURE__ */ jsx(Text, {
|
|
169
|
+
bold: true,
|
|
170
|
+
children: `${LABELS[snapshot.provider]} · ${snapshot.status}`
|
|
171
|
+
}),
|
|
172
|
+
snapshot.windows.length === 0 && /* @__PURE__ */ jsx(Text, { children: snapshot.errorCode ? safeTerminalText(snapshot.errorCode) : "quota unknown" }),
|
|
173
|
+
snapshot.windows.filter(showWindow).map((window) => {
|
|
174
|
+
const pace = windowPace(snapshot.provider, window, now);
|
|
175
|
+
const remaining = window.remainingPercent ?? (window.usedPercent !== null ? 100 - window.usedPercent : null);
|
|
176
|
+
const idealRemaining = pace ? 100 - pace.idealUsedPercent : null;
|
|
177
|
+
const history = props.series?.[seriesKey(snapshot.provider, window.windowId)] ?? [];
|
|
178
|
+
const forecast = props.forecasts?.find((f) => f.provider === snapshot.provider && f.windowId === window.windowId);
|
|
179
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
180
|
+
flexDirection: "column",
|
|
181
|
+
children: [
|
|
182
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${safeTerminalText(window.windowId)}: ${remaining === null ? "unknown" : `${Math.round(remaining)}% remaining`}${window.resetAt ? ` · resets ${window.resetAt}` : ""}` }),
|
|
183
|
+
remaining !== null && /* @__PURE__ */ jsxs(Box, {
|
|
184
|
+
flexDirection: "row",
|
|
185
|
+
children: [
|
|
186
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
187
|
+
bar(barCells(remaining, idealRemaining, BAR_WIDTH), remaining),
|
|
188
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${Math.round(remaining)}% left${idealRemaining !== null && pace ? ` · ideal ${Math.round(idealRemaining)}% left (${pace.quality})` : ""}` })
|
|
189
|
+
]
|
|
190
|
+
}),
|
|
191
|
+
props.forecasts !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [history.length > 0 && /* @__PURE__ */ jsx(Text, {
|
|
192
|
+
dim: true,
|
|
193
|
+
children: ` trend ${sparkline(history, 30)}`
|
|
194
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
195
|
+
dim: true,
|
|
196
|
+
children: ` ${describeExhaustion(forecast)}`
|
|
197
|
+
})] })
|
|
198
|
+
]
|
|
199
|
+
}, window.windowId);
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ jsx(Text, { children: " " })
|
|
202
|
+
]
|
|
203
|
+
}, snapshot.provider)),
|
|
204
|
+
/* @__PURE__ */ jsx(Text, { children: "r refresh · 1/2/3/p provider · a all · w window · s snapshot inspect · q quit" })
|
|
205
|
+
]
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const DAY = 864e5;
|
|
210
|
+
/** Read-only: the TUI never writes the journal; `kuota collect --once` owns appends. */
|
|
211
|
+
async function loadHistoryView(directory, now) {
|
|
212
|
+
try {
|
|
213
|
+
const { events } = await createLocalJsonlJournal({ directory }).readAll({
|
|
214
|
+
from: (/* @__PURE__ */ new Date(now.getTime() - 30 * DAY)).toISOString(),
|
|
215
|
+
to: now.toISOString()
|
|
216
|
+
});
|
|
217
|
+
const series = {};
|
|
218
|
+
const keys = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const event of [...events].sort((a, b) => a.observedAt.localeCompare(b.observedAt))) {
|
|
220
|
+
if (event.kind !== "quota_observed" || event.usedPercent === null) continue;
|
|
221
|
+
const key = seriesKey(event.provider, event.windowId);
|
|
222
|
+
(series[key] ??= []).push(event.usedPercent);
|
|
223
|
+
keys.set(key, event);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
series,
|
|
227
|
+
forecasts: [...keys.values()].map((k) => buildForecastV1({
|
|
228
|
+
now,
|
|
229
|
+
provider: k.provider,
|
|
230
|
+
windowId: k.windowId,
|
|
231
|
+
observations: events,
|
|
232
|
+
mode: "all-window"
|
|
233
|
+
}))
|
|
234
|
+
};
|
|
235
|
+
} catch {
|
|
236
|
+
return {
|
|
237
|
+
series: {},
|
|
238
|
+
forecasts: []
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function ensureInteractiveTerminal(inputIsTty, outputIsTty) {
|
|
243
|
+
if (!inputIsTty || !outputIsTty) throw new Error("Kuota TUI requires an interactive terminal; run `kuota status --json` instead.");
|
|
244
|
+
}
|
|
245
|
+
/** Interactive entrypoint. Consumers of @kuota/core never import or start this. */
|
|
246
|
+
async function startTui() {
|
|
247
|
+
ensureInteractiveTerminal(Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY));
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
let stopped = false;
|
|
250
|
+
let loading = true;
|
|
251
|
+
let selected = null;
|
|
252
|
+
let selectedWindow = null;
|
|
253
|
+
let history = {
|
|
254
|
+
series: {},
|
|
255
|
+
forecasts: []
|
|
256
|
+
};
|
|
257
|
+
let snapshots = [];
|
|
258
|
+
let app;
|
|
259
|
+
const service = createQuotaService();
|
|
260
|
+
const rerender = () => {
|
|
261
|
+
if (!app || stopped) return;
|
|
262
|
+
app.render(/* @__PURE__ */ jsx(QuotaApp, {
|
|
263
|
+
snapshots,
|
|
264
|
+
loading,
|
|
265
|
+
selected,
|
|
266
|
+
selectedWindow,
|
|
267
|
+
series: history.series,
|
|
268
|
+
forecasts: history.forecasts,
|
|
269
|
+
onSelectWindow: (windowId) => {
|
|
270
|
+
selectedWindow = windowId;
|
|
271
|
+
rerender();
|
|
272
|
+
},
|
|
273
|
+
onRefresh: () => {
|
|
274
|
+
refresh();
|
|
275
|
+
},
|
|
276
|
+
onQuit: quit,
|
|
277
|
+
onSelect: (provider) => {
|
|
278
|
+
selected = provider;
|
|
279
|
+
selectedWindow = null;
|
|
280
|
+
rerender();
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
283
|
+
};
|
|
284
|
+
const quit = () => {
|
|
285
|
+
if (stopped) return;
|
|
286
|
+
stopped = true;
|
|
287
|
+
process.off("SIGINT", quit);
|
|
288
|
+
queueMicrotask(() => {
|
|
289
|
+
try {
|
|
290
|
+
app?.destroy();
|
|
291
|
+
resolve();
|
|
292
|
+
} catch (error) {
|
|
293
|
+
reject(error);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
const refresh = async () => {
|
|
298
|
+
if (stopped || loading) return;
|
|
299
|
+
loading = true;
|
|
300
|
+
rerender();
|
|
301
|
+
try {
|
|
302
|
+
snapshots = (await Effect.runPromise(service.read(PROVIDER_METADATA.map(({ id }) => id), { force: true }))).map((observation) => observation.snapshot);
|
|
303
|
+
history = await loadHistoryView(defaultHistoryDirectory(), /* @__PURE__ */ new Date());
|
|
304
|
+
} catch {} finally {
|
|
305
|
+
loading = false;
|
|
306
|
+
rerender();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
app = render(/* @__PURE__ */ jsx(QuotaApp, {
|
|
311
|
+
snapshots,
|
|
312
|
+
loading: true,
|
|
313
|
+
selected,
|
|
314
|
+
selectedWindow,
|
|
315
|
+
series: history.series,
|
|
316
|
+
forecasts: history.forecasts,
|
|
317
|
+
onSelectWindow: (windowId) => {
|
|
318
|
+
selectedWindow = windowId;
|
|
319
|
+
rerender();
|
|
320
|
+
},
|
|
321
|
+
onRefresh: () => {
|
|
322
|
+
refresh();
|
|
323
|
+
},
|
|
324
|
+
onQuit: quit,
|
|
325
|
+
onSelect: (provider) => {
|
|
326
|
+
selected = provider;
|
|
327
|
+
selectedWindow = null;
|
|
328
|
+
rerender();
|
|
329
|
+
}
|
|
330
|
+
}));
|
|
331
|
+
process.on("SIGINT", quit);
|
|
332
|
+
loading = false;
|
|
333
|
+
refresh();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
process.off("SIGINT", quit);
|
|
336
|
+
reject(error);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
export { startTui };
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import "./src-y_QKmTyj.mjs";
|
|
2
|
+
// ../core/src/normalize.ts
|
|
3
|
+
function safePercent(value) {
|
|
4
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? value : null;
|
|
5
|
+
}
|
|
6
|
+
function safeIsoDate(value) {
|
|
7
|
+
if (typeof value !== "string" && typeof value !== "number") return null;
|
|
8
|
+
const date = new Date(value);
|
|
9
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
10
|
+
}
|
|
11
|
+
function normalizeWindow(input) {
|
|
12
|
+
const usedPercent = safePercent(input.usedPercent);
|
|
13
|
+
return {
|
|
14
|
+
windowId: input.windowId,
|
|
15
|
+
usedPercent,
|
|
16
|
+
remainingPercent: usedPercent === null ? null : Number((100 - usedPercent).toFixed(2)),
|
|
17
|
+
resetAt: safeIsoDate(input.resetAt)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Whitelist public response fields: never forward token-like keys or provider raw payloads. */
|
|
21
|
+
function redactSnapshot(input) {
|
|
22
|
+
const provider = input.provider;
|
|
23
|
+
const status = input.status;
|
|
24
|
+
return {
|
|
25
|
+
provider: provider === "claude" || provider === "antigravity" ? provider : "codex",
|
|
26
|
+
status: status === "ok" || status === "partial" || status === "unauthenticated" || status === "unavailable" || status === "unsupported" || status === "error" ? status : "error",
|
|
27
|
+
fetchedAt: safeIsoDate(input.fetchedAt) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
28
|
+
source: typeof input.source === "string" ? input.source : "unknown",
|
|
29
|
+
windows: Array.isArray(input.windows) ? input.windows.flatMap((window) => {
|
|
30
|
+
if (!window || typeof window !== "object") return [];
|
|
31
|
+
const value = window;
|
|
32
|
+
if (typeof value.windowId !== "string") return [];
|
|
33
|
+
return [normalizeWindow({
|
|
34
|
+
windowId: value.windowId,
|
|
35
|
+
usedPercent: value.usedPercent,
|
|
36
|
+
resetAt: value.resetAt
|
|
37
|
+
})];
|
|
38
|
+
}) : [],
|
|
39
|
+
errorCode: typeof input.errorCode === "string" ? input.errorCode : null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function makeUnavailableSnapshot(provider, errorCode, status = "unavailable") {
|
|
43
|
+
return {
|
|
44
|
+
provider,
|
|
45
|
+
status,
|
|
46
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47
|
+
source: "local-probe",
|
|
48
|
+
windows: [],
|
|
49
|
+
errorCode
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ../core/src/metrics/pace.ts
|
|
53
|
+
/** Returns null when the provider has not proved a comparable fixed window. */
|
|
54
|
+
function calculatePace(input) {
|
|
55
|
+
if (input.semantics === "rolling" || input.semantics === "unknown" || input.startSource === "unknown" || input.semantics === "first_use" && input.startSource !== "provider" || input.startAt === null || input.resetAt === null || input.usedPercent === null || !Number.isFinite(input.usedPercent) || input.usedPercent < 0 || input.usedPercent > 100) return null;
|
|
56
|
+
const now = input.now.getTime();
|
|
57
|
+
const start = input.startAt.getTime();
|
|
58
|
+
const reset = input.resetAt.getTime();
|
|
59
|
+
if (!Number.isFinite(now) || !Number.isFinite(start) || !Number.isFinite(reset) || reset <= start || now < start) return null;
|
|
60
|
+
const elapsedFraction = Math.min(1, (now - start) / (reset - start));
|
|
61
|
+
const idealUsedPercent = elapsedFraction * 100;
|
|
62
|
+
return {
|
|
63
|
+
elapsedFraction,
|
|
64
|
+
idealUsedPercent,
|
|
65
|
+
idealRemainingPercent: 100 - idealUsedPercent,
|
|
66
|
+
actualUsedPercent: input.usedPercent,
|
|
67
|
+
paceDeltaPoints: input.usedPercent - idealUsedPercent,
|
|
68
|
+
paceRatio: idealUsedPercent === 0 ? null : input.usedPercent / idealUsedPercent,
|
|
69
|
+
remainingHours: Math.max(0, reset - now) / 36e5,
|
|
70
|
+
quality: input.startSource === "provider" ? "exact" : "inferred"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ../core/src/observation.ts
|
|
74
|
+
function iso(ms) {
|
|
75
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Pure constructor. If `snapshot.fetchedAt` is not a valid timestamp we fall back to the
|
|
79
|
+
* injected `checkedAtMs` as the observation time, so no NaN / Invalid Date is ever emitted.
|
|
80
|
+
*/
|
|
81
|
+
function makeObservation(input) {
|
|
82
|
+
const parsed = Date.parse(input.snapshot.fetchedAt);
|
|
83
|
+
const observedMs = Number.isFinite(parsed) ? parsed : input.checkedAtMs;
|
|
84
|
+
const next = input.nextAttemptAtMs;
|
|
85
|
+
return {
|
|
86
|
+
snapshot: input.snapshot,
|
|
87
|
+
freshness: input.freshness,
|
|
88
|
+
observedAt: iso(observedMs),
|
|
89
|
+
checkedAt: iso(input.checkedAtMs),
|
|
90
|
+
ageMs: Math.max(0, input.nowMs - observedMs),
|
|
91
|
+
nextAttemptAt: next === null || next === void 0 || !Number.isFinite(next) ? null : iso(next),
|
|
92
|
+
latestErrorCode: input.latestErrorCode ?? null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ../core/src/forecast/forecast.ts
|
|
96
|
+
const H = 36e5;
|
|
97
|
+
const EPOCH_TOLERANCE_MS = 6e5;
|
|
98
|
+
const DECREASE_TOLERANCE = .5;
|
|
99
|
+
const GAP_CAP_HOURS = 6;
|
|
100
|
+
const RECENT_HOURS = 24;
|
|
101
|
+
const STALE_LATEST_MS = 6 * H;
|
|
102
|
+
const SLOPE_EPSILON = 1e-6;
|
|
103
|
+
const LEVELS = [
|
|
104
|
+
"low",
|
|
105
|
+
"medium",
|
|
106
|
+
"high"
|
|
107
|
+
];
|
|
108
|
+
const ms = (iso) => iso === null ? NaN : Date.parse(iso);
|
|
109
|
+
const boundary = (e) => {
|
|
110
|
+
const reset = ms(e.resetAt);
|
|
111
|
+
return Number.isFinite(reset) ? reset : ms(e.epochId);
|
|
112
|
+
};
|
|
113
|
+
function isMeasurable(e) {
|
|
114
|
+
return e.kind === "quota_observed" && (e.state === "ok" || e.state === "partial" || e.state === "stale") && e.usedPercent !== null && Number.isFinite(e.usedPercent) && Number.isFinite(ms(e.observedAt));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reset/epoch detection. A new segment starts when the provider-reported reset boundary moves
|
|
118
|
+
* (beyond jitter tolerance) or when used% drops (credit / unannounced reset). Fixed windows only
|
|
119
|
+
* ever compare points inside one segment.
|
|
120
|
+
*/
|
|
121
|
+
function detectEpochs(events) {
|
|
122
|
+
const sorted = events.filter(isMeasurable).map((e, i) => ({
|
|
123
|
+
e,
|
|
124
|
+
i
|
|
125
|
+
})).sort((a, b) => ms(a.e.observedAt) - ms(b.e.observedAt) || a.i - b.i).map(({ e }) => e);
|
|
126
|
+
const segments = [];
|
|
127
|
+
let lastTime = NaN;
|
|
128
|
+
for (const event of sorted) {
|
|
129
|
+
const time = ms(event.observedAt);
|
|
130
|
+
if (time === lastTime) continue;
|
|
131
|
+
lastTime = time;
|
|
132
|
+
const current = segments[segments.length - 1];
|
|
133
|
+
const prev = current?.events[current.events.length - 1];
|
|
134
|
+
if (!current || !prev) {
|
|
135
|
+
segments.push({
|
|
136
|
+
events: [event],
|
|
137
|
+
reason: null
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const a = boundary(prev);
|
|
142
|
+
const b = boundary(event);
|
|
143
|
+
if (Number.isFinite(a) && Number.isFinite(b) && Math.abs(a - b) > EPOCH_TOLERANCE_MS) segments.push({
|
|
144
|
+
events: [event],
|
|
145
|
+
reason: "EPOCH_RESET_DETECTED"
|
|
146
|
+
});
|
|
147
|
+
else if (event.usedPercent < prev.usedPercent - DECREASE_TOLERANCE) segments.push({
|
|
148
|
+
events: [event],
|
|
149
|
+
reason: "USAGE_DECREASE_SPLIT"
|
|
150
|
+
});
|
|
151
|
+
else current.events.push(event);
|
|
152
|
+
}
|
|
153
|
+
return segments;
|
|
154
|
+
}
|
|
155
|
+
function weightedMedian(items) {
|
|
156
|
+
const sorted = [...items].sort((a, b) => a.value - b.value);
|
|
157
|
+
const total = sorted.reduce((sum, item) => sum + item.weight, 0);
|
|
158
|
+
let acc = 0;
|
|
159
|
+
for (const item of sorted) {
|
|
160
|
+
acc += item.weight;
|
|
161
|
+
if (acc >= total / 2) return item.value;
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
const unsupported = (reasons) => ({
|
|
166
|
+
slopePercentPerHour: null,
|
|
167
|
+
exhaustedAt: null,
|
|
168
|
+
projectedUsedAtResetPercent: null,
|
|
169
|
+
sampleCount: 0,
|
|
170
|
+
observedSpanHours: 0,
|
|
171
|
+
quality: "unsupported",
|
|
172
|
+
reasons
|
|
173
|
+
});
|
|
174
|
+
function forecastQuota(input) {
|
|
175
|
+
const segments = detectEpochs(input.observations);
|
|
176
|
+
const last = segments[segments.length - 1];
|
|
177
|
+
if (!last) return {
|
|
178
|
+
...unsupported([]),
|
|
179
|
+
quality: "insufficient",
|
|
180
|
+
reasons: ["NO_OBSERVATIONS"]
|
|
181
|
+
};
|
|
182
|
+
const latest = last.events[last.events.length - 1];
|
|
183
|
+
if (latest.semantics === "rolling") return unsupported(["ROLLING_WINDOW_UNMODELED"]);
|
|
184
|
+
if (latest.semantics === "unknown") return unsupported(["WINDOW_SEMANTICS_UNKNOWN"]);
|
|
185
|
+
const reasons = [];
|
|
186
|
+
if (last.reason) reasons.push(last.reason);
|
|
187
|
+
let samples = last.events;
|
|
188
|
+
const latestMs = ms(latest.observedAt);
|
|
189
|
+
if (input.mode === "recent-activity") samples = samples.filter((e) => ms(e.observedAt) >= latestMs - RECENT_HOURS * H);
|
|
190
|
+
const first = samples[0];
|
|
191
|
+
const spanHours = (latestMs - ms(first.observedAt)) / H;
|
|
192
|
+
const used = latest.usedPercent;
|
|
193
|
+
const base = {
|
|
194
|
+
sampleCount: samples.length,
|
|
195
|
+
observedSpanHours: spanHours
|
|
196
|
+
};
|
|
197
|
+
if (samples.length < 3 || spanHours < 2) return {
|
|
198
|
+
...base,
|
|
199
|
+
slopePercentPerHour: null,
|
|
200
|
+
exhaustedAt: used >= 100 ? latest.observedAt : null,
|
|
201
|
+
projectedUsedAtResetPercent: null,
|
|
202
|
+
quality: "insufficient",
|
|
203
|
+
reasons: [
|
|
204
|
+
...reasons,
|
|
205
|
+
"INSUFFICIENT_DATA",
|
|
206
|
+
...used >= 100 ? ["ALREADY_EXHAUSTED"] : []
|
|
207
|
+
]
|
|
208
|
+
};
|
|
209
|
+
const intervals = [];
|
|
210
|
+
let changes = 0;
|
|
211
|
+
for (let i = 1; i < samples.length; i++) {
|
|
212
|
+
const dt = (ms(samples[i].observedAt) - ms(samples[i - 1].observedAt)) / H;
|
|
213
|
+
const du = samples[i].usedPercent - samples[i - 1].usedPercent;
|
|
214
|
+
if (dt <= 0 || du < 0) continue;
|
|
215
|
+
if (du > 0) changes++;
|
|
216
|
+
intervals.push({
|
|
217
|
+
value: du / dt,
|
|
218
|
+
weight: Math.min(dt, GAP_CAP_HOURS)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const slope = input.mode === "all-window" ? (used - first.usedPercent) / spanHours : weightedMedian(intervals);
|
|
222
|
+
const positive = intervals.filter((i) => i.value > 0).map((i) => i.value);
|
|
223
|
+
const mean = positive.reduce((a, b) => a + b, 0) / (positive.length || 1);
|
|
224
|
+
const variance = positive.reduce((a, b) => a + (b - mean) ** 2, 0) / (positive.length || 1);
|
|
225
|
+
const lowVariability = mean > 0 && Math.sqrt(variance) / mean < .5;
|
|
226
|
+
let level = 0;
|
|
227
|
+
if (spanHours >= 24 && used >= 10) level = 1;
|
|
228
|
+
if (spanHours >= 72 && changes >= 20 && lowVariability && used >= 10) level = 2;
|
|
229
|
+
else if (spanHours >= 24 && changes < 6) level = 0;
|
|
230
|
+
if (samples.some((s) => s.state === "stale")) {
|
|
231
|
+
reasons.push("STALE_INPUT");
|
|
232
|
+
level = Math.max(0, level - 1);
|
|
233
|
+
}
|
|
234
|
+
if (input.now.getTime() - latestMs > STALE_LATEST_MS) {
|
|
235
|
+
reasons.push("LATEST_SAMPLE_OLD");
|
|
236
|
+
level = Math.max(0, level - 1);
|
|
237
|
+
}
|
|
238
|
+
const quality = LEVELS[level];
|
|
239
|
+
const resetMs = input.resetAt ? input.resetAt.getTime() : boundary(latest);
|
|
240
|
+
const hasReset = Number.isFinite(resetMs);
|
|
241
|
+
const roundedSlope = Math.max(0, slope);
|
|
242
|
+
let exhaustedAt = null;
|
|
243
|
+
let projected = null;
|
|
244
|
+
if (used >= 100) {
|
|
245
|
+
exhaustedAt = latest.observedAt;
|
|
246
|
+
projected = 100;
|
|
247
|
+
reasons.push("ALREADY_EXHAUSTED");
|
|
248
|
+
} else if (roundedSlope <= SLOPE_EPSILON) {
|
|
249
|
+
reasons.push("STABLE_AT_CURRENT_ACTIVITY");
|
|
250
|
+
projected = used;
|
|
251
|
+
} else {
|
|
252
|
+
const exhaustMs = latestMs + (100 - used) / roundedSlope * H;
|
|
253
|
+
projected = Math.min(100, used + roundedSlope * (hasReset ? Math.max(0, resetMs - latestMs) / H : 0));
|
|
254
|
+
if (!hasReset || exhaustMs <= resetMs) {
|
|
255
|
+
exhaustedAt = new Date(exhaustMs).toISOString();
|
|
256
|
+
reasons.push("EXHAUSTION_BEFORE_RESET");
|
|
257
|
+
} else reasons.push("ON_PACE");
|
|
258
|
+
if (!hasReset) projected = null;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
slopePercentPerHour: roundedSlope,
|
|
263
|
+
exhaustedAt,
|
|
264
|
+
projectedUsedAtResetPercent: projected,
|
|
265
|
+
quality,
|
|
266
|
+
reasons
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** Account-safe wrapper: only events sharing the newest event's accountScope are considered. */
|
|
270
|
+
function buildForecastV1(input) {
|
|
271
|
+
const scoped = input.observations.filter((e) => e.provider === input.provider && e.windowId === input.windowId && isMeasurable(e)).sort((a, b) => ms(a.observedAt) - ms(b.observedAt));
|
|
272
|
+
const newest = scoped[scoped.length - 1];
|
|
273
|
+
const events = newest ? scoped.filter((e) => e.accountScope === newest.accountScope) : [];
|
|
274
|
+
const result = forecastQuota({
|
|
275
|
+
now: input.now,
|
|
276
|
+
resetAt: null,
|
|
277
|
+
observations: events,
|
|
278
|
+
mode: input.mode
|
|
279
|
+
});
|
|
280
|
+
const latest = events[events.length - 1];
|
|
281
|
+
const pace = latest ? calculatePace({
|
|
282
|
+
now: input.now,
|
|
283
|
+
startAt: latest.startAt ? new Date(latest.startAt) : null,
|
|
284
|
+
resetAt: latest.resetAt ? new Date(latest.resetAt) : null,
|
|
285
|
+
usedPercent: latest.usedPercent,
|
|
286
|
+
semantics: latest.semantics,
|
|
287
|
+
startSource: latest.startAt ? "inferred" : "unknown"
|
|
288
|
+
}) : null;
|
|
289
|
+
return {
|
|
290
|
+
schemaVersion: 1,
|
|
291
|
+
provider: input.provider,
|
|
292
|
+
windowId: input.windowId,
|
|
293
|
+
computedAt: input.now.toISOString(),
|
|
294
|
+
currentUsedPercent: latest ? latest.usedPercent : null,
|
|
295
|
+
idealUsedPercent: pace ? pace.idealUsedPercent : null,
|
|
296
|
+
observedSpanHours: result.observedSpanHours,
|
|
297
|
+
sampleCount: result.sampleCount,
|
|
298
|
+
slopePercentPerHour: result.slopePercentPerHour,
|
|
299
|
+
exhaustedAt: result.exhaustedAt,
|
|
300
|
+
projectedUsedAtResetPercent: result.projectedUsedAtResetPercent,
|
|
301
|
+
expectedTimeToLimitHours: result.exhaustedAt ? Math.max(0, (Date.parse(result.exhaustedAt) - input.now.getTime()) / H) : null,
|
|
302
|
+
mode: input.mode,
|
|
303
|
+
quality: result.quality,
|
|
304
|
+
reasons: [...result.reasons]
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
export { makeObservation as a, normalizeWindow as c, isMeasurable as i, redactSnapshot as l, detectEpochs as n, calculatePace as o, forecastQuota as r, makeUnavailableSnapshot as s, buildForecastV1 as t };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "./index-NHzh2TGn.mjs";
|
|
2
|
+
/** A linear *time budget* reference; this is not a statistically inferred forecast. */
|
|
3
|
+
interface PaceInput {
|
|
4
|
+
now: Date;
|
|
5
|
+
startAt: Date | null;
|
|
6
|
+
resetAt: Date | null;
|
|
7
|
+
usedPercent: number | null;
|
|
8
|
+
semantics: "fixed" | "rolling" | "first_use" | "unknown";
|
|
9
|
+
startSource: "provider" | "inferred" | "unknown";
|
|
10
|
+
}
|
|
11
|
+
interface PaceResult {
|
|
12
|
+
/** 0 at the window start, 1 at the reset boundary. */
|
|
13
|
+
elapsedFraction: number;
|
|
14
|
+
/** Position of the white tick on a *used* progress bar. */
|
|
15
|
+
idealUsedPercent: number;
|
|
16
|
+
/** Position of the white tick on a *remaining* progress bar. */
|
|
17
|
+
idealRemainingPercent: number;
|
|
18
|
+
actualUsedPercent: number;
|
|
19
|
+
/** Positive means consumption is ahead of linear elapsed-time pace. */
|
|
20
|
+
paceDeltaPoints: number;
|
|
21
|
+
paceRatio: number | null;
|
|
22
|
+
remainingHours: number;
|
|
23
|
+
/** "inferred" is never presented as an exact provider-reported epoch. */
|
|
24
|
+
quality: "exact" | "inferred";
|
|
25
|
+
}
|
|
26
|
+
/** Returns null when the provider has not proved a comparable fixed window. */
|
|
27
|
+
declare function calculatePace(input: PaceInput): PaceResult | null;
|
|
28
|
+
/** Dependency-free SHA-256 (hex). Keeps @kuota/core free of node:crypto so it stays browser-safe. */
|
|
29
|
+
declare function sha256Hex(text: string): string;
|
|
30
|
+
export { calculatePace as i, PaceInput as n, PaceResult as r, sha256Hex as t };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { _ as normalizeWindow, a as detectEpochs, g as makeUnavailableSnapshot, h as makeObservation, i as buildForecastV1, n as ForecastQuality, o as forecastQuota, p as windowProfile, r as QuotaForecastV1, s as isMeasurable, t as ForecastMode, u as WindowProfile, v as redactSnapshot } from "./index-NHzh2TGn.mjs";
|
|
2
|
+
import { i as calculatePace, n as PaceInput, r as PaceResult, t as sha256Hex } from "./metrics-D7oNul_9.mjs";
|
|
3
|
+
export { type ForecastMode, type ForecastQuality, type PaceInput, type PaceResult, type QuotaForecastV1, type WindowProfile, buildForecastV1, calculatePace, detectEpochs, forecastQuota, isMeasurable, makeObservation, makeUnavailableSnapshot, normalizeWindow, redactSnapshot, sha256Hex, windowProfile };
|
package/dist/metrics.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as sha256Hex, i as windowProfile } from "./src-y_QKmTyj.mjs";
|
|
2
|
+
import { a as makeObservation, c as normalizeWindow, i as isMeasurable, l as redactSnapshot, n as detectEpochs, o as calculatePace, r as forecastQuota, s as makeUnavailableSnapshot, t as buildForecastV1 } from "./metrics-Bzmrfgn-.mjs";
|
|
3
|
+
export { buildForecastV1, calculatePace, detectEpochs, forecastQuota, isMeasurable, makeObservation, makeUnavailableSnapshot, normalizeWindow, redactSnapshot, sha256Hex, windowProfile };
|