@love-moon/ai-sdk 0.3.2 → 0.4.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/built-in-backends.d.ts +1 -0
  3. package/dist/built-in-backends.js +6 -0
  4. package/dist/client.d.ts +15 -0
  5. package/dist/client.js +103 -1
  6. package/dist/external-provider-registry.js +4 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +3 -0
  9. package/dist/manager/account.d.ts +6 -0
  10. package/dist/manager/account.js +121 -0
  11. package/dist/manager/auth-parser.d.ts +27 -0
  12. package/dist/manager/auth-parser.js +54 -0
  13. package/dist/manager/config.d.ts +6 -0
  14. package/dist/manager/config.js +32 -0
  15. package/dist/manager/index.d.ts +12 -0
  16. package/dist/manager/index.js +11 -0
  17. package/dist/manager/install.d.ts +9 -0
  18. package/dist/manager/install.js +117 -0
  19. package/dist/manager/manager.d.ts +51 -0
  20. package/dist/manager/manager.js +105 -0
  21. package/dist/manager/network.d.ts +8 -0
  22. package/dist/manager/network.js +46 -0
  23. package/dist/manager/paths.d.ts +6 -0
  24. package/dist/manager/paths.js +16 -0
  25. package/dist/manager/quota/cache.d.ts +9 -0
  26. package/dist/manager/quota/cache.js +33 -0
  27. package/dist/manager/quota/claude.d.ts +19 -0
  28. package/dist/manager/quota/claude.js +193 -0
  29. package/dist/manager/quota/codex.d.ts +27 -0
  30. package/dist/manager/quota/codex.js +182 -0
  31. package/dist/manager/quota/copilot.d.ts +64 -0
  32. package/dist/manager/quota/copilot.js +718 -0
  33. package/dist/manager/quota/external.d.ts +29 -0
  34. package/dist/manager/quota/external.js +176 -0
  35. package/dist/manager/quota/headers.d.ts +5 -0
  36. package/dist/manager/quota/headers.js +29 -0
  37. package/dist/manager/quota/kimi.d.ts +24 -0
  38. package/dist/manager/quota/kimi.js +230 -0
  39. package/dist/manager/types.d.ts +166 -0
  40. package/dist/manager/types.js +1 -0
  41. package/dist/providers/chat-web-session.d.ts +218 -0
  42. package/dist/providers/chat-web-session.js +593 -0
  43. package/dist/providers/claude-agent-sdk-session.d.ts +35 -1
  44. package/dist/providers/claude-agent-sdk-session.js +109 -1
  45. package/dist/providers/codex-app-server-session.d.ts +107 -0
  46. package/dist/providers/codex-app-server-session.js +479 -9
  47. package/dist/providers/copilot-sdk-session.d.ts +1 -1
  48. package/dist/resume/chat-web.d.ts +20 -0
  49. package/dist/resume/chat-web.js +44 -0
  50. package/dist/resume/index.js +2 -0
  51. package/dist/session-factory.d.ts +3 -1
  52. package/dist/session-factory.js +17 -4
  53. package/dist/shared.d.ts +159 -0
  54. package/dist/shared.js +111 -0
  55. package/dist/transports/codex-app-server-transport.d.ts +1 -0
  56. package/dist/transports/codex-app-server-transport.js +45 -1
  57. package/dist/worker.js +19 -5
  58. package/package.json +10 -3
