@zhangferry-dev/tokendash 1.6.1 → 1.6.2
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 +146 -83
- package/dist/client/assets/index-Bw503sNp.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/daemon.cjs +3306 -0
- package/dist/daemon.cjs.map +7 -0
- package/dist/electron-server.cjs +1019 -28
- package/dist/electron-server.cjs.map +4 -4
- package/dist/server/ccusage.d.ts +7 -0
- package/dist/server/ccusage.js +69 -0
- package/dist/server/daemon.d.ts +12 -0
- package/dist/server/daemon.js +176 -0
- package/dist/server/index.js +22 -11
- package/dist/server/insightsCalculator.d.ts +15 -0
- package/dist/server/insightsCalculator.js +276 -0
- package/dist/server/quota/adapter.d.ts +47 -0
- package/dist/server/quota/adapter.js +41 -0
- package/dist/server/quota/adapters/claude.d.ts +2 -0
- package/dist/server/quota/adapters/claude.js +124 -0
- package/dist/server/quota/adapters/codex.d.ts +2 -0
- package/dist/server/quota/adapters/codex.js +188 -0
- package/dist/server/quota/adapters/glm.d.ts +2 -0
- package/dist/server/quota/adapters/glm.js +133 -0
- package/dist/server/quota/adapters/kimi.d.ts +2 -0
- package/dist/server/quota/adapters/kimi.js +184 -0
- package/dist/server/quota/adapters/minimax.d.ts +2 -0
- package/dist/server/quota/adapters/minimax.js +77 -0
- package/dist/server/quota/cache.d.ts +20 -0
- package/dist/server/quota/cache.js +44 -0
- package/dist/server/quota/credentialsFile.d.ts +13 -0
- package/dist/server/quota/credentialsFile.js +23 -0
- package/dist/server/quota/helpers.d.ts +39 -0
- package/dist/server/quota/helpers.js +93 -0
- package/dist/server/quota/index.d.ts +5 -0
- package/dist/server/quota/index.js +23 -0
- package/dist/server/quota/quotaService.d.ts +37 -0
- package/dist/server/quota/quotaService.js +141 -0
- package/dist/server/quota/schemas.d.ts +358 -0
- package/dist/server/quota/schemas.js +53 -0
- package/dist/server/quota/types.d.ts +65 -0
- package/dist/server/quota/types.js +10 -0
- package/dist/server/routes/api.js +15 -0
- package/dist/server/routes/insights.d.ts +2 -0
- package/dist/server/routes/insights.js +155 -0
- package/package.json +6 -10
- package/resources/entitlements.mac.plist +10 -0
- package/resources/icon-1024.png +0 -0
- package/resources/icon.icns +0 -0
- package/resources/icon.png +0 -0
- package/resources/product_menu.png +0 -0
- package/resources/readme-hero.png +0 -0
- package/dist/client/assets/index-_yA9tOzZ.css +0 -1
- package/electron/main.cjs +0 -516
- package/electron/npmSync.cjs +0 -62
- package/electron/preload.cjs +0 -36
- package/electron/serverReuse.cjs +0 -59
- package/electron/trayBadge.cjs +0 -27
- package/electron/trayHelper +0 -0
- package/electron/trayHelper.swift +0 -152
- package/electron/updateService.cjs +0 -220
- package/electron-builder.yml +0 -20
- /package/dist/client/assets/{index-CY4G_b0x.js → index-C913wKtU.js} +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schemas for the normalized quota contract.
|
|
4
|
+
*
|
|
5
|
+
* Provider adapter OUTPUT is validated here before it leaves the service,
|
|
6
|
+
* so the API response (and the Swift client) can always trust the shape.
|
|
7
|
+
* Validation is tolerant of additive upstream fields — adapters strip those —
|
|
8
|
+
* but strict on fields used in calculations.
|
|
9
|
+
*/
|
|
10
|
+
export declare const QuotaWindowSchema: z.ZodObject<{
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
label: z.ZodString;
|
|
13
|
+
usedPercent: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
remainingPercent: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
used: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
durationMins: z.ZodOptional<z.ZodNumber>;
|
|
18
|
+
resetsAt: z.ZodOptional<z.ZodString>;
|
|
19
|
+
isUnlimited: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
modelName: z.ZodOptional<z.ZodString>;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
usedPercent: number;
|
|
25
|
+
remainingPercent: number;
|
|
26
|
+
modelName?: string | undefined;
|
|
27
|
+
used?: number | undefined;
|
|
28
|
+
limit?: number | undefined;
|
|
29
|
+
durationMins?: number | undefined;
|
|
30
|
+
resetsAt?: string | undefined;
|
|
31
|
+
isUnlimited?: boolean | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
modelName?: string | undefined;
|
|
36
|
+
usedPercent?: number | undefined;
|
|
37
|
+
remainingPercent?: number | undefined;
|
|
38
|
+
used?: number | undefined;
|
|
39
|
+
limit?: number | undefined;
|
|
40
|
+
durationMins?: number | undefined;
|
|
41
|
+
resetsAt?: string | undefined;
|
|
42
|
+
isUnlimited?: boolean | undefined;
|
|
43
|
+
}>;
|
|
44
|
+
export declare const QuotaProviderStatusSchema: z.ZodObject<{
|
|
45
|
+
state: z.ZodEnum<["ok", "auth_failed", "not_configured", "upstream_unavailable", "rate_limited", "malformed_response", "timed_out", "error"]>;
|
|
46
|
+
message: z.ZodOptional<z.ZodString>;
|
|
47
|
+
category: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, "strip", z.ZodTypeAny, {
|
|
49
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
50
|
+
message?: string | undefined;
|
|
51
|
+
category?: string | undefined;
|
|
52
|
+
}, {
|
|
53
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
54
|
+
message?: string | undefined;
|
|
55
|
+
category?: string | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export declare const QuotaSnapshotSchema: z.ZodObject<{
|
|
58
|
+
provider: z.ZodEnum<["codex", "claude", "glm", "minimax", "kimi"]>;
|
|
59
|
+
displayName: z.ZodString;
|
|
60
|
+
planName: z.ZodOptional<z.ZodString>;
|
|
61
|
+
fetchedAt: z.ZodString;
|
|
62
|
+
freshness: z.ZodEnum<["live", "cached", "stale"]>;
|
|
63
|
+
windows: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
label: z.ZodString;
|
|
66
|
+
usedPercent: z.ZodDefault<z.ZodNumber>;
|
|
67
|
+
remainingPercent: z.ZodDefault<z.ZodNumber>;
|
|
68
|
+
used: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
durationMins: z.ZodOptional<z.ZodNumber>;
|
|
71
|
+
resetsAt: z.ZodOptional<z.ZodString>;
|
|
72
|
+
isUnlimited: z.ZodOptional<z.ZodBoolean>;
|
|
73
|
+
modelName: z.ZodOptional<z.ZodString>;
|
|
74
|
+
}, "strip", z.ZodTypeAny, {
|
|
75
|
+
id: string;
|
|
76
|
+
label: string;
|
|
77
|
+
usedPercent: number;
|
|
78
|
+
remainingPercent: number;
|
|
79
|
+
modelName?: string | undefined;
|
|
80
|
+
used?: number | undefined;
|
|
81
|
+
limit?: number | undefined;
|
|
82
|
+
durationMins?: number | undefined;
|
|
83
|
+
resetsAt?: string | undefined;
|
|
84
|
+
isUnlimited?: boolean | undefined;
|
|
85
|
+
}, {
|
|
86
|
+
id: string;
|
|
87
|
+
label: string;
|
|
88
|
+
modelName?: string | undefined;
|
|
89
|
+
usedPercent?: number | undefined;
|
|
90
|
+
remainingPercent?: number | undefined;
|
|
91
|
+
used?: number | undefined;
|
|
92
|
+
limit?: number | undefined;
|
|
93
|
+
durationMins?: number | undefined;
|
|
94
|
+
resetsAt?: string | undefined;
|
|
95
|
+
isUnlimited?: boolean | undefined;
|
|
96
|
+
}>, "many">>;
|
|
97
|
+
status: z.ZodObject<{
|
|
98
|
+
state: z.ZodEnum<["ok", "auth_failed", "not_configured", "upstream_unavailable", "rate_limited", "malformed_response", "timed_out", "error"]>;
|
|
99
|
+
message: z.ZodOptional<z.ZodString>;
|
|
100
|
+
category: z.ZodOptional<z.ZodString>;
|
|
101
|
+
}, "strip", z.ZodTypeAny, {
|
|
102
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
103
|
+
message?: string | undefined;
|
|
104
|
+
category?: string | undefined;
|
|
105
|
+
}, {
|
|
106
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
107
|
+
message?: string | undefined;
|
|
108
|
+
category?: string | undefined;
|
|
109
|
+
}>;
|
|
110
|
+
}, "strip", z.ZodTypeAny, {
|
|
111
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
112
|
+
status: {
|
|
113
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
114
|
+
message?: string | undefined;
|
|
115
|
+
category?: string | undefined;
|
|
116
|
+
};
|
|
117
|
+
windows: {
|
|
118
|
+
id: string;
|
|
119
|
+
label: string;
|
|
120
|
+
usedPercent: number;
|
|
121
|
+
remainingPercent: number;
|
|
122
|
+
modelName?: string | undefined;
|
|
123
|
+
used?: number | undefined;
|
|
124
|
+
limit?: number | undefined;
|
|
125
|
+
durationMins?: number | undefined;
|
|
126
|
+
resetsAt?: string | undefined;
|
|
127
|
+
isUnlimited?: boolean | undefined;
|
|
128
|
+
}[];
|
|
129
|
+
displayName: string;
|
|
130
|
+
fetchedAt: string;
|
|
131
|
+
freshness: "live" | "cached" | "stale";
|
|
132
|
+
planName?: string | undefined;
|
|
133
|
+
}, {
|
|
134
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
135
|
+
status: {
|
|
136
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
137
|
+
message?: string | undefined;
|
|
138
|
+
category?: string | undefined;
|
|
139
|
+
};
|
|
140
|
+
displayName: string;
|
|
141
|
+
fetchedAt: string;
|
|
142
|
+
freshness: "live" | "cached" | "stale";
|
|
143
|
+
windows?: {
|
|
144
|
+
id: string;
|
|
145
|
+
label: string;
|
|
146
|
+
modelName?: string | undefined;
|
|
147
|
+
usedPercent?: number | undefined;
|
|
148
|
+
remainingPercent?: number | undefined;
|
|
149
|
+
used?: number | undefined;
|
|
150
|
+
limit?: number | undefined;
|
|
151
|
+
durationMins?: number | undefined;
|
|
152
|
+
resetsAt?: string | undefined;
|
|
153
|
+
isUnlimited?: boolean | undefined;
|
|
154
|
+
}[] | undefined;
|
|
155
|
+
planName?: string | undefined;
|
|
156
|
+
}>;
|
|
157
|
+
export declare const QuotaResponseSchema: z.ZodObject<{
|
|
158
|
+
providers: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
159
|
+
provider: z.ZodEnum<["codex", "claude", "glm", "minimax", "kimi"]>;
|
|
160
|
+
displayName: z.ZodString;
|
|
161
|
+
planName: z.ZodOptional<z.ZodString>;
|
|
162
|
+
fetchedAt: z.ZodString;
|
|
163
|
+
freshness: z.ZodEnum<["live", "cached", "stale"]>;
|
|
164
|
+
windows: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
165
|
+
id: z.ZodString;
|
|
166
|
+
label: z.ZodString;
|
|
167
|
+
usedPercent: z.ZodDefault<z.ZodNumber>;
|
|
168
|
+
remainingPercent: z.ZodDefault<z.ZodNumber>;
|
|
169
|
+
used: z.ZodOptional<z.ZodNumber>;
|
|
170
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
171
|
+
durationMins: z.ZodOptional<z.ZodNumber>;
|
|
172
|
+
resetsAt: z.ZodOptional<z.ZodString>;
|
|
173
|
+
isUnlimited: z.ZodOptional<z.ZodBoolean>;
|
|
174
|
+
modelName: z.ZodOptional<z.ZodString>;
|
|
175
|
+
}, "strip", z.ZodTypeAny, {
|
|
176
|
+
id: string;
|
|
177
|
+
label: string;
|
|
178
|
+
usedPercent: number;
|
|
179
|
+
remainingPercent: number;
|
|
180
|
+
modelName?: string | undefined;
|
|
181
|
+
used?: number | undefined;
|
|
182
|
+
limit?: number | undefined;
|
|
183
|
+
durationMins?: number | undefined;
|
|
184
|
+
resetsAt?: string | undefined;
|
|
185
|
+
isUnlimited?: boolean | undefined;
|
|
186
|
+
}, {
|
|
187
|
+
id: string;
|
|
188
|
+
label: string;
|
|
189
|
+
modelName?: string | undefined;
|
|
190
|
+
usedPercent?: number | undefined;
|
|
191
|
+
remainingPercent?: number | undefined;
|
|
192
|
+
used?: number | undefined;
|
|
193
|
+
limit?: number | undefined;
|
|
194
|
+
durationMins?: number | undefined;
|
|
195
|
+
resetsAt?: string | undefined;
|
|
196
|
+
isUnlimited?: boolean | undefined;
|
|
197
|
+
}>, "many">>;
|
|
198
|
+
status: z.ZodObject<{
|
|
199
|
+
state: z.ZodEnum<["ok", "auth_failed", "not_configured", "upstream_unavailable", "rate_limited", "malformed_response", "timed_out", "error"]>;
|
|
200
|
+
message: z.ZodOptional<z.ZodString>;
|
|
201
|
+
category: z.ZodOptional<z.ZodString>;
|
|
202
|
+
}, "strip", z.ZodTypeAny, {
|
|
203
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
204
|
+
message?: string | undefined;
|
|
205
|
+
category?: string | undefined;
|
|
206
|
+
}, {
|
|
207
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
208
|
+
message?: string | undefined;
|
|
209
|
+
category?: string | undefined;
|
|
210
|
+
}>;
|
|
211
|
+
}, "strip", z.ZodTypeAny, {
|
|
212
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
213
|
+
status: {
|
|
214
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
215
|
+
message?: string | undefined;
|
|
216
|
+
category?: string | undefined;
|
|
217
|
+
};
|
|
218
|
+
windows: {
|
|
219
|
+
id: string;
|
|
220
|
+
label: string;
|
|
221
|
+
usedPercent: number;
|
|
222
|
+
remainingPercent: number;
|
|
223
|
+
modelName?: string | undefined;
|
|
224
|
+
used?: number | undefined;
|
|
225
|
+
limit?: number | undefined;
|
|
226
|
+
durationMins?: number | undefined;
|
|
227
|
+
resetsAt?: string | undefined;
|
|
228
|
+
isUnlimited?: boolean | undefined;
|
|
229
|
+
}[];
|
|
230
|
+
displayName: string;
|
|
231
|
+
fetchedAt: string;
|
|
232
|
+
freshness: "live" | "cached" | "stale";
|
|
233
|
+
planName?: string | undefined;
|
|
234
|
+
}, {
|
|
235
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
236
|
+
status: {
|
|
237
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
238
|
+
message?: string | undefined;
|
|
239
|
+
category?: string | undefined;
|
|
240
|
+
};
|
|
241
|
+
displayName: string;
|
|
242
|
+
fetchedAt: string;
|
|
243
|
+
freshness: "live" | "cached" | "stale";
|
|
244
|
+
windows?: {
|
|
245
|
+
id: string;
|
|
246
|
+
label: string;
|
|
247
|
+
modelName?: string | undefined;
|
|
248
|
+
usedPercent?: number | undefined;
|
|
249
|
+
remainingPercent?: number | undefined;
|
|
250
|
+
used?: number | undefined;
|
|
251
|
+
limit?: number | undefined;
|
|
252
|
+
durationMins?: number | undefined;
|
|
253
|
+
resetsAt?: string | undefined;
|
|
254
|
+
isUnlimited?: boolean | undefined;
|
|
255
|
+
}[] | undefined;
|
|
256
|
+
planName?: string | undefined;
|
|
257
|
+
}>, "many">>;
|
|
258
|
+
}, "strip", z.ZodTypeAny, {
|
|
259
|
+
providers: {
|
|
260
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
261
|
+
status: {
|
|
262
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
263
|
+
message?: string | undefined;
|
|
264
|
+
category?: string | undefined;
|
|
265
|
+
};
|
|
266
|
+
windows: {
|
|
267
|
+
id: string;
|
|
268
|
+
label: string;
|
|
269
|
+
usedPercent: number;
|
|
270
|
+
remainingPercent: number;
|
|
271
|
+
modelName?: string | undefined;
|
|
272
|
+
used?: number | undefined;
|
|
273
|
+
limit?: number | undefined;
|
|
274
|
+
durationMins?: number | undefined;
|
|
275
|
+
resetsAt?: string | undefined;
|
|
276
|
+
isUnlimited?: boolean | undefined;
|
|
277
|
+
}[];
|
|
278
|
+
displayName: string;
|
|
279
|
+
fetchedAt: string;
|
|
280
|
+
freshness: "live" | "cached" | "stale";
|
|
281
|
+
planName?: string | undefined;
|
|
282
|
+
}[];
|
|
283
|
+
}, {
|
|
284
|
+
providers?: {
|
|
285
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
286
|
+
status: {
|
|
287
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
288
|
+
message?: string | undefined;
|
|
289
|
+
category?: string | undefined;
|
|
290
|
+
};
|
|
291
|
+
displayName: string;
|
|
292
|
+
fetchedAt: string;
|
|
293
|
+
freshness: "live" | "cached" | "stale";
|
|
294
|
+
windows?: {
|
|
295
|
+
id: string;
|
|
296
|
+
label: string;
|
|
297
|
+
modelName?: string | undefined;
|
|
298
|
+
usedPercent?: number | undefined;
|
|
299
|
+
remainingPercent?: number | undefined;
|
|
300
|
+
used?: number | undefined;
|
|
301
|
+
limit?: number | undefined;
|
|
302
|
+
durationMins?: number | undefined;
|
|
303
|
+
resetsAt?: string | undefined;
|
|
304
|
+
isUnlimited?: boolean | undefined;
|
|
305
|
+
}[] | undefined;
|
|
306
|
+
planName?: string | undefined;
|
|
307
|
+
}[] | undefined;
|
|
308
|
+
}>;
|
|
309
|
+
export declare function validateQuotaSnapshot(data: unknown): {
|
|
310
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
311
|
+
status: {
|
|
312
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
313
|
+
message?: string | undefined;
|
|
314
|
+
category?: string | undefined;
|
|
315
|
+
};
|
|
316
|
+
windows: {
|
|
317
|
+
id: string;
|
|
318
|
+
label: string;
|
|
319
|
+
usedPercent: number;
|
|
320
|
+
remainingPercent: number;
|
|
321
|
+
modelName?: string | undefined;
|
|
322
|
+
used?: number | undefined;
|
|
323
|
+
limit?: number | undefined;
|
|
324
|
+
durationMins?: number | undefined;
|
|
325
|
+
resetsAt?: string | undefined;
|
|
326
|
+
isUnlimited?: boolean | undefined;
|
|
327
|
+
}[];
|
|
328
|
+
displayName: string;
|
|
329
|
+
fetchedAt: string;
|
|
330
|
+
freshness: "live" | "cached" | "stale";
|
|
331
|
+
planName?: string | undefined;
|
|
332
|
+
};
|
|
333
|
+
export declare function validateQuotaResponse(data: unknown): {
|
|
334
|
+
providers: {
|
|
335
|
+
provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
|
|
336
|
+
status: {
|
|
337
|
+
state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
|
|
338
|
+
message?: string | undefined;
|
|
339
|
+
category?: string | undefined;
|
|
340
|
+
};
|
|
341
|
+
windows: {
|
|
342
|
+
id: string;
|
|
343
|
+
label: string;
|
|
344
|
+
usedPercent: number;
|
|
345
|
+
remainingPercent: number;
|
|
346
|
+
modelName?: string | undefined;
|
|
347
|
+
used?: number | undefined;
|
|
348
|
+
limit?: number | undefined;
|
|
349
|
+
durationMins?: number | undefined;
|
|
350
|
+
resetsAt?: string | undefined;
|
|
351
|
+
isUnlimited?: boolean | undefined;
|
|
352
|
+
}[];
|
|
353
|
+
displayName: string;
|
|
354
|
+
fetchedAt: string;
|
|
355
|
+
freshness: "live" | "cached" | "stale";
|
|
356
|
+
planName?: string | undefined;
|
|
357
|
+
}[];
|
|
358
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Zod schemas for the normalized quota contract.
|
|
4
|
+
*
|
|
5
|
+
* Provider adapter OUTPUT is validated here before it leaves the service,
|
|
6
|
+
* so the API response (and the Swift client) can always trust the shape.
|
|
7
|
+
* Validation is tolerant of additive upstream fields — adapters strip those —
|
|
8
|
+
* but strict on fields used in calculations.
|
|
9
|
+
*/
|
|
10
|
+
export const QuotaWindowSchema = z.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
label: z.string(),
|
|
13
|
+
usedPercent: z.number().min(0).max(100).default(0),
|
|
14
|
+
remainingPercent: z.number().min(0).max(100).default(0),
|
|
15
|
+
used: z.number().optional(),
|
|
16
|
+
limit: z.number().optional(),
|
|
17
|
+
durationMins: z.number().optional(),
|
|
18
|
+
resetsAt: z.string().optional(),
|
|
19
|
+
isUnlimited: z.boolean().optional(),
|
|
20
|
+
modelName: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
export const QuotaProviderStatusSchema = z.object({
|
|
23
|
+
state: z.enum([
|
|
24
|
+
'ok',
|
|
25
|
+
'auth_failed',
|
|
26
|
+
'not_configured',
|
|
27
|
+
'upstream_unavailable',
|
|
28
|
+
'rate_limited',
|
|
29
|
+
'malformed_response',
|
|
30
|
+
'timed_out',
|
|
31
|
+
'error',
|
|
32
|
+
]),
|
|
33
|
+
message: z.string().optional(),
|
|
34
|
+
category: z.string().optional(),
|
|
35
|
+
});
|
|
36
|
+
export const QuotaSnapshotSchema = z.object({
|
|
37
|
+
provider: z.enum(['codex', 'claude', 'glm', 'minimax', 'kimi']),
|
|
38
|
+
displayName: z.string(),
|
|
39
|
+
planName: z.string().optional(),
|
|
40
|
+
fetchedAt: z.string(),
|
|
41
|
+
freshness: z.enum(['live', 'cached', 'stale']),
|
|
42
|
+
windows: z.array(QuotaWindowSchema).default([]),
|
|
43
|
+
status: QuotaProviderStatusSchema,
|
|
44
|
+
});
|
|
45
|
+
export const QuotaResponseSchema = z.object({
|
|
46
|
+
providers: z.array(QuotaSnapshotSchema).default([]),
|
|
47
|
+
});
|
|
48
|
+
export function validateQuotaSnapshot(data) {
|
|
49
|
+
return QuotaSnapshotSchema.parse(data);
|
|
50
|
+
}
|
|
51
|
+
export function validateQuotaResponse(data) {
|
|
52
|
+
return QuotaResponseSchema.parse(data);
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized quota domain types.
|
|
3
|
+
*
|
|
4
|
+
* These are provider-neutral. Every provider adapter converts its own
|
|
5
|
+
* heterogeneous response shape into this contract so the rest of the app
|
|
6
|
+
* never sees provider-specific fields.
|
|
7
|
+
*
|
|
8
|
+
* Mirrored on the Swift side by TokenDashSwift/Sources/TokenDash/Models/APIModels.swift.
|
|
9
|
+
*/
|
|
10
|
+
export type QuotaProviderId = 'codex' | 'claude' | 'glm' | 'minimax' | 'kimi';
|
|
11
|
+
/** A normalized snapshot of one provider's current quota state. */
|
|
12
|
+
export interface QuotaSnapshot {
|
|
13
|
+
/** Stable provider id, e.g. "codex" | "claude" | "glm" | "minimax" | "kimi". */
|
|
14
|
+
provider: QuotaProviderId;
|
|
15
|
+
/** Human-facing name, e.g. "OpenAI Codex". */
|
|
16
|
+
displayName: string;
|
|
17
|
+
/** Subscription tier when known, e.g. "Pro" | "Plus" | "LEVEL_INTERMEDIATE". */
|
|
18
|
+
planName?: string;
|
|
19
|
+
/** ISO 8601 timestamp of the most recent successful fetch. */
|
|
20
|
+
fetchedAt: string;
|
|
21
|
+
/** "live" = fresh this cycle, "cached" = within ttl, "stale" = last good after a failure. */
|
|
22
|
+
freshness: 'live' | 'cached' | 'stale';
|
|
23
|
+
/** Independent quota windows. Never merged into one synthetic number. */
|
|
24
|
+
windows: QuotaWindow[];
|
|
25
|
+
/** Structured status — never carries secrets, only redacted messages. */
|
|
26
|
+
status: QuotaProviderStatus;
|
|
27
|
+
}
|
|
28
|
+
/** A single independent quota window (e.g. 5-hour, weekly, MCP-monthly). */
|
|
29
|
+
export interface QuotaWindow {
|
|
30
|
+
/** Stable per-snapshot id, e.g. "five_hour" | "weekly" | "codex_primary". */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Human label, e.g. "5-Hour Window" | "Weekly". */
|
|
33
|
+
label: string;
|
|
34
|
+
/** Consumed percentage 0-100. */
|
|
35
|
+
usedPercent: number;
|
|
36
|
+
/** Remaining percentage 0-100 (100 - usedPercent). */
|
|
37
|
+
remainingPercent: number;
|
|
38
|
+
/** Absolute used value when the provider reports one. */
|
|
39
|
+
used?: number;
|
|
40
|
+
/** Absolute limit value when the provider reports one. */
|
|
41
|
+
limit?: number;
|
|
42
|
+
/** Window length in minutes (300 = 5h, 10080 = 7d). */
|
|
43
|
+
durationMins?: number;
|
|
44
|
+
/** ISO 8601 timestamp when this window resets. */
|
|
45
|
+
resetsAt?: string;
|
|
46
|
+
/** True for unlimited / boosted windows. */
|
|
47
|
+
isUnlimited?: boolean;
|
|
48
|
+
/** Per-model windows (MiniMax returns per-model buckets). */
|
|
49
|
+
modelName?: string;
|
|
50
|
+
}
|
|
51
|
+
export type QuotaProviderState = 'ok' | 'auth_failed' | 'not_configured' | 'upstream_unavailable' | 'rate_limited' | 'malformed_response' | 'timed_out' | 'error';
|
|
52
|
+
/**
|
|
53
|
+
* Structured provider status. Flat shape (not a discriminated union) so it
|
|
54
|
+
* round-trips cleanly through the Zod schema. Adapters populate `message`
|
|
55
|
+
* only when it carries actionable detail; `state: 'ok'` omits it.
|
|
56
|
+
*/
|
|
57
|
+
export interface QuotaProviderStatus {
|
|
58
|
+
state: QuotaProviderState;
|
|
59
|
+
message?: string;
|
|
60
|
+
category?: string;
|
|
61
|
+
}
|
|
62
|
+
/** Full API response for GET /api/quota — only configured providers appear. */
|
|
63
|
+
export interface QuotaResponse {
|
|
64
|
+
providers: QuotaSnapshot[];
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized quota domain types.
|
|
3
|
+
*
|
|
4
|
+
* These are provider-neutral. Every provider adapter converts its own
|
|
5
|
+
* heterogeneous response shape into this contract so the rest of the app
|
|
6
|
+
* never sees provider-specific fields.
|
|
7
|
+
*
|
|
8
|
+
* Mirrored on the Swift side by TokenDashSwift/Sources/TokenDash/Models/APIModels.swift.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -7,6 +7,20 @@ import { getAnalytics } from './analytics.js';
|
|
|
7
7
|
import { detectAvailableAgents } from '../agentDetection.js';
|
|
8
8
|
import { isOpenClawAccessible } from '../openclawParser.js';
|
|
9
9
|
import { isOpencodeAccessible } from '../opencodeParser.js';
|
|
10
|
+
import { quotaService } from '../quota/index.js';
|
|
11
|
+
async function getQuota(_req, res) {
|
|
12
|
+
// Fresh data only — quotaService handles cache, stale retention, and per-provider
|
|
13
|
+
// failure isolation internally. One provider's error never fails the whole response.
|
|
14
|
+
const force = _req.query.refresh === '1' || _req.query.refresh === 'true';
|
|
15
|
+
try {
|
|
16
|
+
const data = force ? await quotaService.refreshAll() : await quotaService.fetchAll();
|
|
17
|
+
res.json(data);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
21
|
+
res.status(500).json({ error: 'Failed to fetch quota', hint: message });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
10
24
|
function getAgents(_req, res) {
|
|
11
25
|
try {
|
|
12
26
|
const agents = detectAvailableAgents();
|
|
@@ -44,4 +58,5 @@ export function registerApiRoutes(router, appInfo) {
|
|
|
44
58
|
router.get('/projects', getProjects);
|
|
45
59
|
router.get('/blocks', getBlocks);
|
|
46
60
|
router.get('/analytics', getAnalytics);
|
|
61
|
+
router.get('/quota', getQuota);
|
|
47
62
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { cache } from '../cache.js';
|
|
2
|
+
import { validateInsights } from '../../shared/schemas.js';
|
|
3
|
+
import { getDailyResponse as getCodexDailyResponse, getProjectsResponse as getCodexProjectsResponse } from '../codexParser.js';
|
|
4
|
+
import { getDailyResponse as getOpenClawDailyResponse, getProjectsResponse as getOpenClawProjectsResponse } from '../openclawParser.js';
|
|
5
|
+
import { getDailyResponse as getOpencodeDailyResponse, getProjectsResponse as getOpencodeProjectsResponse } from '../opencodeParser.js';
|
|
6
|
+
import { getDailyResponse as getClaudeDailyResponse, getProjectsResponse as getClaudeProjectsResponse } from '../claudeJsonlParser.js';
|
|
7
|
+
import { detectAvailableAgents } from '../agentDetection.js';
|
|
8
|
+
import { isOpenClawAccessible } from '../openclawParser.js';
|
|
9
|
+
import { isOpencodeAccessible } from '../opencodeParser.js';
|
|
10
|
+
import { buildInsightsResponse, mergeDailyResponsesByDate } from '../insightsCalculator.js';
|
|
11
|
+
const SUPPORTED_AGENTS = ['claude', 'codex', 'openclaw', 'opencode'];
|
|
12
|
+
export async function getInsights(req, res) {
|
|
13
|
+
const agent = req.query.agent || 'claude';
|
|
14
|
+
const project = req.query.project || undefined;
|
|
15
|
+
const cacheKey = `insights:${agent}:${project || 'all'}`;
|
|
16
|
+
try {
|
|
17
|
+
const cached = cache.get(cacheKey);
|
|
18
|
+
if (cached) {
|
|
19
|
+
res.json(cached);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const stale = cache.getStale(cacheKey);
|
|
23
|
+
if (stale) {
|
|
24
|
+
refreshInsightsCache(agent, project, cacheKey);
|
|
25
|
+
res.json(stale);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const data = fetchInsightsData(agent, project);
|
|
29
|
+
cache.set(cacheKey, data);
|
|
30
|
+
res.json(data);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
34
|
+
console.error('Error fetching insights:', error);
|
|
35
|
+
res.status(502).json({
|
|
36
|
+
error: `Failed to fetch insights from ${agent}`,
|
|
37
|
+
hint: message,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function fetchInsightsData(agent, project) {
|
|
42
|
+
if (agent === 'all') {
|
|
43
|
+
return validateInsights(fetchAllAgentInsights());
|
|
44
|
+
}
|
|
45
|
+
const selectedAgent = normalizeAgent(agent);
|
|
46
|
+
const daily = fetchDailyData(selectedAgent);
|
|
47
|
+
const projects = fetchProjectsData(selectedAgent);
|
|
48
|
+
const scopedDaily = project ? (projects.projects[project] ?? []) : daily.daily;
|
|
49
|
+
return validateInsights(buildInsightsResponse({
|
|
50
|
+
agent: selectedAgent,
|
|
51
|
+
project,
|
|
52
|
+
daily: scopedDaily,
|
|
53
|
+
projects: project ? undefined : projects,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function fetchAllAgentInsights() {
|
|
57
|
+
const available = availableAgents();
|
|
58
|
+
const dailyResponses = [];
|
|
59
|
+
const partialAgents = [];
|
|
60
|
+
const todayDrivers = [];
|
|
61
|
+
for (const agent of available) {
|
|
62
|
+
try {
|
|
63
|
+
const daily = fetchDailyData(agent);
|
|
64
|
+
dailyResponses.push(daily);
|
|
65
|
+
const today = todayEntry(daily);
|
|
66
|
+
if (today.totalTokens > 0 || today.totalCost > 0) {
|
|
67
|
+
todayDrivers.push({
|
|
68
|
+
name: agent,
|
|
69
|
+
tokens: today.totalTokens,
|
|
70
|
+
cost: today.totalCost,
|
|
71
|
+
share: 0,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
partialAgents.push(agent);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const totalTokens = todayDrivers.reduce((sum, driver) => sum + driver.tokens, 0);
|
|
80
|
+
const topAgent = todayDrivers
|
|
81
|
+
.map(driver => ({ ...driver, share: totalTokens > 0 ? driver.tokens / totalTokens : 0 }))
|
|
82
|
+
.sort((a, b) => b.tokens - a.tokens)[0];
|
|
83
|
+
return buildInsightsResponse({
|
|
84
|
+
agent: 'all',
|
|
85
|
+
daily: mergeDailyResponsesByDate(dailyResponses.map(response => response.daily)),
|
|
86
|
+
topAgent,
|
|
87
|
+
partialAgents,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function todayEntry(response) {
|
|
91
|
+
const today = [
|
|
92
|
+
new Date().getFullYear(),
|
|
93
|
+
String(new Date().getMonth() + 1).padStart(2, '0'),
|
|
94
|
+
String(new Date().getDate()).padStart(2, '0'),
|
|
95
|
+
].join('-');
|
|
96
|
+
return response.daily.find(entry => entry.date === today) ?? {
|
|
97
|
+
date: today,
|
|
98
|
+
inputTokens: 0,
|
|
99
|
+
outputTokens: 0,
|
|
100
|
+
cacheCreationTokens: 0,
|
|
101
|
+
cacheReadTokens: 0,
|
|
102
|
+
totalTokens: 0,
|
|
103
|
+
totalCost: 0,
|
|
104
|
+
modelsUsed: [],
|
|
105
|
+
modelBreakdowns: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function availableAgents() {
|
|
109
|
+
const detected = detectAvailableAgents();
|
|
110
|
+
const agents = [];
|
|
111
|
+
if (detected.claude)
|
|
112
|
+
agents.push('claude');
|
|
113
|
+
if (detected.codex)
|
|
114
|
+
agents.push('codex');
|
|
115
|
+
if (isOpenClawAccessible())
|
|
116
|
+
agents.push('openclaw');
|
|
117
|
+
if (isOpencodeAccessible())
|
|
118
|
+
agents.push('opencode');
|
|
119
|
+
return agents.length > 0 ? agents : ['claude'];
|
|
120
|
+
}
|
|
121
|
+
function normalizeAgent(agent) {
|
|
122
|
+
return SUPPORTED_AGENTS.includes(agent) ? agent : 'claude';
|
|
123
|
+
}
|
|
124
|
+
function fetchDailyData(agent) {
|
|
125
|
+
if (agent === 'codex') {
|
|
126
|
+
return getCodexDailyResponse();
|
|
127
|
+
}
|
|
128
|
+
else if (agent === 'openclaw') {
|
|
129
|
+
return getOpenClawDailyResponse();
|
|
130
|
+
}
|
|
131
|
+
else if (agent === 'opencode') {
|
|
132
|
+
return getOpencodeDailyResponse();
|
|
133
|
+
}
|
|
134
|
+
return getClaudeDailyResponse();
|
|
135
|
+
}
|
|
136
|
+
function fetchProjectsData(agent) {
|
|
137
|
+
if (agent === 'codex') {
|
|
138
|
+
return getCodexProjectsResponse();
|
|
139
|
+
}
|
|
140
|
+
else if (agent === 'openclaw') {
|
|
141
|
+
return getOpenClawProjectsResponse();
|
|
142
|
+
}
|
|
143
|
+
else if (agent === 'opencode') {
|
|
144
|
+
return getOpencodeProjectsResponse();
|
|
145
|
+
}
|
|
146
|
+
return getClaudeProjectsResponse();
|
|
147
|
+
}
|
|
148
|
+
function refreshInsightsCache(agent, project, cacheKey) {
|
|
149
|
+
Promise.resolve()
|
|
150
|
+
.then(() => {
|
|
151
|
+
const data = fetchInsightsData(agent, project);
|
|
152
|
+
cache.set(cacheKey, data);
|
|
153
|
+
})
|
|
154
|
+
.catch(err => console.error('Background refresh failed (insights):', err));
|
|
155
|
+
}
|