@@ -0,0 +1,29 @@
1
+ import type { ExternalQuota, ExternalQuotaList } from "../types.js";
2
+ export interface GetExternalQuotaOptions {
3
+ /** External ai-sdk provider backend or alias. */
4
+ backend: string;
5
+ model?: string;
6
+ forceRefresh?: boolean;
7
+ ttlSeconds?: number;
8
+ timeoutMs?: number;
9
+ configPath?: string;
10
+ }
11
+ export interface GetExternalQuotaListOptions {
12
+ /** External ai-sdk provider backend or alias. */
13
+ backend: string;
14
+ forceRefresh?: boolean;
15
+ ttlSeconds?: number;
16
+ timeoutMs?: number;
17
+ configPath?: string;
18
+ }
19
+ export declare function normalizeExternalQuota(value: unknown, options: {
20
+ backend: string;
21
+ model?: string;
22
+ error?: string;
23
+ }): ExternalQuota;
24
+ export declare function normalizeExternalQuotaList(value: unknown, options: {
25
+ backend: string;
26
+ error?: string;
27
+ }): ExternalQuotaList;
28
+ export declare function getExternalQuota(opts: GetExternalQuotaOptions): Promise<ExternalQuota>;
29
+ export declare function getExternalQuotaList(opts: GetExternalQuotaListOptions): Promise<ExternalQuotaList>;
@@ -0,0 +1,176 @@
1
+ import { getExternalProviderDescriptor } from "../../external-provider-registry.js";
2
+ function nowSeconds() {
3
+ return Math.floor(Date.now() / 1000);
4
+ }
5
+ function isRecord(value) {
6
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function normalizeString(value) {
9
+ if (typeof value !== "string")
10
+ return undefined;
11
+ const trimmed = value.trim();
12
+ return trimmed || undefined;
13
+ }
14
+ function normalizeNumber(value) {
15
+ if (typeof value === "number") {
16
+ return Number.isFinite(value) ? value : undefined;
17
+ }
18
+ if (typeof value === "string" && value.trim()) {
19
+ const parsed = Number(value);
20
+ return Number.isFinite(parsed) ? parsed : undefined;
21
+ }
22
+ return undefined;
23
+ }
24
+ function normalizeSource(value) {
25
+ return value === "fresh" || value === "cached" || value === "stale" || value === "unknown"
26
+ ? value
27
+ : "unknown";
28
+ }
29
+ function normalizeWindow(value) {
30
+ const record = isRecord(value) ? value : {};
31
+ const usedPercent = normalizeNumber(record.usedPercent) ?? 0;
32
+ const remainingPercent = normalizeNumber(record.remainingPercent) ?? Math.max(0, 100 - usedPercent);
33
+ const window = {
34
+ usedPercent,
35
+ remainingPercent,
36
+ };
37
+ const resetAfterSeconds = normalizeNumber(record.resetAfterSeconds);
38
+ if (resetAfterSeconds !== undefined)
39
+ window.resetAfterSeconds = resetAfterSeconds;
40
+ const resetAt = normalizeNumber(record.resetAt);
41
+ if (resetAt !== undefined)
42
+ window.resetAt = resetAt;
43
+ const resetOnDate = normalizeString(record.resetOnDate);
44
+ if (resetOnDate)
45
+ window.resetOnDate = resetOnDate;
46
+ const status = normalizeString(record.status);
47
+ if (status)
48
+ window.status = status;
49
+ const windowMinutes = normalizeNumber(record.windowMinutes);
50
+ if (windowMinutes !== undefined)
51
+ window.windowMinutes = windowMinutes;
52
+ const limit = normalizeNumber(record.limit);
53
+ if (limit !== undefined)
54
+ window.limit = limit;
55
+ const used = normalizeNumber(record.used);
56
+ if (used !== undefined)
57
+ window.used = used;
58
+ const remaining = normalizeNumber(record.remaining);
59
+ if (remaining !== undefined)
60
+ window.remaining = remaining;
61
+ return window;
62
+ }
63
+ export function normalizeExternalQuota(value, options) {
64
+ const record = isRecord(value) ? value : {};
65
+ const model = normalizeString(record.model) ?? normalizeString(options.model) ?? "";
66
+ return {
67
+ backend: options.backend,
68
+ model,
69
+ daily: normalizeWindow(record.daily),
70
+ fetchedAt: normalizeNumber(record.fetchedAt) ?? nowSeconds(),
71
+ source: normalizeSource(record.source),
72
+ username: normalizeString(record.username),
73
+ organization: normalizeString(record.organization),
74
+ label: normalizeString(record.label),
75
+ limitSource: normalizeString(record.limitSource),
76
+ quotaReleaseMode: normalizeString(record.quotaReleaseMode),
77
+ quotaResetTime: normalizeString(record.quotaResetTime),
78
+ error: normalizeString(record.error) ?? options.error,
79
+ raw: isRecord(record.raw) ? record.raw : undefined,
80
+ };
81
+ }
82
+ export function normalizeExternalQuotaList(value, options) {
83
+ const record = isRecord(value) ? value : {};
84
+ const quotas = Array.isArray(record.quotas)
85
+ ? record.quotas.map((item) => normalizeExternalQuota(item, { backend: options.backend }))
86
+ : [];
87
+ const quotaByModel = {};
88
+ for (const quota of quotas) {
89
+ if (quota.model) {
90
+ quotaByModel[quota.model] = quota;
91
+ }
92
+ }
93
+ return {
94
+ backend: options.backend,
95
+ fetchedAt: normalizeNumber(record.fetchedAt) ?? nowSeconds(),
96
+ source: normalizeSource(record.source),
97
+ username: normalizeString(record.username),
98
+ organization: normalizeString(record.organization),
99
+ label: normalizeString(record.label),
100
+ count: normalizeNumber(record.count) ?? quotas.length,
101
+ quotas,
102
+ quotaByModel,
103
+ error: normalizeString(record.error) ?? options.error,
104
+ raw: isRecord(record.raw) ? record.raw : undefined,
105
+ };
106
+ }
107
+ function emptyQuota(error, options) {
108
+ return {
109
+ backend: options.backend,
110
+ model: options.model ?? "",
111
+ daily: { usedPercent: 0, remainingPercent: 0 },
112
+ fetchedAt: nowSeconds(),
113
+ source: "unknown",
114
+ error,
115
+ };
116
+ }
117
+ function emptyQuotaList(error, options) {
118
+ return {
119
+ backend: options.backend,
120
+ fetchedAt: nowSeconds(),
121
+ source: "unknown",
122
+ count: 0,
123
+ quotas: [],
124
+ quotaByModel: {},
125
+ error,
126
+ };
127
+ }
128
+ async function getExternalDescriptor(backend, configPath) {
129
+ return getExternalProviderDescriptor(backend, configPath ? { configFile: configPath } : {});
130
+ }
131
+ function providerOptions(opts) {
132
+ return {
133
+ backend: opts.backend,
134
+ model: "model" in opts ? opts.model : undefined,
135
+ forceRefresh: opts.forceRefresh,
136
+ ttlSeconds: opts.ttlSeconds,
137
+ timeoutMs: opts.timeoutMs,
138
+ configPath: opts.configPath,
139
+ };
140
+ }
141
+ export async function getExternalQuota(opts) {
142
+ const backend = normalizeString(opts.backend);
143
+ if (!backend) {
144
+ return emptyQuota("external provider backend is required", { backend: "", model: opts.model });
145
+ }
146
+ try {
147
+ const descriptor = await getExternalDescriptor(backend, opts.configPath);
148
+ if (!descriptor?.getQuota) {
149
+ return emptyQuota("external provider quota hook unavailable", { backend, model: opts.model });
150
+ }
151
+ const result = await descriptor.getQuota(providerOptions({ ...opts, backend }));
152
+ return normalizeExternalQuota(result, { backend: descriptor.backend || backend, model: opts.model });
153
+ }
154
+ catch (err) {
155
+ const message = err instanceof Error ? err.message : String(err);
156
+ return emptyQuota(message, { backend, model: opts.model });
157
+ }
158
+ }
159
+ export async function getExternalQuotaList(opts) {
160
+ const backend = normalizeString(opts.backend);
161
+ if (!backend) {
162
+ return emptyQuotaList("external provider backend is required", { backend: "" });
163
+ }
164
+ try {
165
+ const descriptor = await getExternalDescriptor(backend, opts.configPath);
166
+ if (!descriptor?.getQuotaList) {
167
+ return emptyQuotaList("external provider quota list hook unavailable", { backend });
168
+ }
169
+ const result = await descriptor.getQuotaList(providerOptions({ ...opts, backend }));
170
+ return normalizeExternalQuotaList(result, { backend: descriptor.backend || backend });
171
+ }
172
+ catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ return emptyQuotaList(message, { backend });
175
+ }
176
+ }
@@ -0,0 +1,5 @@
1
+ /** Convert fetch Headers to a case-insensitive plain map. */
2
+ export declare function headersToMap(h: Headers): Record<string, string>;
3
+ export declare function num(map: Record<string, string>, key: string): number | undefined;
4
+ export declare function str(map: Record<string, string>, key: string): string | undefined;
5
+ export declare function bool(map: Record<string, string>, key: string): boolean | undefined;
@@ -0,0 +1,29 @@
1
+ /** Convert fetch Headers to a case-insensitive plain map. */
2
+ export function headersToMap(h) {
3
+ const out = {};
4
+ h.forEach((value, key) => {
5
+ out[key.toLowerCase()] = value;
6
+ });
7
+ return out;
8
+ }
9
+ export function num(map, key) {
10
+ const v = map[key.toLowerCase()];
11
+ if (v === undefined || v === "")
12
+ return undefined;
13
+ const n = Number(v);
14
+ return Number.isFinite(n) ? n : undefined;
15
+ }
16
+ export function str(map, key) {
17
+ const v = map[key.toLowerCase()];
18
+ return v === undefined || v === "" ? undefined : v;
19
+ }
20
+ export function bool(map, key) {
21
+ const v = map[key.toLowerCase()];
22
+ if (v === undefined)
23
+ return undefined;
24
+ if (/^true$/i.test(v))
25
+ return true;
26
+ if (/^false$/i.test(v))
27
+ return false;
28
+ return undefined;
29
+ }
@@ -0,0 +1,24 @@
1
+ import type { KimiQuota } from "../types.js";
2
+ export interface KimiCredential {
3
+ access_token: string;
4
+ refresh_token: string;
5
+ expires_at: number;
6
+ scope?: string;
7
+ token_type?: string;
8
+ }
9
+ export interface GetKimiQuotaOptions {
10
+ credentialPath?: string;
11
+ ttlSeconds?: number;
12
+ forceRefresh?: boolean;
13
+ timeoutMs?: number;
14
+ cacheDir?: string;
15
+ /** Override the global fetch (test-only). */
16
+ fetcher?: typeof fetch;
17
+ }
18
+ export declare function getKimiQuota(opts?: GetKimiQuotaOptions): Promise<KimiQuota>;
19
+ /** Exported for tests. */
20
+ export declare function refreshKimiToken(cred: KimiCredential, credentialPath: string, timeoutMs: number, doFetch?: typeof fetch): Promise<KimiCredential>;
21
+ /** Exported for tests. */
22
+ export declare function parseUsagePayload(payload: any): KimiQuota;
23
+ /** Exported for tests. */
24
+ export declare function parseResetTime(value: any): number | undefined;
@@ -0,0 +1,230 @@
1
+ import { readFile, writeFile, rename } from "node:fs/promises";
2
+ import { DEFAULT_KIMI_CREDENTIAL } from "../paths.js";
3
+ import { cacheFile, fingerprintKey, isFresh, readCache, writeCache } from "./cache.js";
4
+ const USAGE_URL = "https://api.kimi.com/coding/v1/usages";
5
+ const OAUTH_TOKEN_URL = "https://auth.kimi.com/api/oauth/token";
6
+ const KIMI_CODE_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
7
+ const DEFAULT_TTL = 60;
8
+ const DEFAULT_TIMEOUT_MS = 15_000;
9
+ const REFRESH_LEEWAY_SECONDS = 60;
10
+ export async function getKimiQuota(opts = {}) {
11
+ const credentialPath = opts.credentialPath ?? DEFAULT_KIMI_CREDENTIAL;
12
+ const ttl = opts.ttlSeconds ?? DEFAULT_TTL;
13
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
14
+ const doFetch = opts.fetcher ?? fetch;
15
+ let cred;
16
+ try {
17
+ cred = JSON.parse(await readFile(credentialPath, "utf8"));
18
+ }
19
+ catch (err) {
20
+ return emptyQuota("unknown", `kimi credential not readable: ${err?.message ?? err}`);
21
+ }
22
+ if (!cred.access_token) {
23
+ return emptyQuota("unknown", "kimi credential missing access_token");
24
+ }
25
+ const fp = fingerprintKey(["kimi", cred.access_token.slice(-32)]);
26
+ const file = cacheFile("kimi", fp, opts.cacheDir);
27
+ if (!opts.forceRefresh && ttl > 0) {
28
+ const cached = await readCache(file);
29
+ if (isFresh(cached, ttl) && cached) {
30
+ return { ...cached.value, source: "cached" };
31
+ }
32
+ }
33
+ // Refresh proactively if access_token is expired or about to.
34
+ const nowSec = Math.floor(Date.now() / 1000);
35
+ if (cred.expires_at && cred.expires_at - nowSec < REFRESH_LEEWAY_SECONDS) {
36
+ try {
37
+ cred = await refreshKimiToken(cred, credentialPath, timeoutMs, doFetch);
38
+ }
39
+ catch (err) {
40
+ return await fallbackFromCache(file, ttl, `kimi token refresh failed: ${err?.message ?? err}`);
41
+ }
42
+ }
43
+ let res = await fetchUsage(cred.access_token, timeoutMs, doFetch);
44
+ if (res.status === 401 || res.status === 403) {
45
+ // Token may have been invalidated mid-flight; try one refresh + retry.
46
+ try {
47
+ cred = await refreshKimiToken(cred, credentialPath, timeoutMs, doFetch);
48
+ res = await fetchUsage(cred.access_token, timeoutMs, doFetch);
49
+ }
50
+ catch (err) {
51
+ return await fallbackFromCache(file, ttl, `kimi auth failed: ${err?.message ?? err}`);
52
+ }
53
+ }
54
+ if (!res.ok) {
55
+ return await fallbackFromCache(file, ttl, `kimi usage HTTP ${res.status}`);
56
+ }
57
+ const fresh = parseUsagePayload(res.payload);
58
+ await writeCache(file, fresh);
59
+ return fresh;
60
+ }
61
+ async function fetchUsage(token, timeoutMs, doFetch) {
62
+ const controller = new AbortController();
63
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
64
+ try {
65
+ const r = await doFetch(USAGE_URL, {
66
+ method: "GET",
67
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
68
+ signal: controller.signal,
69
+ });
70
+ let payload = null;
71
+ try {
72
+ payload = await r.json();
73
+ }
74
+ catch {
75
+ // ignore body parse errors
76
+ }
77
+ return { ok: r.ok, status: r.status, payload };
78
+ }
79
+ finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+ /** Exported for tests. */
84
+ export async function refreshKimiToken(cred, credentialPath, timeoutMs, doFetch = fetch) {
85
+ if (!cred.refresh_token)
86
+ throw new Error("missing refresh_token");
87
+ const controller = new AbortController();
88
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
89
+ try {
90
+ const body = new URLSearchParams({
91
+ client_id: KIMI_CODE_CLIENT_ID,
92
+ grant_type: "refresh_token",
93
+ refresh_token: cred.refresh_token,
94
+ });
95
+ const r = await doFetch(OAUTH_TOKEN_URL, {
96
+ method: "POST",
97
+ headers: {
98
+ "Content-Type": "application/x-www-form-urlencoded",
99
+ Accept: "application/json",
100
+ },
101
+ body: body.toString(),
102
+ signal: controller.signal,
103
+ });
104
+ if (!r.ok) {
105
+ const text = await r.text().catch(() => "");
106
+ throw new Error(`HTTP ${r.status}: ${text.slice(0, 200)}`);
107
+ }
108
+ const data = (await r.json());
109
+ const next = {
110
+ access_token: String(data.access_token ?? ""),
111
+ refresh_token: String(data.refresh_token ?? cred.refresh_token),
112
+ expires_at: typeof data.expires_at === "number"
113
+ ? data.expires_at
114
+ : Math.floor(Date.now() / 1000) + (data.expires_in ?? 0),
115
+ scope: data.scope ?? cred.scope,
116
+ token_type: data.token_type ?? cred.token_type,
117
+ };
118
+ if (!next.access_token)
119
+ throw new Error("refresh response missing access_token");
120
+ await persistCredential(credentialPath, next);
121
+ return next;
122
+ }
123
+ finally {
124
+ clearTimeout(timer);
125
+ }
126
+ }
127
+ async function persistCredential(path, cred) {
128
+ const tmp = `${path}.tmp`;
129
+ await writeFile(tmp, JSON.stringify(cred), { mode: 0o600 });
130
+ await rename(tmp, path);
131
+ }
132
+ /** Exported for tests. */
133
+ export function parseUsagePayload(payload) {
134
+ const user = payload?.user ?? {};
135
+ const usage = payload?.user && payload?.usage ? payload.usage : payload?.usage ?? {};
136
+ const limits = Array.isArray(payload?.limits) ? payload.limits : [];
137
+ const fiveHourEntry = limits.find((entry) => {
138
+ const w = entry?.window;
139
+ if (!w)
140
+ return false;
141
+ if (w.timeUnit === "TIME_UNIT_MINUTE" && Number(w.duration) === 300)
142
+ return true;
143
+ if (w.timeUnit === "TIME_UNIT_HOUR" && Number(w.duration) === 5)
144
+ return true;
145
+ return false;
146
+ });
147
+ const fiveHourDetail = fiveHourEntry?.detail ?? fiveHourEntry ?? {};
148
+ return {
149
+ tool: "kimi",
150
+ userId: typeof user.userId === "string" ? user.userId : undefined,
151
+ region: typeof user.region === "string" ? user.region : undefined,
152
+ membership: typeof user?.membership?.level === "string" ? user.membership.level : undefined,
153
+ fiveHour: windowFromKimi(fiveHourDetail, 300),
154
+ weekly: windowFromKimi(usage, 10080),
155
+ parallelLimit: toInt(payload?.parallel?.limit),
156
+ fetchedAt: Math.floor(Date.now() / 1000),
157
+ source: "fresh",
158
+ raw: payload && typeof payload === "object" ? payload : undefined,
159
+ };
160
+ }
161
+ function windowFromKimi(entry, windowMinutes) {
162
+ const limit = toInt(entry?.limit);
163
+ let used = toInt(entry?.used);
164
+ let remaining = toInt(entry?.remaining);
165
+ if (limit !== undefined) {
166
+ if (used === undefined && remaining !== undefined)
167
+ used = limit - remaining;
168
+ if (remaining === undefined && used !== undefined)
169
+ remaining = limit - used;
170
+ }
171
+ const usedPercent = limit && limit > 0 && used !== undefined ? (used / limit) * 100 : 0;
172
+ const remainingPercent = limit && limit > 0 && remaining !== undefined
173
+ ? (remaining / limit) * 100
174
+ : Math.max(0, 100 - usedPercent);
175
+ const resetAt = parseResetTime(entry?.resetTime ?? entry?.reset_time ?? entry?.resetAt);
176
+ return {
177
+ usedPercent: round1(usedPercent),
178
+ remainingPercent: round1(remainingPercent),
179
+ resetAt,
180
+ windowMinutes,
181
+ limit,
182
+ used,
183
+ remaining,
184
+ };
185
+ }
186
+ /** Exported for tests. */
187
+ export function parseResetTime(value) {
188
+ if (typeof value !== "string" || !value)
189
+ return undefined;
190
+ // Trim sub-microsecond fraction so Date can parse it (Kimi sends nanoseconds).
191
+ let v = value;
192
+ if (v.endsWith("Z") && v.includes(".")) {
193
+ const [base, frac] = v.slice(0, -1).split(".");
194
+ v = `${base}.${frac.slice(0, 6)}Z`;
195
+ }
196
+ const t = Date.parse(v);
197
+ if (Number.isNaN(t))
198
+ return undefined;
199
+ return Math.floor(t / 1000);
200
+ }
201
+ function toInt(value) {
202
+ if (value === null || value === undefined)
203
+ return undefined;
204
+ const n = typeof value === "number" ? value : Number(value);
205
+ return Number.isFinite(n) ? Math.trunc(n) : undefined;
206
+ }
207
+ function round1(n) {
208
+ return Math.round(n * 10) / 10;
209
+ }
210
+ async function fallbackFromCache(file, ttl, error) {
211
+ const cached = await readCache(file);
212
+ if (cached) {
213
+ return {
214
+ ...cached.value,
215
+ source: isFresh(cached, ttl) ? "cached" : "stale",
216
+ error,
217
+ };
218
+ }
219
+ return emptyQuota("unknown", error);
220
+ }
221
+ function emptyQuota(source, error) {
222
+ return {
223
+ tool: "kimi",
224
+ source,
225
+ error,
226
+ fetchedAt: Math.floor(Date.now() / 1000),
227
+ fiveHour: { usedPercent: 0, remainingPercent: 0 },
228
+ weekly: { usedPercent: 0, remainingPercent: 0 },
229
+ };
230
+ }
@@ -0,0 +1,166 @@
1
+ export type Tool = "codex" | "claude" | "kimi" | "copilot";
2
+ export type QuotaSource = "fresh" | "cached" | "stale" | "unknown";
3
+ export interface InstallStatus {
4
+ installed: boolean;
5
+ path?: string;
6
+ version?: string;
7
+ error?: string;
8
+ }
9
+ export interface NetworkStatus {
10
+ reachable: boolean;
11
+ latencyMs?: number;
12
+ httpStatus?: number;
13
+ error?: string;
14
+ endpoint: string;
15
+ }
16
+ export interface QuotaWindow {
17
+ /** Percentage of the window already used, 0-100. */
18
+ usedPercent: number;
19
+ /** Percentage of the window remaining, 0-100. */
20
+ remainingPercent: number;
21
+ /** Seconds until the window resets. */
22
+ resetAfterSeconds?: number;
23
+ /** Unix timestamp (seconds) at which the window resets. */
24
+ resetAt?: number;
25
+ /** Calendar date when the provider only exposes day-level reset semantics. */
26
+ resetOnDate?: string;
27
+ /** Status label from the provider (allowed/warning/blocked), if any. */
28
+ status?: string;
29
+ /** Window length in minutes, if known (300 = 5h, 10080 = 7d). */
30
+ windowMinutes?: number;
31
+ /** Absolute counts when the provider exposes them (Kimi). */
32
+ limit?: number;
33
+ used?: number;
34
+ remaining?: number;
35
+ }
36
+ export interface CodexQuota {
37
+ tool: "codex";
38
+ plan?: string;
39
+ activeLimit?: string;
40
+ fiveHour: QuotaWindow;
41
+ weekly: QuotaWindow;
42
+ credits?: {
43
+ hasCredits: boolean;
44
+ balance?: string;
45
+ unlimited: boolean;
46
+ };
47
+ fetchedAt: number;
48
+ source: QuotaSource;
49
+ email?: string;
50
+ accountId?: string;
51
+ raw?: Record<string, string>;
52
+ error?: string;
53
+ }
54
+ export interface ClaudeQuota {
55
+ tool: "claude";
56
+ plan?: string;
57
+ overallStatus?: string;
58
+ fiveHour: QuotaWindow;
59
+ weekly: QuotaWindow;
60
+ weeklySonnet?: QuotaWindow;
61
+ overage?: {
62
+ status?: string;
63
+ disabledReason?: string;
64
+ };
65
+ fetchedAt: number;
66
+ source: QuotaSource;
67
+ raw?: Record<string, string>;
68
+ error?: string;
69
+ }
70
+ export interface KimiQuota {
71
+ tool: "kimi";
72
+ userId?: string;
73
+ region?: string;
74
+ membership?: string;
75
+ fiveHour: QuotaWindow;
76
+ weekly: QuotaWindow;
77
+ parallelLimit?: number;
78
+ fetchedAt: number;
79
+ source: QuotaSource;
80
+ raw?: Record<string, unknown>;
81
+ error?: string;
82
+ }
83
+ export interface CopilotQuotaSnapshot {
84
+ entitlementRequests: number;
85
+ usedRequests: number;
86
+ remainingPercentage: number;
87
+ overage: number;
88
+ overageAllowedWithExhaustedQuota: boolean;
89
+ resetDate?: string;
90
+ isUnlimitedEntitlement?: boolean;
91
+ usageAllowedWithExhaustedQuota?: boolean;
92
+ }
93
+ export interface CopilotQuota {
94
+ tool: "copilot";
95
+ /** Best single quota window to show when callers do not care about quota type. */
96
+ primary?: QuotaWindow;
97
+ chat?: QuotaWindow;
98
+ completions?: QuotaWindow;
99
+ premiumInteractions?: QuotaWindow;
100
+ login?: string;
101
+ authType?: string;
102
+ host?: string;
103
+ loginSource?: "sdk" | "github_token";
104
+ snapshots: Record<string, CopilotQuotaSnapshot>;
105
+ fetchedAt: number;
106
+ source: QuotaSource;
107
+ raw?: Record<string, unknown>;
108
+ error?: string;
109
+ }
110
+ export interface ExternalQuota {
111
+ backend?: string;
112
+ model: string;
113
+ daily: QuotaWindow;
114
+ fetchedAt: number;
115
+ source: QuotaSource;
116
+ username?: string;
117
+ organization?: string;
118
+ label?: string;
119
+ limitSource?: string;
120
+ quotaReleaseMode?: string;
121
+ quotaResetTime?: string;
122
+ raw?: Record<string, unknown>;
123
+ error?: string;
124
+ }
125
+ export interface ExternalQuotaList {
126
+ backend?: string;
127
+ fetchedAt: number;
128
+ source: QuotaSource;
129
+ username?: string;
130
+ organization?: string;
131
+ label?: string;
132
+ count: number;
133
+ quotas: ExternalQuota[];
134
+ quotaByModel: Record<string, ExternalQuota>;
135
+ raw?: Record<string, unknown>;
136
+ error?: string;
137
+ }
138
+ export interface CodexAccount {
139
+ /** Short label derived from file name (strip `auth_` prefix and `.json` suffix). */
140
+ name: string;
141
+ /** Absolute path to the auth.json file. */
142
+ path: string;
143
+ email?: string;
144
+ accountId?: string;
145
+ planType?: string;
146
+ lastRefresh?: string;
147
+ isCurrent: boolean;
148
+ /**
149
+ * Last-known quota snapshot for this account read from the daemon's on-disk
150
+ * cache. Populated by `list_accounts` so web clients can restore an
151
+ * inactive account's bars across page refreshes without a network call.
152
+ * Absent when the daemon has never successfully fetched quota for this
153
+ * account.
154
+ */
155
+ cachedQuota?: CodexQuota;
156
+ }
157
+ export interface SwitchResult {
158
+ previousName?: string;
159
+ newName: string;
160
+ backupPath: string;
161
+ }
162
+ export interface AiManagerConfig {
163
+ codex: {
164
+ authJson: string[];
165
+ };
166
+ }
@@ -0,0 +1 @@
1
+ export {};