@zhangferry-dev/tokendash 1.6.0 → 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.
Files changed (60) hide show
  1. package/README.md +146 -83
  2. package/dist/client/assets/index-Bw503sNp.css +1 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/client/popover.html +4 -3
  5. package/dist/daemon.cjs +3306 -0
  6. package/dist/daemon.cjs.map +7 -0
  7. package/dist/electron-server.cjs +1043 -27
  8. package/dist/electron-server.cjs.map +4 -4
  9. package/dist/server/daemon.d.ts +12 -0
  10. package/dist/server/daemon.js +176 -0
  11. package/dist/server/index.js +39 -13
  12. package/dist/server/insightsCalculator.d.ts +15 -0
  13. package/dist/server/insightsCalculator.js +276 -0
  14. package/dist/server/quota/adapter.d.ts +47 -0
  15. package/dist/server/quota/adapter.js +41 -0
  16. package/dist/server/quota/adapters/claude.d.ts +2 -0
  17. package/dist/server/quota/adapters/claude.js +124 -0
  18. package/dist/server/quota/adapters/codex.d.ts +2 -0
  19. package/dist/server/quota/adapters/codex.js +188 -0
  20. package/dist/server/quota/adapters/glm.d.ts +2 -0
  21. package/dist/server/quota/adapters/glm.js +133 -0
  22. package/dist/server/quota/adapters/kimi.d.ts +2 -0
  23. package/dist/server/quota/adapters/kimi.js +184 -0
  24. package/dist/server/quota/adapters/minimax.d.ts +2 -0
  25. package/dist/server/quota/adapters/minimax.js +77 -0
  26. package/dist/server/quota/cache.d.ts +20 -0
  27. package/dist/server/quota/cache.js +44 -0
  28. package/dist/server/quota/credentialsFile.d.ts +13 -0
  29. package/dist/server/quota/credentialsFile.js +23 -0
  30. package/dist/server/quota/helpers.d.ts +39 -0
  31. package/dist/server/quota/helpers.js +93 -0
  32. package/dist/server/quota/index.d.ts +5 -0
  33. package/dist/server/quota/index.js +23 -0
  34. package/dist/server/quota/quotaService.d.ts +37 -0
  35. package/dist/server/quota/quotaService.js +141 -0
  36. package/dist/server/quota/schemas.d.ts +358 -0
  37. package/dist/server/quota/schemas.js +53 -0
  38. package/dist/server/quota/types.d.ts +65 -0
  39. package/dist/server/quota/types.js +10 -0
  40. package/dist/server/routes/api.d.ts +6 -1
  41. package/dist/server/routes/api.js +26 -1
  42. package/dist/server/routes/insights.d.ts +2 -0
  43. package/dist/server/routes/insights.js +155 -0
  44. package/package.json +6 -10
  45. package/resources/icon-1024.png +0 -0
  46. package/resources/icon.icns +0 -0
  47. package/resources/icon.png +0 -0
  48. package/resources/product_menu.png +0 -0
  49. package/resources/readme-hero.png +0 -0
  50. package/dist/client/assets/index-_yA9tOzZ.css +0 -1
  51. package/electron/main.cjs +0 -490
  52. package/electron/main.js +0 -291
  53. package/electron/preload.cjs +0 -36
  54. package/electron/trayBadge.cjs +0 -27
  55. package/electron/trayBadge.js +0 -30
  56. package/electron/trayHelper +0 -0
  57. package/electron/trayHelper.swift +0 -152
  58. package/electron/updateService.cjs +0 -148
  59. package/electron-builder.yml +0 -20
  60. /package/dist/client/assets/{index-CY4G_b0x.js → index-C913wKtU.js} +0 -0
@@ -0,0 +1,3306 @@
1
+ var __esbuild_import_meta_url = require("url").pathToFileURL(__filename).href;
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/server/index.ts
27
+ var import_express = __toESM(require("express"), 1);
28
+ var import_node_fs13 = require("node:fs");
29
+ var import_node_url = require("node:url");
30
+ var import_node_path13 = require("node:path");
31
+
32
+ // src/server/cache.ts
33
+ var import_node_fs = require("node:fs");
34
+ var import_node_path = require("node:path");
35
+ var import_node_os = require("node:os");
36
+ var DEFAULT_TTL = 5 * 60 * 1e3;
37
+ var DISK_TTL = 60 * 60 * 1e3;
38
+ var CACHE_DIR = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "tokendash-cache");
39
+ function diskPath(key) {
40
+ const safe = key.replace(/[^a-zA-Z0-9_-]/g, "_");
41
+ return (0, import_node_path.join)(CACHE_DIR, `${safe}.json`);
42
+ }
43
+ var Cache = class {
44
+ store = /* @__PURE__ */ new Map();
45
+ get(key) {
46
+ const entry = this.store.get(key);
47
+ if (entry && Date.now() <= entry.expiresAt) {
48
+ return entry.data;
49
+ }
50
+ return null;
51
+ }
52
+ /** Get data even if stale (for stale-while-revalidate) */
53
+ getStale(key) {
54
+ const entry = this.store.get(key);
55
+ if (entry) return entry.data;
56
+ return this.readFromDisk(key);
57
+ }
58
+ set(key, data, ttl = DEFAULT_TTL) {
59
+ const entry = {
60
+ data,
61
+ expiresAt: Date.now() + ttl,
62
+ updatedAt: Date.now()
63
+ };
64
+ this.store.set(key, entry);
65
+ this.writeToDisk(key, entry);
66
+ }
67
+ clear() {
68
+ this.store.clear();
69
+ }
70
+ delete(key) {
71
+ return this.store.delete(key);
72
+ }
73
+ has(key) {
74
+ const entry = this.store.get(key);
75
+ if (!entry) return false;
76
+ if (Date.now() > entry.expiresAt) {
77
+ this.store.delete(key);
78
+ return false;
79
+ }
80
+ return true;
81
+ }
82
+ writeToDisk(key, entry) {
83
+ try {
84
+ if (!(0, import_node_fs.existsSync)(CACHE_DIR)) (0, import_node_fs.mkdirSync)(CACHE_DIR, { recursive: true });
85
+ (0, import_node_fs.writeFileSync)(diskPath(key), JSON.stringify(entry), "utf-8");
86
+ } catch {
87
+ }
88
+ }
89
+ readFromDisk(key) {
90
+ try {
91
+ const path = diskPath(key);
92
+ if (!(0, import_node_fs.existsSync)(path)) return null;
93
+ const raw = (0, import_node_fs.readFileSync)(path, "utf-8");
94
+ const entry = JSON.parse(raw);
95
+ if (Date.now() - entry.updatedAt < DISK_TTL) {
96
+ this.store.set(key, { ...entry, expiresAt: 0 });
97
+ return entry.data;
98
+ }
99
+ } catch {
100
+ }
101
+ return null;
102
+ }
103
+ };
104
+ var cache = new Cache();
105
+
106
+ // src/shared/schemas.ts
107
+ var import_zod = require("zod");
108
+ var ModelBreakdownSchema = import_zod.z.object({
109
+ modelName: import_zod.z.string(),
110
+ inputTokens: import_zod.z.number().default(0),
111
+ outputTokens: import_zod.z.number().default(0),
112
+ cacheCreationTokens: import_zod.z.number().default(0),
113
+ cacheReadTokens: import_zod.z.number().default(0),
114
+ cost: import_zod.z.number().default(0)
115
+ });
116
+ var DailyEntrySchema = import_zod.z.object({
117
+ date: import_zod.z.string(),
118
+ inputTokens: import_zod.z.number().default(0),
119
+ outputTokens: import_zod.z.number().default(0),
120
+ cacheCreationTokens: import_zod.z.number().default(0),
121
+ cacheReadTokens: import_zod.z.number().default(0),
122
+ totalTokens: import_zod.z.number().default(0),
123
+ totalCost: import_zod.z.number().default(0),
124
+ modelsUsed: import_zod.z.array(import_zod.z.string()).default([]),
125
+ modelBreakdowns: import_zod.z.array(ModelBreakdownSchema).default([])
126
+ });
127
+ var TotalsSchema = import_zod.z.object({
128
+ inputTokens: import_zod.z.number().default(0),
129
+ outputTokens: import_zod.z.number().default(0),
130
+ cacheCreationTokens: import_zod.z.number().default(0),
131
+ cacheReadTokens: import_zod.z.number().default(0),
132
+ totalTokens: import_zod.z.number().default(0),
133
+ totalCost: import_zod.z.number().default(0)
134
+ });
135
+ var DailyResponseSchema = import_zod.z.object({
136
+ daily: import_zod.z.array(DailyEntrySchema).default([]),
137
+ totals: TotalsSchema
138
+ });
139
+ var ProjectEntrySchema = import_zod.z.object({
140
+ projectPath: import_zod.z.string(),
141
+ instances: import_zod.z.array(DailyEntrySchema).default([])
142
+ });
143
+ var ProjectsResponseSchema = import_zod.z.object({
144
+ projects: import_zod.z.record(import_zod.z.array(DailyEntrySchema).default([])).default({})
145
+ });
146
+ function validateDaily(data) {
147
+ return DailyResponseSchema.parse(data);
148
+ }
149
+ function validateProjects(data) {
150
+ return ProjectsResponseSchema.parse(data);
151
+ }
152
+ var BlockEntrySchema = import_zod.z.object({
153
+ id: import_zod.z.string(),
154
+ startTime: import_zod.z.string(),
155
+ endTime: import_zod.z.string(),
156
+ actualEndTime: import_zod.z.string().nullable().default(null),
157
+ isActive: import_zod.z.boolean().default(false),
158
+ isGap: import_zod.z.boolean().default(false),
159
+ entries: import_zod.z.number().default(0),
160
+ tokenCounts: import_zod.z.object({
161
+ inputTokens: import_zod.z.number().default(0),
162
+ outputTokens: import_zod.z.number().default(0),
163
+ cacheCreationInputTokens: import_zod.z.number().default(0),
164
+ cacheReadInputTokens: import_zod.z.number().default(0)
165
+ }).default({ inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 }),
166
+ totalTokens: import_zod.z.number().default(0),
167
+ costUSD: import_zod.z.number().default(0),
168
+ models: import_zod.z.array(import_zod.z.string()).default([])
169
+ });
170
+ var BlocksResponseSchema = import_zod.z.object({
171
+ blocks: import_zod.z.array(BlockEntrySchema).default([])
172
+ });
173
+ function validateBlocks(data) {
174
+ return BlocksResponseSchema.parse(data);
175
+ }
176
+ var DailyCodeChangeSchema = import_zod.z.object({
177
+ date: import_zod.z.string(),
178
+ linesAdded: import_zod.z.number().default(0),
179
+ linesDeleted: import_zod.z.number().default(0),
180
+ netChange: import_zod.z.number().default(0),
181
+ filesModified: import_zod.z.number().default(0)
182
+ });
183
+ var ToolUsageEntrySchema = import_zod.z.object({
184
+ name: import_zod.z.string(),
185
+ count: import_zod.z.number().default(0)
186
+ });
187
+ var ProductivityKPIsSchema = import_zod.z.object({
188
+ avgLinesPerEdit: import_zod.z.number().default(0),
189
+ filesModifiedPerDay: import_zod.z.number().default(0),
190
+ addDeleteRatio: import_zod.z.number().default(0),
191
+ totalEdits: import_zod.z.number().default(0),
192
+ totalFilesModified: import_zod.z.number().default(0),
193
+ activeDaysWithEdits: import_zod.z.number().default(0)
194
+ });
195
+ var AnalyticsResponseSchema = import_zod.z.object({
196
+ codeChangeTrend: import_zod.z.array(DailyCodeChangeSchema).default([]),
197
+ toolUsageDistribution: import_zod.z.array(ToolUsageEntrySchema).default([]),
198
+ productivityKPIs: ProductivityKPIsSchema,
199
+ toolCallTrend: import_zod.z.array(import_zod.z.record(import_zod.z.union([import_zod.z.string(), import_zod.z.number()]))).default([])
200
+ });
201
+ function validateAnalytics(data) {
202
+ return AnalyticsResponseSchema.parse(data);
203
+ }
204
+
205
+ // src/server/codexParser.ts
206
+ var import_node_fs2 = require("node:fs");
207
+ var import_node_path2 = require("node:path");
208
+ var import_node_os2 = require("node:os");
209
+ var import_zod2 = require("zod");
210
+
211
+ // src/server/codexPricing.ts
212
+ var MODEL_PRICING = {
213
+ "gpt-5.4": {
214
+ inputPer1M: 2.5,
215
+ cachedInputPer1M: 0.25,
216
+ outputPer1M: 15
217
+ }
218
+ };
219
+ var DEFAULT_PRICING = {
220
+ inputPer1M: 2.5,
221
+ cachedInputPer1M: 0.25,
222
+ outputPer1M: 15
223
+ };
224
+ function calculateCost(tokens, models) {
225
+ const model = [...models][0] ?? "";
226
+ const pricing = MODEL_PRICING[model] ?? DEFAULT_PRICING;
227
+ const nonCachedInput = Math.max(tokens.inputTokens - tokens.cachedInputTokens, 0);
228
+ const cachedInput = Math.min(tokens.cachedInputTokens, tokens.inputTokens);
229
+ const outputTokens = tokens.outputTokens;
230
+ const inputCost = nonCachedInput / 1e6 * pricing.inputPer1M;
231
+ const cachedCost = cachedInput / 1e6 * pricing.cachedInputPer1M;
232
+ const outputCost = outputTokens / 1e6 * pricing.outputPer1M;
233
+ return inputCost + cachedCost + outputCost;
234
+ }
235
+
236
+ // src/server/codexParser.ts
237
+ var TokenUsageSchema = import_zod2.z.object({
238
+ input_tokens: import_zod2.z.number().default(0),
239
+ cached_input_tokens: import_zod2.z.number().default(0),
240
+ output_tokens: import_zod2.z.number().default(0),
241
+ reasoning_output_tokens: import_zod2.z.number().default(0),
242
+ total_tokens: import_zod2.z.number().default(0)
243
+ }).default({ input_tokens: 0, cached_input_tokens: 0, output_tokens: 0, reasoning_output_tokens: 0, total_tokens: 0 });
244
+ var TokenCountInfoSchema = import_zod2.z.object({
245
+ total_token_usage: TokenUsageSchema,
246
+ last_token_usage: TokenUsageSchema.optional()
247
+ }).nullable().default(null);
248
+ var TokenCountPayloadSchema = import_zod2.z.object({
249
+ type: import_zod2.z.literal("token_count"),
250
+ info: TokenCountInfoSchema
251
+ });
252
+ function subtractTokenUsage(current, previous) {
253
+ return {
254
+ timestamp: "",
255
+ inputTokens: Math.max(0, current.input_tokens - (previous?.input_tokens ?? 0)),
256
+ cachedInputTokens: Math.max(0, current.cached_input_tokens - (previous?.cached_input_tokens ?? 0)),
257
+ outputTokens: Math.max(0, current.output_tokens - (previous?.output_tokens ?? 0)),
258
+ reasoningOutputTokens: Math.max(0, current.reasoning_output_tokens - (previous?.reasoning_output_tokens ?? 0)),
259
+ totalTokens: Math.max(0, current.total_tokens - (previous?.total_tokens ?? 0))
260
+ };
261
+ }
262
+ function displayInputTokens(inputTokens, cachedInputTokens) {
263
+ return Math.max(0, inputTokens - cachedInputTokens);
264
+ }
265
+ function getSessionsDir() {
266
+ return (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".codex", "sessions");
267
+ }
268
+ function scanCodexSessions() {
269
+ const sessionsDir = getSessionsDir();
270
+ const results = [];
271
+ function walk(dir) {
272
+ let entries;
273
+ try {
274
+ entries = (0, import_node_fs2.readdirSync)(dir);
275
+ } catch {
276
+ return;
277
+ }
278
+ for (const entry of entries) {
279
+ const full = (0, import_node_path2.join)(dir, entry);
280
+ let st;
281
+ try {
282
+ st = (0, import_node_fs2.statSync)(full);
283
+ } catch {
284
+ continue;
285
+ }
286
+ if (st.isDirectory()) {
287
+ walk(full);
288
+ } else if (entry.endsWith(".jsonl")) {
289
+ results.push(full);
290
+ }
291
+ }
292
+ }
293
+ walk(sessionsDir);
294
+ return results.sort();
295
+ }
296
+ function parseCodexSession(filepath) {
297
+ let content;
298
+ try {
299
+ content = (0, import_node_fs2.readFileSync)(filepath, "utf-8");
300
+ } catch {
301
+ return null;
302
+ }
303
+ const lines = content.split("\n");
304
+ let sessionId = "";
305
+ let cwd = "";
306
+ let model = "";
307
+ let createdAt = "";
308
+ const tokenEvents = [];
309
+ let previousTotalUsage = null;
310
+ const seenTotalUsageSnapshots = /* @__PURE__ */ new Set();
311
+ const seenUsageEvents = /* @__PURE__ */ new Set();
312
+ for (const line of lines) {
313
+ const trimmed = line.trim();
314
+ if (!trimmed) continue;
315
+ let obj;
316
+ try {
317
+ obj = JSON.parse(trimmed);
318
+ } catch {
319
+ continue;
320
+ }
321
+ const type = obj.type;
322
+ if (type === "session_meta") {
323
+ const payload = obj.payload || {};
324
+ sessionId = payload.id || "";
325
+ cwd = payload.cwd || "";
326
+ createdAt = payload.timestamp || "";
327
+ }
328
+ if (type === "turn_context") {
329
+ const payload = obj.payload || {};
330
+ if (!model && payload.model) {
331
+ model = payload.model;
332
+ }
333
+ }
334
+ if (type === "event_msg") {
335
+ const payload = obj.payload || {};
336
+ if (payload.type === "token_count") {
337
+ const timestamp = obj.timestamp || "";
338
+ const parseResult = TokenCountPayloadSchema.safeParse(payload);
339
+ if (!parseResult.success) {
340
+ console.warn(`[codexParser] Schema validation failed in ${filepath}:`, parseResult.error.message);
341
+ continue;
342
+ }
343
+ const info = parseResult.data.info;
344
+ if (!info) continue;
345
+ const totalUsageKey = [
346
+ info.total_token_usage.input_tokens,
347
+ info.total_token_usage.cached_input_tokens,
348
+ info.total_token_usage.output_tokens,
349
+ info.total_token_usage.reasoning_output_tokens,
350
+ info.total_token_usage.total_tokens
351
+ ].join(":");
352
+ if (seenTotalUsageSnapshots.has(totalUsageKey)) continue;
353
+ seenTotalUsageSnapshots.add(totalUsageKey);
354
+ const last = info.last_token_usage ?? info.total_token_usage;
355
+ const rawEvent = info.last_token_usage ? subtractTokenUsage(last, null) : subtractTokenUsage(last, previousTotalUsage);
356
+ previousTotalUsage = info.total_token_usage;
357
+ if (rawEvent.inputTokens === 0 && rawEvent.cachedInputTokens === 0 && rawEvent.outputTokens === 0 && rawEvent.reasoningOutputTokens === 0) {
358
+ continue;
359
+ }
360
+ const event = {
361
+ ...rawEvent,
362
+ timestamp,
363
+ cachedInputTokens: Math.min(rawEvent.cachedInputTokens, rawEvent.inputTokens)
364
+ };
365
+ const eventKey = [
366
+ timestamp,
367
+ model,
368
+ event.inputTokens,
369
+ event.cachedInputTokens,
370
+ event.outputTokens,
371
+ event.reasoningOutputTokens,
372
+ event.totalTokens
373
+ ].join(":");
374
+ if (seenUsageEvents.has(eventKey)) {
375
+ continue;
376
+ }
377
+ seenUsageEvents.add(eventKey);
378
+ tokenEvents.push(event);
379
+ }
380
+ }
381
+ }
382
+ if (!sessionId) return null;
383
+ return { id: sessionId, cwd, model, createdAt, tokenEvents };
384
+ }
385
+ function parseAllSessions() {
386
+ return scanCodexSessions().map(parseCodexSession).filter((s) => s !== null);
387
+ }
388
+ var TZ_OFFSETS = {
389
+ "Asia/Shanghai": 8,
390
+ "Asia/Tokyo": 9,
391
+ "America/New_York": -5,
392
+ "America/Los_Angeles": -8,
393
+ "Europe/London": 0,
394
+ "UTC": 0
395
+ };
396
+ function getTzOffsetHours(tz) {
397
+ return TZ_OFFSETS[tz] ?? 8;
398
+ }
399
+ function toLocalISO(ts, tz) {
400
+ const d = new Date(ts);
401
+ return new Date(d.getTime() + getTzOffsetHours(tz) * 36e5);
402
+ }
403
+ function getDateKey(ts, tz) {
404
+ return toLocalISO(ts, tz).toISOString().slice(0, 10);
405
+ }
406
+ function getHourKey(ts, tz) {
407
+ const local = toLocalISO(ts, tz);
408
+ return local.toISOString().slice(0, 13).replace("T", " ") + ":00";
409
+ }
410
+ function getMonthKey(ts, tz) {
411
+ return getDateKey(ts, tz).slice(0, 7);
412
+ }
413
+ function extractProjectName(cwd) {
414
+ if (!cwd) return "unknown";
415
+ const parts = cwd.replace(/\/+$/, "").split("/");
416
+ return parts[parts.length - 1] || "unknown";
417
+ }
418
+ function emptyAcc() {
419
+ return { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0, totalTokens: 0 };
420
+ }
421
+ function addAcc(a, ev) {
422
+ a.inputTokens += ev.inputTokens;
423
+ a.cachedInputTokens += ev.cachedInputTokens;
424
+ a.outputTokens += ev.outputTokens;
425
+ a.reasoningOutputTokens += ev.reasoningOutputTokens;
426
+ a.totalTokens += ev.totalTokens;
427
+ }
428
+ function displayAcc(acc) {
429
+ return {
430
+ ...acc,
431
+ inputTokens: displayInputTokens(acc.inputTokens, acc.cachedInputTokens)
432
+ };
433
+ }
434
+ function mergeAcc(a, b) {
435
+ a.inputTokens += b.inputTokens;
436
+ a.cachedInputTokens += b.cachedInputTokens;
437
+ a.outputTokens += b.outputTokens;
438
+ a.reasoningOutputTokens += b.reasoningOutputTokens;
439
+ a.totalTokens += b.totalTokens;
440
+ }
441
+ function addAccToBucket(bucket, ev, model) {
442
+ addAcc(bucket.acc, ev);
443
+ if (!model) return;
444
+ if (!bucket.models.has(model)) bucket.models.set(model, emptyAcc());
445
+ addAcc(bucket.models.get(model), ev);
446
+ }
447
+ function accToEntry(date, acc, modelAccs) {
448
+ const display = displayAcc(acc);
449
+ const modelNames = [...modelAccs.keys()];
450
+ const modelBreakdowns = buildModelBreakdowns(modelAccs);
451
+ const totalCost = modelBreakdowns.reduce((sum, model) => sum + model.cost, 0);
452
+ return {
453
+ date,
454
+ inputTokens: display.inputTokens,
455
+ outputTokens: display.outputTokens,
456
+ cacheCreationTokens: 0,
457
+ cacheReadTokens: display.cachedInputTokens,
458
+ totalTokens: display.totalTokens,
459
+ totalCost,
460
+ modelsUsed: modelNames,
461
+ modelBreakdowns
462
+ };
463
+ }
464
+ function buildModelBreakdowns(modelAccs) {
465
+ return [...modelAccs.entries()].map(([modelName, acc]) => {
466
+ const display = displayAcc(acc);
467
+ return {
468
+ modelName,
469
+ inputTokens: display.inputTokens,
470
+ outputTokens: display.outputTokens,
471
+ cacheCreationTokens: 0,
472
+ cacheReadTokens: display.cachedInputTokens,
473
+ cost: calculateCost(acc, /* @__PURE__ */ new Set([modelName]))
474
+ };
475
+ });
476
+ }
477
+ function groupSessions(sessions, options) {
478
+ const tz = options.timezone || "Asia/Shanghai";
479
+ const grouped = /* @__PURE__ */ new Map();
480
+ for (const session of sessions) {
481
+ if (options.project && extractProjectName(session.cwd) !== options.project) continue;
482
+ for (const ev of session.tokenEvents) {
483
+ const evDate = new Date(ev.timestamp);
484
+ if (options.since && evDate < options.since) continue;
485
+ if (options.until && evDate > options.until) continue;
486
+ let key;
487
+ switch (options.groupBy) {
488
+ case "hour":
489
+ key = getHourKey(ev.timestamp, tz);
490
+ break;
491
+ case "month":
492
+ key = getMonthKey(ev.timestamp, tz);
493
+ break;
494
+ case "session":
495
+ key = session.id;
496
+ break;
497
+ case "project":
498
+ key = extractProjectName(session.cwd);
499
+ break;
500
+ default:
501
+ key = getDateKey(ev.timestamp, tz);
502
+ break;
503
+ }
504
+ if (!grouped.has(key)) {
505
+ grouped.set(key, { acc: emptyAcc(), models: /* @__PURE__ */ new Map() });
506
+ }
507
+ addAccToBucket(grouped.get(key), ev, session.model);
508
+ }
509
+ }
510
+ return grouped;
511
+ }
512
+ function buildDailyResponse(sessions, options) {
513
+ const grouped = groupSessions(sessions, { groupBy: "day", ...options });
514
+ const daily = [];
515
+ const totalsAcc = emptyAcc();
516
+ const totalModels = /* @__PURE__ */ new Map();
517
+ for (const [date, { acc, models }] of grouped) {
518
+ daily.push(accToEntry(date, acc, models));
519
+ mergeAcc(totalsAcc, acc);
520
+ for (const [model, modelAcc] of models) {
521
+ if (!totalModels.has(model)) totalModels.set(model, emptyAcc());
522
+ mergeAcc(totalModels.get(model), modelAcc);
523
+ }
524
+ }
525
+ daily.sort((a, b) => a.date.localeCompare(b.date));
526
+ const totalCost = buildModelBreakdowns(totalModels).reduce((sum, model) => sum + model.cost, 0);
527
+ return {
528
+ daily,
529
+ totals: {
530
+ inputTokens: displayInputTokens(totalsAcc.inputTokens, totalsAcc.cachedInputTokens),
531
+ outputTokens: totalsAcc.outputTokens,
532
+ cacheCreationTokens: 0,
533
+ cacheReadTokens: totalsAcc.cachedInputTokens,
534
+ totalTokens: totalsAcc.totalTokens,
535
+ totalCost
536
+ }
537
+ };
538
+ }
539
+ function buildProjectsResponse(sessions, options) {
540
+ const tz = options?.timezone || "Asia/Shanghai";
541
+ const projectGroups = /* @__PURE__ */ new Map();
542
+ for (const session of sessions) {
543
+ const projectName = extractProjectName(session.cwd);
544
+ if (options?.project && projectName !== options.project) continue;
545
+ if (!projectGroups.has(projectName)) projectGroups.set(projectName, /* @__PURE__ */ new Map());
546
+ const dailyMap = projectGroups.get(projectName);
547
+ for (const ev of session.tokenEvents) {
548
+ const evDate = new Date(ev.timestamp);
549
+ if (options?.since && evDate < options.since) continue;
550
+ if (options?.until && evDate > options.until) continue;
551
+ const dayKey = getDateKey(ev.timestamp, tz);
552
+ if (!dailyMap.has(dayKey)) {
553
+ dailyMap.set(dayKey, { acc: emptyAcc(), models: /* @__PURE__ */ new Map() });
554
+ }
555
+ addAccToBucket(dailyMap.get(dayKey), ev, session.model);
556
+ }
557
+ }
558
+ const projects = {};
559
+ for (const [projectName, dailyMap] of projectGroups) {
560
+ projects[projectName] = [...dailyMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, { acc, models }]) => accToEntry(date, acc, models));
561
+ }
562
+ return { projects };
563
+ }
564
+ function buildBlocksResponse(sessions, options) {
565
+ const grouped = groupSessions(sessions, { groupBy: "hour", ...options });
566
+ const blocks = [];
567
+ let idx = 0;
568
+ for (const [hourKey, { acc, models }] of grouped) {
569
+ const cost = buildModelBreakdowns(models).reduce((sum, model) => sum + model.cost, 0);
570
+ const [datePart, timePart] = hourKey.split(" ");
571
+ const hour = timePart.split(":")[0];
572
+ blocks.push({
573
+ id: `codex-hour-${idx}`,
574
+ startTime: `${datePart}T${hour}:00:00`,
575
+ endTime: `${datePart}T${hour}:59:59`,
576
+ actualEndTime: null,
577
+ isActive: false,
578
+ isGap: false,
579
+ entries: acc.totalTokens > 0 ? 1 : 0,
580
+ tokenCounts: {
581
+ inputTokens: displayInputTokens(acc.inputTokens, acc.cachedInputTokens),
582
+ outputTokens: acc.outputTokens,
583
+ cacheCreationInputTokens: 0,
584
+ cacheReadInputTokens: acc.cachedInputTokens
585
+ },
586
+ totalTokens: acc.totalTokens,
587
+ costUSD: cost,
588
+ models: [...models.keys()]
589
+ });
590
+ idx++;
591
+ }
592
+ blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
593
+ return { blocks };
594
+ }
595
+ function getDailyResponse(options) {
596
+ return buildDailyResponse(parseAllSessions(), options);
597
+ }
598
+ function getProjectsResponse(options) {
599
+ return buildProjectsResponse(parseAllSessions(), options);
600
+ }
601
+ function getBlocksResponse(options) {
602
+ return buildBlocksResponse(parseAllSessions(), options);
603
+ }
604
+
605
+ // src/server/openclawParser.ts
606
+ var import_node_fs3 = require("node:fs");
607
+ var import_node_path3 = require("node:path");
608
+ var import_node_os3 = require("node:os");
609
+ function getOpenClawDirs() {
610
+ const home = (0, import_node_os3.homedir)();
611
+ return [
612
+ (0, import_node_path3.join)(home, ".openclaw"),
613
+ (0, import_node_path3.join)(home, ".clawdbot"),
614
+ // legacy name 1
615
+ (0, import_node_path3.join)(home, ".moltbot"),
616
+ // legacy name 2
617
+ (0, import_node_path3.join)(home, ".moldbot")
618
+ // legacy name 3
619
+ ];
620
+ }
621
+ function isOpenClawAccessible() {
622
+ for (const dir of getOpenClawDirs()) {
623
+ try {
624
+ (0, import_node_fs3.accessSync)((0, import_node_path3.join)(dir, "agents"), import_node_fs3.constants.R_OK);
625
+ return true;
626
+ } catch {
627
+ }
628
+ }
629
+ return false;
630
+ }
631
+ function scanOpenClawSessions() {
632
+ const refs = [];
633
+ for (const baseDir of getOpenClawDirs()) {
634
+ const agentsDir = (0, import_node_path3.join)(baseDir, "agents");
635
+ let agentEntries;
636
+ try {
637
+ agentEntries = (0, import_node_fs3.readdirSync)(agentsDir);
638
+ } catch {
639
+ continue;
640
+ }
641
+ for (const agentEntry of agentEntries) {
642
+ const sessionsDir = (0, import_node_path3.join)(agentsDir, agentEntry, "sessions");
643
+ const indexedPaths = /* @__PURE__ */ new Set();
644
+ const indexPath = (0, import_node_path3.join)(sessionsDir, "sessions.json");
645
+ try {
646
+ const raw = (0, import_node_fs3.readFileSync)(indexPath, "utf-8");
647
+ const index = JSON.parse(raw);
648
+ for (const entry of Object.values(index)) {
649
+ if (!entry.sessionId) continue;
650
+ let sessionPath;
651
+ if (entry.sessionFile) {
652
+ const filePath = entry.sessionFile;
653
+ if (filePath.startsWith("/")) {
654
+ if (!getOpenClawDirs().some((dir) => filePath.startsWith(dir))) continue;
655
+ sessionPath = filePath;
656
+ } else {
657
+ sessionPath = (0, import_node_path3.join)(sessionsDir, filePath);
658
+ }
659
+ } else {
660
+ sessionPath = (0, import_node_path3.join)(sessionsDir, `${entry.sessionId}.jsonl`);
661
+ }
662
+ indexedPaths.add(sessionPath);
663
+ refs.push({ sessionId: entry.sessionId, sessionFile: sessionPath, agentId: agentEntry });
664
+ }
665
+ } catch {
666
+ }
667
+ let files;
668
+ try {
669
+ files = (0, import_node_fs3.readdirSync)(sessionsDir);
670
+ } catch {
671
+ continue;
672
+ }
673
+ for (const f of files) {
674
+ if (!f.endsWith(".jsonl")) continue;
675
+ const fullPath = (0, import_node_path3.join)(sessionsDir, f);
676
+ if (indexedPaths.has(fullPath)) continue;
677
+ const sessionId = f.replace(/\.jsonl.*$/, "");
678
+ refs.push({ sessionId, sessionFile: fullPath, agentId: agentEntry });
679
+ }
680
+ }
681
+ }
682
+ return refs;
683
+ }
684
+ var sessionCache = /* @__PURE__ */ new Map();
685
+ function parseOpenClawSession(ref) {
686
+ let fileMtimeMs = 0;
687
+ try {
688
+ fileMtimeMs = (0, import_node_fs3.statSync)(ref.sessionFile).mtimeMs;
689
+ } catch {
690
+ }
691
+ const cached = sessionCache.get(ref.sessionFile);
692
+ if (cached && cached.mtime === fileMtimeMs) {
693
+ return cached.result;
694
+ }
695
+ let content;
696
+ try {
697
+ content = (0, import_node_fs3.readFileSync)(ref.sessionFile, "utf-8");
698
+ } catch {
699
+ return null;
700
+ }
701
+ const tokenEvents = [];
702
+ let currentModel = "";
703
+ let currentProvider = "";
704
+ for (const line of content.split("\n")) {
705
+ const trimmed = line.trim();
706
+ if (!trimmed) continue;
707
+ let obj;
708
+ try {
709
+ obj = JSON.parse(trimmed);
710
+ } catch {
711
+ continue;
712
+ }
713
+ const type = obj.type;
714
+ if (type === "model_change") {
715
+ if (obj.modelId) currentModel = obj.modelId;
716
+ if (obj.provider) currentProvider = obj.provider;
717
+ continue;
718
+ }
719
+ if (type === "custom" && obj.customType === "model-snapshot") {
720
+ const data = obj.data || {};
721
+ if (data.modelId) currentModel = data.modelId;
722
+ if (data.provider) currentProvider = data.provider;
723
+ continue;
724
+ }
725
+ if (type === "message") {
726
+ const msg = obj.message || {};
727
+ if (msg.role !== "assistant") continue;
728
+ const usage = msg.usage || {};
729
+ if (!usage) continue;
730
+ const model = (msg.model || currentModel || "").trim();
731
+ const provider = (msg.provider || currentProvider || "").trim();
732
+ if (!model) continue;
733
+ if (model) currentModel = model;
734
+ if (provider) currentProvider = provider;
735
+ const input = Number(usage.input ?? 0);
736
+ const output = Number(usage.output ?? 0);
737
+ const cacheRead = Number(usage.cacheRead ?? 0);
738
+ const cacheWrite = Number(usage.cacheWrite ?? 0);
739
+ const costObj = usage.cost || {};
740
+ const cost = Number(costObj.total ?? 0);
741
+ const timestampMs = Number(msg.timestamp ?? fileMtimeMs);
742
+ tokenEvents.push({
743
+ timestampMs,
744
+ inputTokens: Math.max(0, input),
745
+ outputTokens: Math.max(0, output),
746
+ cacheReadTokens: Math.max(0, cacheRead),
747
+ cacheWriteTokens: Math.max(0, cacheWrite),
748
+ totalTokens: Math.max(0, input + output + cacheRead),
749
+ cost: Math.max(0, cost),
750
+ model: `${provider}/${model}`
751
+ });
752
+ }
753
+ }
754
+ if (tokenEvents.length === 0) {
755
+ sessionCache.set(ref.sessionFile, { mtime: fileMtimeMs, result: null });
756
+ return null;
757
+ }
758
+ const result = { id: ref.sessionId, agentId: ref.agentId, tokenEvents };
759
+ sessionCache.set(ref.sessionFile, { mtime: fileMtimeMs, result });
760
+ return result;
761
+ }
762
+ function parseAllOpenClawSessions() {
763
+ return scanOpenClawSessions().map(parseOpenClawSession).filter((s) => s !== null);
764
+ }
765
+ var TZ_OFFSETS2 = {
766
+ "Asia/Shanghai": 8,
767
+ "Asia/Tokyo": 9,
768
+ "America/New_York": -5,
769
+ "America/Los_Angeles": -8,
770
+ "Europe/London": 0,
771
+ "UTC": 0
772
+ };
773
+ function getTzOffsetHours2(tz) {
774
+ return TZ_OFFSETS2[tz] ?? 8;
775
+ }
776
+ function msToLocalDate(ms, tz) {
777
+ return new Date(ms + getTzOffsetHours2(tz) * 36e5);
778
+ }
779
+ function getDateKey2(ms, tz) {
780
+ return msToLocalDate(ms, tz).toISOString().slice(0, 10);
781
+ }
782
+ function getHourKey2(ms, tz) {
783
+ const d = msToLocalDate(ms, tz);
784
+ return d.toISOString().slice(0, 13).replace("T", " ") + ":00";
785
+ }
786
+ function emptyAcc2() {
787
+ return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, cost: 0 };
788
+ }
789
+ function addEvent(acc, ev) {
790
+ acc.inputTokens += ev.inputTokens;
791
+ acc.outputTokens += ev.outputTokens;
792
+ acc.cacheReadTokens += ev.cacheReadTokens;
793
+ acc.cacheWriteTokens += ev.cacheWriteTokens;
794
+ acc.totalTokens += ev.totalTokens;
795
+ acc.cost += ev.cost;
796
+ }
797
+ function mergeAcc2(a, b) {
798
+ a.inputTokens += b.inputTokens;
799
+ a.outputTokens += b.outputTokens;
800
+ a.cacheReadTokens += b.cacheReadTokens;
801
+ a.cacheWriteTokens += b.cacheWriteTokens;
802
+ a.totalTokens += b.totalTokens;
803
+ a.cost += b.cost;
804
+ }
805
+ function accToEntry2(date, acc, models) {
806
+ const modelList = [...models];
807
+ const costPerModel = modelList.length > 0 ? acc.cost / modelList.length : 0;
808
+ return {
809
+ date,
810
+ inputTokens: acc.inputTokens,
811
+ outputTokens: acc.outputTokens,
812
+ cacheCreationTokens: acc.cacheWriteTokens,
813
+ cacheReadTokens: acc.cacheReadTokens,
814
+ totalTokens: acc.totalTokens,
815
+ totalCost: acc.cost,
816
+ modelsUsed: modelList,
817
+ modelBreakdowns: modelList.map((name) => ({
818
+ modelName: name,
819
+ inputTokens: acc.inputTokens,
820
+ outputTokens: acc.outputTokens,
821
+ cacheCreationTokens: acc.cacheWriteTokens,
822
+ cacheReadTokens: acc.cacheReadTokens,
823
+ cost: costPerModel
824
+ }))
825
+ };
826
+ }
827
+ function getDailyResponse2(options) {
828
+ const sessions = parseAllOpenClawSessions();
829
+ const tz = options?.timezone || "Asia/Shanghai";
830
+ const grouped = /* @__PURE__ */ new Map();
831
+ const totalsAcc = emptyAcc2();
832
+ for (const session of sessions) {
833
+ if (options?.project && session.agentId !== options.project) continue;
834
+ for (const ev of session.tokenEvents) {
835
+ if (options?.since && ev.timestampMs < options.since.getTime()) continue;
836
+ if (options?.until && ev.timestampMs > options.until.getTime()) continue;
837
+ const key = getDateKey2(ev.timestampMs, tz);
838
+ if (!grouped.has(key)) grouped.set(key, { acc: emptyAcc2(), models: /* @__PURE__ */ new Set() });
839
+ const entry = grouped.get(key);
840
+ addEvent(entry.acc, ev);
841
+ entry.models.add(ev.model);
842
+ }
843
+ }
844
+ const daily = [];
845
+ for (const [date, { acc, models }] of grouped) {
846
+ daily.push(accToEntry2(date, acc, models));
847
+ mergeAcc2(totalsAcc, acc);
848
+ }
849
+ daily.sort((a, b) => a.date.localeCompare(b.date));
850
+ return {
851
+ daily,
852
+ totals: {
853
+ inputTokens: totalsAcc.inputTokens,
854
+ outputTokens: totalsAcc.outputTokens,
855
+ cacheCreationTokens: totalsAcc.cacheWriteTokens,
856
+ cacheReadTokens: totalsAcc.cacheReadTokens,
857
+ totalTokens: totalsAcc.totalTokens,
858
+ totalCost: totalsAcc.cost
859
+ }
860
+ };
861
+ }
862
+ function getProjectsResponse2(options) {
863
+ const sessions = parseAllOpenClawSessions();
864
+ const tz = options?.timezone || "Asia/Shanghai";
865
+ const projects = {};
866
+ for (const session of sessions) {
867
+ const projectName = session.agentId;
868
+ const dailyMap = /* @__PURE__ */ new Map();
869
+ for (const ev of session.tokenEvents) {
870
+ if (options?.since && ev.timestampMs < options.since.getTime()) continue;
871
+ if (options?.until && ev.timestampMs > options.until.getTime()) continue;
872
+ const dayKey = getDateKey2(ev.timestampMs, tz);
873
+ if (!dailyMap.has(dayKey)) dailyMap.set(dayKey, { acc: emptyAcc2(), models: /* @__PURE__ */ new Set() });
874
+ addEvent(dailyMap.get(dayKey).acc, ev);
875
+ dailyMap.get(dayKey).models.add(ev.model);
876
+ }
877
+ if (!projects[projectName]) projects[projectName] = [];
878
+ for (const [date, { acc, models }] of dailyMap) {
879
+ projects[projectName].push(accToEntry2(date, acc, models));
880
+ }
881
+ }
882
+ for (const key of Object.keys(projects)) {
883
+ projects[key].sort((a, b) => a.date.localeCompare(b.date));
884
+ }
885
+ return { projects };
886
+ }
887
+ function getBlocksResponse2(options) {
888
+ const sessions = parseAllOpenClawSessions();
889
+ const tz = options?.timezone || "Asia/Shanghai";
890
+ const grouped = /* @__PURE__ */ new Map();
891
+ for (const session of sessions) {
892
+ if (options?.project && session.agentId !== options.project) continue;
893
+ for (const ev of session.tokenEvents) {
894
+ if (options?.since && ev.timestampMs < options.since.getTime()) continue;
895
+ if (options?.until && ev.timestampMs > options.until.getTime()) continue;
896
+ const key = getHourKey2(ev.timestampMs, tz);
897
+ if (!grouped.has(key)) grouped.set(key, { acc: emptyAcc2(), models: /* @__PURE__ */ new Set() });
898
+ addEvent(grouped.get(key).acc, ev);
899
+ grouped.get(key).models.add(ev.model);
900
+ }
901
+ }
902
+ const blocks = [];
903
+ let idx = 0;
904
+ for (const [hourKey, { acc, models }] of grouped) {
905
+ const [datePart, timePart] = hourKey.split(" ");
906
+ const hour = timePart.split(":")[0];
907
+ blocks.push({
908
+ id: `openclaw-hour-${idx}`,
909
+ startTime: `${datePart}T${hour}:00:00`,
910
+ endTime: `${datePart}T${hour}:59:59`,
911
+ actualEndTime: null,
912
+ isActive: false,
913
+ isGap: false,
914
+ entries: acc.totalTokens > 0 ? 1 : 0,
915
+ tokenCounts: {
916
+ inputTokens: acc.inputTokens,
917
+ outputTokens: acc.outputTokens,
918
+ cacheCreationInputTokens: acc.cacheWriteTokens,
919
+ cacheReadInputTokens: acc.cacheReadTokens
920
+ },
921
+ totalTokens: acc.totalTokens,
922
+ costUSD: acc.cost,
923
+ models: [...models]
924
+ });
925
+ idx++;
926
+ }
927
+ blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
928
+ return { blocks };
929
+ }
930
+
931
+ // src/server/opencodeParser.ts
932
+ var import_node_fs4 = require("node:fs");
933
+ var import_node_child_process = require("node:child_process");
934
+ var import_node_path4 = require("node:path");
935
+ var import_node_os4 = require("node:os");
936
+ var OPENCODE_DB = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".local", "share", "opencode", "opencode.db");
937
+ function isOpencodeAccessible() {
938
+ return (0, import_node_fs4.existsSync)(OPENCODE_DB);
939
+ }
940
+ function queryOpenCodeDB(sql) {
941
+ return (0, import_node_child_process.execSync)(`sqlite3 -json "${OPENCODE_DB}" "${sql}"`, {
942
+ encoding: "utf-8",
943
+ maxBuffer: 50 * 1024 * 1024,
944
+ timeout: 1e4
945
+ });
946
+ }
947
+ function parseAllOpenCodeEvents(project) {
948
+ let sql = `SELECT data FROM message WHERE json_extract(data, '$.role') = 'assistant'`;
949
+ if (project) {
950
+ sql += ` AND json_extract(data, '$.path.cwd') = '${project.replace(/'/g, "''")}'`;
951
+ }
952
+ let raw;
953
+ try {
954
+ raw = queryOpenCodeDB(sql);
955
+ } catch {
956
+ return [];
957
+ }
958
+ let rows;
959
+ try {
960
+ rows = JSON.parse(raw);
961
+ } catch {
962
+ return [];
963
+ }
964
+ const events = [];
965
+ for (const row of rows) {
966
+ let data;
967
+ try {
968
+ data = JSON.parse(row.data);
969
+ } catch {
970
+ continue;
971
+ }
972
+ const tokens = data.tokens || {};
973
+ const cache2 = tokens.cache || {};
974
+ const time = data.time || {};
975
+ const path = data.path || {};
976
+ const input = Number(tokens.input ?? 0);
977
+ const output = Number(tokens.output ?? 0);
978
+ const cacheRead = Number(cache2.read ?? 0);
979
+ const cacheWrite = Number(cache2.write ?? 0);
980
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) continue;
981
+ events.push({
982
+ timestampMs: Number(time.created ?? 0),
983
+ inputTokens: Math.max(0, input),
984
+ outputTokens: Math.max(0, output),
985
+ cacheReadTokens: Math.max(0, cacheRead),
986
+ cacheWriteTokens: Math.max(0, cacheWrite),
987
+ totalTokens: Math.max(0, input + output + cacheRead),
988
+ cost: Math.max(0, Number(data.cost ?? 0)),
989
+ model: String(data.modelID ?? "unknown"),
990
+ project: String(path.cwd ?? "")
991
+ });
992
+ }
993
+ return events;
994
+ }
995
+ var TZ_OFFSETS3 = {
996
+ "Asia/Shanghai": 8,
997
+ "Asia/Tokyo": 9,
998
+ "America/New_York": -5,
999
+ "America/Los_Angeles": -8,
1000
+ "Europe/London": 0,
1001
+ "UTC": 0
1002
+ };
1003
+ function getTzOffsetHours3(tz) {
1004
+ return TZ_OFFSETS3[tz] ?? 8;
1005
+ }
1006
+ function msToLocalDate2(ms, tz) {
1007
+ return new Date(ms + getTzOffsetHours3(tz) * 36e5);
1008
+ }
1009
+ function getDateKey3(ms, tz) {
1010
+ return msToLocalDate2(ms, tz).toISOString().slice(0, 10);
1011
+ }
1012
+ function getHourKey3(ms, tz) {
1013
+ const d = msToLocalDate2(ms, tz);
1014
+ return d.toISOString().slice(0, 13).replace("T", " ") + ":00";
1015
+ }
1016
+ function emptyAcc3() {
1017
+ return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, cost: 0 };
1018
+ }
1019
+ function addEvent2(acc, ev) {
1020
+ acc.inputTokens += ev.inputTokens;
1021
+ acc.outputTokens += ev.outputTokens;
1022
+ acc.cacheReadTokens += ev.cacheReadTokens;
1023
+ acc.cacheWriteTokens += ev.cacheWriteTokens;
1024
+ acc.totalTokens += ev.totalTokens;
1025
+ acc.cost += ev.cost;
1026
+ }
1027
+ function getDailyResponse3(options) {
1028
+ const events = parseAllOpenCodeEvents(options?.project);
1029
+ const tz = options?.timezone || "Asia/Shanghai";
1030
+ const grouped = /* @__PURE__ */ new Map();
1031
+ for (const ev of events) {
1032
+ const key = getDateKey3(ev.timestampMs, tz);
1033
+ if (!grouped.has(key)) grouped.set(key, { totals: emptyAcc3(), models: /* @__PURE__ */ new Map() });
1034
+ const g = grouped.get(key);
1035
+ addEvent2(g.totals, ev);
1036
+ if (!g.models.has(ev.model)) {
1037
+ g.models.set(ev.model, { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, cost: 0 });
1038
+ }
1039
+ const m = g.models.get(ev.model);
1040
+ m.inputTokens += ev.inputTokens;
1041
+ m.outputTokens += ev.outputTokens;
1042
+ m.cacheCreationTokens += ev.cacheWriteTokens;
1043
+ m.cacheReadTokens += ev.cacheReadTokens;
1044
+ m.cost += ev.cost;
1045
+ }
1046
+ const totalsAcc = emptyAcc3();
1047
+ const daily = [];
1048
+ for (const [date, g] of grouped) {
1049
+ mergeAcc3(totalsAcc, g.totals);
1050
+ const modelList = [...g.models.keys()];
1051
+ daily.push({
1052
+ date,
1053
+ inputTokens: g.totals.inputTokens,
1054
+ outputTokens: g.totals.outputTokens,
1055
+ cacheCreationTokens: g.totals.cacheWriteTokens,
1056
+ cacheReadTokens: g.totals.cacheReadTokens,
1057
+ totalTokens: g.totals.totalTokens,
1058
+ totalCost: g.totals.cost,
1059
+ modelsUsed: modelList,
1060
+ modelBreakdowns: modelList.map((name) => {
1061
+ const m = g.models.get(name);
1062
+ return {
1063
+ modelName: name,
1064
+ inputTokens: m.inputTokens,
1065
+ outputTokens: m.outputTokens,
1066
+ cacheCreationTokens: m.cacheCreationTokens,
1067
+ cacheReadTokens: m.cacheReadTokens,
1068
+ cost: m.cost
1069
+ };
1070
+ })
1071
+ });
1072
+ }
1073
+ daily.sort((a, b) => a.date.localeCompare(b.date));
1074
+ return {
1075
+ daily,
1076
+ totals: {
1077
+ inputTokens: totalsAcc.inputTokens,
1078
+ outputTokens: totalsAcc.outputTokens,
1079
+ cacheCreationTokens: totalsAcc.cacheWriteTokens,
1080
+ cacheReadTokens: totalsAcc.cacheReadTokens,
1081
+ totalTokens: totalsAcc.totalTokens,
1082
+ totalCost: totalsAcc.cost
1083
+ }
1084
+ };
1085
+ }
1086
+ function mergeAcc3(a, b) {
1087
+ a.inputTokens += b.inputTokens;
1088
+ a.outputTokens += b.outputTokens;
1089
+ a.cacheReadTokens += b.cacheReadTokens;
1090
+ a.cacheWriteTokens += b.cacheWriteTokens;
1091
+ a.totalTokens += b.totalTokens;
1092
+ a.cost += b.cost;
1093
+ }
1094
+ function getProjectsResponse3(options) {
1095
+ const events = parseAllOpenCodeEvents();
1096
+ const tz = options?.timezone || "Asia/Shanghai";
1097
+ const projects = {};
1098
+ for (const ev of events) {
1099
+ const projectName = ev.project || "unknown";
1100
+ const dayKey = getDateKey3(ev.timestampMs, tz);
1101
+ if (!projects[projectName]) projects[projectName] = [];
1102
+ let dayEntry = projects[projectName].find((d) => d.date === dayKey);
1103
+ if (!dayEntry) {
1104
+ dayEntry = {
1105
+ date: dayKey,
1106
+ inputTokens: 0,
1107
+ outputTokens: 0,
1108
+ cacheCreationTokens: 0,
1109
+ cacheReadTokens: 0,
1110
+ totalTokens: 0,
1111
+ totalCost: 0,
1112
+ modelsUsed: [],
1113
+ modelBreakdowns: []
1114
+ };
1115
+ projects[projectName].push(dayEntry);
1116
+ }
1117
+ dayEntry.inputTokens += ev.inputTokens;
1118
+ dayEntry.outputTokens += ev.outputTokens;
1119
+ dayEntry.cacheCreationTokens += ev.cacheWriteTokens;
1120
+ dayEntry.cacheReadTokens += ev.cacheReadTokens;
1121
+ dayEntry.totalTokens += ev.totalTokens;
1122
+ dayEntry.totalCost += ev.cost;
1123
+ if (!dayEntry.modelsUsed.includes(ev.model)) {
1124
+ dayEntry.modelsUsed.push(ev.model);
1125
+ }
1126
+ let breakdown = dayEntry.modelBreakdowns.find((b) => b.modelName === ev.model);
1127
+ if (!breakdown) {
1128
+ breakdown = {
1129
+ modelName: ev.model,
1130
+ inputTokens: 0,
1131
+ outputTokens: 0,
1132
+ cacheCreationTokens: 0,
1133
+ cacheReadTokens: 0,
1134
+ cost: 0
1135
+ };
1136
+ dayEntry.modelBreakdowns.push(breakdown);
1137
+ }
1138
+ breakdown.inputTokens += ev.inputTokens;
1139
+ breakdown.outputTokens += ev.outputTokens;
1140
+ breakdown.cacheCreationTokens += ev.cacheWriteTokens;
1141
+ breakdown.cacheReadTokens += ev.cacheReadTokens;
1142
+ breakdown.cost += ev.cost;
1143
+ }
1144
+ for (const key of Object.keys(projects)) {
1145
+ projects[key].sort((a, b) => a.date.localeCompare(b.date));
1146
+ }
1147
+ return { projects };
1148
+ }
1149
+ function getBlocksResponse3(options) {
1150
+ const events = parseAllOpenCodeEvents(options?.project);
1151
+ const tz = options?.timezone || "Asia/Shanghai";
1152
+ const grouped = /* @__PURE__ */ new Map();
1153
+ for (const ev of events) {
1154
+ const key = getHourKey3(ev.timestampMs, tz);
1155
+ if (!grouped.has(key)) grouped.set(key, { acc: emptyAcc3(), models: /* @__PURE__ */ new Set() });
1156
+ addEvent2(grouped.get(key).acc, ev);
1157
+ grouped.get(key).models.add(ev.model);
1158
+ }
1159
+ const blocks = [];
1160
+ let idx = 0;
1161
+ for (const [hourKey, { acc, models }] of grouped) {
1162
+ const [datePart, timePart] = hourKey.split(" ");
1163
+ const hour = timePart.split(":")[0];
1164
+ blocks.push({
1165
+ id: `opencode-hour-${idx}`,
1166
+ startTime: `${datePart}T${hour}:00:00`,
1167
+ endTime: `${datePart}T${hour}:59:59`,
1168
+ actualEndTime: null,
1169
+ isActive: false,
1170
+ isGap: false,
1171
+ entries: acc.totalTokens > 0 ? 1 : 0,
1172
+ tokenCounts: {
1173
+ inputTokens: acc.inputTokens,
1174
+ outputTokens: acc.outputTokens,
1175
+ cacheCreationInputTokens: acc.cacheWriteTokens,
1176
+ cacheReadInputTokens: acc.cacheReadTokens
1177
+ },
1178
+ totalTokens: acc.totalTokens,
1179
+ costUSD: acc.cost,
1180
+ models: [...models]
1181
+ });
1182
+ idx++;
1183
+ }
1184
+ blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
1185
+ return { blocks };
1186
+ }
1187
+
1188
+ // src/server/claudeJsonlParser.ts
1189
+ var import_node_fs5 = require("node:fs");
1190
+ var import_node_path5 = require("node:path");
1191
+ var import_node_os5 = require("node:os");
1192
+ var MODEL_PRICING2 = {
1193
+ // Claude 4.6
1194
+ "claude-opus-4-6": { inputPer1M: 15, cacheCreationPer1M: 18.75, cacheReadPer1M: 1.5, outputPer1M: 75 },
1195
+ "claude-sonnet-4-6": { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.3, outputPer1M: 15 },
1196
+ // Claude 4.5
1197
+ "claude-sonnet-4-5-20250514": { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.3, outputPer1M: 15 },
1198
+ "claude-haiku-4-5-20251001": { inputPer1M: 0.8, cacheCreationPer1M: 1, cacheReadPer1M: 0.08, outputPer1M: 4 },
1199
+ // Older Claude models
1200
+ "claude-3-5-sonnet-20241022": { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.3, outputPer1M: 15 },
1201
+ "claude-3-5-haiku-20241022": { inputPer1M: 0.8, cacheCreationPer1M: 1, cacheReadPer1M: 0.08, outputPer1M: 4 },
1202
+ "claude-3-opus-20240229": { inputPer1M: 15, cacheCreationPer1M: 18.75, cacheReadPer1M: 1.5, outputPer1M: 75 },
1203
+ "claude-3-haiku-20240307": { inputPer1M: 0.25, cacheCreationPer1M: 0.3, cacheReadPer1M: 0.03, outputPer1M: 1.25 }
1204
+ };
1205
+ var DEFAULT_PRICING2 = { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.3, outputPer1M: 15 };
1206
+ function getPricing(model) {
1207
+ if (MODEL_PRICING2[model]) return MODEL_PRICING2[model];
1208
+ const lower = model.toLowerCase();
1209
+ for (const key of Object.keys(MODEL_PRICING2)) {
1210
+ if (lower.startsWith(key) || lower.includes(key)) return MODEL_PRICING2[key];
1211
+ }
1212
+ return DEFAULT_PRICING2;
1213
+ }
1214
+ function calculateCost2(inputTokens, cacheReadTokens, outputTokens, model, cacheCreationTokens = 0) {
1215
+ const p = getPricing(model);
1216
+ return inputTokens / 1e6 * p.inputPer1M + cacheCreationTokens / 1e6 * p.cacheCreationPer1M + cacheReadTokens / 1e6 * p.cacheReadPer1M + outputTokens / 1e6 * p.outputPer1M;
1217
+ }
1218
+ function totalClaudeTokens(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
1219
+ return inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
1220
+ }
1221
+ var CLAUDE_PROJECTS_DIR = (0, import_node_path5.join)((0, import_node_os5.homedir)(), ".claude", "projects");
1222
+ var fileCache = /* @__PURE__ */ new Map();
1223
+ var projectNameCache = /* @__PURE__ */ new Map();
1224
+ function extractProjectName2(dirName) {
1225
+ if (!dirName.startsWith("-")) return dirName;
1226
+ const cached = projectNameCache.get(dirName);
1227
+ if (cached) return cached;
1228
+ const segments = dirName.replace(/^-/, "").split("-").filter(Boolean);
1229
+ if (segments.length === 0) {
1230
+ projectNameCache.set(dirName, dirName);
1231
+ return dirName;
1232
+ }
1233
+ if (segments.length === 1) {
1234
+ projectNameCache.set(dirName, segments[0]);
1235
+ return segments[0];
1236
+ }
1237
+ let bestName = segments[segments.length - 1];
1238
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
1239
+ const parentSegments = segments.slice(0, splitAt);
1240
+ const candidateName = segments.slice(splitAt).join("-");
1241
+ let parentPath = "/";
1242
+ let valid = true;
1243
+ for (const seg of parentSegments) {
1244
+ const regular = (0, import_node_path5.join)(parentPath, seg);
1245
+ const hidden = (0, import_node_path5.join)(parentPath, "." + seg);
1246
+ if ((0, import_node_fs5.existsSync)(regular)) {
1247
+ parentPath = regular;
1248
+ } else if ((0, import_node_fs5.existsSync)(hidden)) {
1249
+ parentPath = hidden;
1250
+ } else {
1251
+ valid = false;
1252
+ break;
1253
+ }
1254
+ }
1255
+ if (!valid) continue;
1256
+ if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(parentPath, candidateName)) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(parentPath, "." + candidateName))) {
1257
+ bestName = candidateName;
1258
+ break;
1259
+ }
1260
+ }
1261
+ projectNameCache.set(dirName, bestName);
1262
+ return bestName;
1263
+ }
1264
+ function matchesProject(dirName, filter) {
1265
+ return extractProjectName2(dirName) === extractProjectName2(filter);
1266
+ }
1267
+ function findJsonlFiles(dir) {
1268
+ const results = [];
1269
+ try {
1270
+ const entries = (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true });
1271
+ for (const entry of entries) {
1272
+ if (entry.isDirectory()) {
1273
+ results.push(...findJsonlFiles((0, import_node_path5.join)(dir, entry.name)));
1274
+ } else if (entry.name.endsWith(".jsonl")) {
1275
+ results.push((0, import_node_path5.join)(dir, entry.name));
1276
+ }
1277
+ }
1278
+ } catch {
1279
+ }
1280
+ return results;
1281
+ }
1282
+ function parseAllSessions2(project) {
1283
+ if (!(0, import_node_fs5.existsSync)(CLAUDE_PROJECTS_DIR)) return [];
1284
+ const results = [];
1285
+ const projectDirs = (0, import_node_fs5.readdirSync)(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1286
+ for (const dirName of projectDirs) {
1287
+ if (project && !matchesProject(dirName, project)) continue;
1288
+ const dirPath = (0, import_node_path5.join)(CLAUDE_PROJECTS_DIR, dirName);
1289
+ const files = findJsonlFiles(dirPath);
1290
+ for (const filePath of files) {
1291
+ let mtime = 0;
1292
+ try {
1293
+ mtime = (0, import_node_fs5.statSync)(filePath).mtimeMs;
1294
+ } catch {
1295
+ }
1296
+ const cached = fileCache.get(filePath);
1297
+ if (cached && cached.mtime === mtime) {
1298
+ results.push(...cached.entries);
1299
+ continue;
1300
+ }
1301
+ const entries = [];
1302
+ let content;
1303
+ try {
1304
+ content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
1305
+ } catch {
1306
+ continue;
1307
+ }
1308
+ for (const line of content.split("\n")) {
1309
+ const trimmed = line.trim();
1310
+ if (!trimmed) continue;
1311
+ let obj;
1312
+ try {
1313
+ obj = JSON.parse(trimmed);
1314
+ } catch {
1315
+ continue;
1316
+ }
1317
+ if (obj.type !== "assistant" || !obj.message) continue;
1318
+ const msg = obj.message;
1319
+ const usage = msg.usage || {};
1320
+ const inputTokens = usage.input_tokens || 0;
1321
+ const outputTokens = usage.output_tokens || 0;
1322
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
1323
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
1324
+ const totalTokens = totalClaudeTokens(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens);
1325
+ if (totalTokens === 0) continue;
1326
+ entries.push({
1327
+ timestamp: obj.timestamp,
1328
+ model: msg.model || "unknown",
1329
+ inputTokens,
1330
+ outputTokens,
1331
+ cacheCreationTokens,
1332
+ cacheReadTokens,
1333
+ projectDir: dirName
1334
+ });
1335
+ }
1336
+ fileCache.set(filePath, { mtime, entries });
1337
+ results.push(...entries);
1338
+ }
1339
+ }
1340
+ return results;
1341
+ }
1342
+ var TZ_OFFSETS4 = {
1343
+ "Asia/Shanghai": 8,
1344
+ "Asia/Tokyo": 9,
1345
+ "America/New_York": -5,
1346
+ "America/Los_Angeles": -8,
1347
+ "Europe/London": 0,
1348
+ "UTC": 0
1349
+ };
1350
+ function getDateKey4(timestamp, tz) {
1351
+ const offset = (TZ_OFFSETS4[tz] ?? 8) * 36e5;
1352
+ const d = new Date(new Date(timestamp).getTime() + offset);
1353
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
1354
+ }
1355
+ function getHourKey4(timestamp, tz) {
1356
+ const offset = (TZ_OFFSETS4[tz] ?? 8) * 36e5;
1357
+ const d = new Date(new Date(timestamp).getTime() + offset);
1358
+ const yyyy = d.getUTCFullYear();
1359
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
1360
+ const dd = String(d.getUTCDate()).padStart(2, "0");
1361
+ const hh = String(d.getUTCHours()).padStart(2, "0");
1362
+ return `${yyyy}-${mm}-${dd}T${hh}`;
1363
+ }
1364
+ function toDailyEntry(agg) {
1365
+ const modelBreakdowns = [...agg.models.entries()].map(([modelName, m]) => ({
1366
+ modelName,
1367
+ inputTokens: m.input,
1368
+ outputTokens: m.output,
1369
+ cacheCreationTokens: m.cacheCreation,
1370
+ cacheReadTokens: m.cacheRead,
1371
+ cost: m.cost
1372
+ }));
1373
+ return {
1374
+ date: agg.date,
1375
+ inputTokens: agg.inputTokens,
1376
+ outputTokens: agg.outputTokens,
1377
+ cacheCreationTokens: agg.cacheCreationTokens,
1378
+ cacheReadTokens: agg.cacheReadTokens,
1379
+ totalTokens: agg.totalTokens,
1380
+ totalCost: Math.round(agg.totalCost * 1e4) / 1e4,
1381
+ modelsUsed: [...agg.models.keys()],
1382
+ modelBreakdowns
1383
+ };
1384
+ }
1385
+ var DEFAULT_TZ = "Asia/Shanghai";
1386
+ function getDailyResponse4(project, tz = DEFAULT_TZ) {
1387
+ const entries = parseAllSessions2(project);
1388
+ const dayMap = /* @__PURE__ */ new Map();
1389
+ for (const e of entries) {
1390
+ const date = getDateKey4(e.timestamp, tz);
1391
+ if (!dayMap.has(date)) {
1392
+ dayMap.set(date, {
1393
+ date,
1394
+ inputTokens: 0,
1395
+ outputTokens: 0,
1396
+ cacheCreationTokens: 0,
1397
+ cacheReadTokens: 0,
1398
+ totalTokens: 0,
1399
+ totalCost: 0,
1400
+ models: /* @__PURE__ */ new Map()
1401
+ });
1402
+ }
1403
+ const agg = dayMap.get(date);
1404
+ agg.inputTokens += e.inputTokens;
1405
+ agg.outputTokens += e.outputTokens;
1406
+ agg.cacheCreationTokens += e.cacheCreationTokens;
1407
+ agg.cacheReadTokens += e.cacheReadTokens;
1408
+ agg.totalTokens += totalClaudeTokens(e.inputTokens, e.outputTokens, e.cacheCreationTokens, e.cacheReadTokens);
1409
+ const cost = calculateCost2(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model, e.cacheCreationTokens);
1410
+ agg.totalCost += cost;
1411
+ if (!agg.models.has(e.model)) {
1412
+ agg.models.set(e.model, { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, cost: 0 });
1413
+ }
1414
+ const m = agg.models.get(e.model);
1415
+ m.input += e.inputTokens;
1416
+ m.output += e.outputTokens;
1417
+ m.cacheCreation += e.cacheCreationTokens;
1418
+ m.cacheRead += e.cacheReadTokens;
1419
+ m.cost += cost;
1420
+ }
1421
+ const daily = [...dayMap.values()].sort((a, b) => a.date.localeCompare(b.date)).map(toDailyEntry);
1422
+ const totals = daily.reduce((acc, d) => ({
1423
+ inputTokens: acc.inputTokens + d.inputTokens,
1424
+ outputTokens: acc.outputTokens + d.outputTokens,
1425
+ cacheCreationTokens: acc.cacheCreationTokens + d.cacheCreationTokens,
1426
+ cacheReadTokens: acc.cacheReadTokens + d.cacheReadTokens,
1427
+ totalTokens: acc.totalTokens + d.totalTokens,
1428
+ totalCost: acc.totalCost + d.totalCost
1429
+ }), { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 0, totalCost: 0 });
1430
+ return { daily, totals };
1431
+ }
1432
+ function getProjectsResponse4(tz = DEFAULT_TZ) {
1433
+ const entries = parseAllSessions2();
1434
+ const projectMap = /* @__PURE__ */ new Map();
1435
+ for (const e of entries) {
1436
+ const date = getDateKey4(e.timestamp, tz);
1437
+ const projectName = extractProjectName2(e.projectDir);
1438
+ if (!projectMap.has(projectName)) {
1439
+ projectMap.set(projectName, /* @__PURE__ */ new Map());
1440
+ }
1441
+ const dayMap = projectMap.get(projectName);
1442
+ if (!dayMap.has(date)) {
1443
+ dayMap.set(date, {
1444
+ date,
1445
+ inputTokens: 0,
1446
+ outputTokens: 0,
1447
+ cacheCreationTokens: 0,
1448
+ cacheReadTokens: 0,
1449
+ totalTokens: 0,
1450
+ totalCost: 0,
1451
+ models: /* @__PURE__ */ new Map()
1452
+ });
1453
+ }
1454
+ const agg = dayMap.get(date);
1455
+ agg.inputTokens += e.inputTokens;
1456
+ agg.outputTokens += e.outputTokens;
1457
+ agg.cacheCreationTokens += e.cacheCreationTokens;
1458
+ agg.cacheReadTokens += e.cacheReadTokens;
1459
+ agg.totalTokens += totalClaudeTokens(e.inputTokens, e.outputTokens, e.cacheCreationTokens, e.cacheReadTokens);
1460
+ const cost = calculateCost2(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model, e.cacheCreationTokens);
1461
+ agg.totalCost += cost;
1462
+ if (!agg.models.has(e.model)) {
1463
+ agg.models.set(e.model, { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, cost: 0 });
1464
+ }
1465
+ const m = agg.models.get(e.model);
1466
+ m.input += e.inputTokens;
1467
+ m.output += e.outputTokens;
1468
+ m.cacheCreation += e.cacheCreationTokens;
1469
+ m.cacheRead += e.cacheReadTokens;
1470
+ m.cost += cost;
1471
+ }
1472
+ const projects = {};
1473
+ for (const [projectName, dayMap] of projectMap) {
1474
+ projects[projectName] = [...dayMap.values()].sort((a, b) => a.date.localeCompare(b.date)).map(toDailyEntry);
1475
+ }
1476
+ return { projects };
1477
+ }
1478
+ function getBlocksResponse4(project, tz = DEFAULT_TZ) {
1479
+ const entries = parseAllSessions2(project);
1480
+ const hourMap = /* @__PURE__ */ new Map();
1481
+ for (const e of entries) {
1482
+ const hourKey = getHourKey4(e.timestamp, tz);
1483
+ if (!hourMap.has(hourKey)) {
1484
+ hourMap.set(hourKey, {
1485
+ inputTokens: 0,
1486
+ outputTokens: 0,
1487
+ cacheCreationTokens: 0,
1488
+ cacheReadTokens: 0,
1489
+ costUSD: 0,
1490
+ models: /* @__PURE__ */ new Set()
1491
+ });
1492
+ }
1493
+ const bucket = hourMap.get(hourKey);
1494
+ bucket.inputTokens += e.inputTokens;
1495
+ bucket.outputTokens += e.outputTokens;
1496
+ bucket.cacheCreationTokens += e.cacheCreationTokens;
1497
+ bucket.cacheReadTokens += e.cacheReadTokens;
1498
+ bucket.costUSD += calculateCost2(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model, e.cacheCreationTokens);
1499
+ bucket.models.add(e.model);
1500
+ }
1501
+ const blocks = [];
1502
+ let idx = 0;
1503
+ for (const [hourKey, bucket] of hourMap) {
1504
+ const totalTokens = totalClaudeTokens(bucket.inputTokens, bucket.outputTokens, bucket.cacheCreationTokens, bucket.cacheReadTokens);
1505
+ blocks.push({
1506
+ id: `claude-${idx}`,
1507
+ startTime: `${hourKey}:00:00`,
1508
+ endTime: `${hourKey}:59:59`,
1509
+ actualEndTime: null,
1510
+ isActive: false,
1511
+ isGap: false,
1512
+ entries: totalTokens > 0 ? 1 : 0,
1513
+ tokenCounts: {
1514
+ inputTokens: bucket.inputTokens,
1515
+ outputTokens: bucket.outputTokens,
1516
+ cacheCreationInputTokens: bucket.cacheCreationTokens,
1517
+ cacheReadInputTokens: bucket.cacheReadTokens
1518
+ },
1519
+ totalTokens,
1520
+ costUSD: Math.round(bucket.costUSD * 1e4) / 1e4,
1521
+ models: [...bucket.models]
1522
+ });
1523
+ idx++;
1524
+ }
1525
+ blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
1526
+ return { blocks };
1527
+ }
1528
+
1529
+ // src/server/routes/daily.ts
1530
+ async function getDaily(req, res) {
1531
+ const agent = req.query.agent || "claude";
1532
+ const cacheKey = `daily:${agent}`;
1533
+ try {
1534
+ const cached = cache.get(cacheKey);
1535
+ if (cached) {
1536
+ res.json(cached);
1537
+ return;
1538
+ }
1539
+ const stale = cache.getStale(cacheKey);
1540
+ if (stale) {
1541
+ refreshDailyCache(agent, cacheKey);
1542
+ res.json(stale);
1543
+ return;
1544
+ }
1545
+ const data = await fetchDailyData(agent);
1546
+ cache.set(cacheKey, data);
1547
+ res.json(data);
1548
+ } catch (error) {
1549
+ const message = error instanceof Error ? error.message : "Unknown error";
1550
+ console.error("Error fetching daily data:", error);
1551
+ res.status(502).json({
1552
+ error: `Failed to fetch daily data from ${agent}`,
1553
+ hint: message
1554
+ });
1555
+ }
1556
+ }
1557
+ function fetchDailyData(agent) {
1558
+ if (agent === "codex") {
1559
+ return Promise.resolve(getDailyResponse());
1560
+ } else if (agent === "openclaw") {
1561
+ return Promise.resolve(validateDaily(getDailyResponse2()));
1562
+ } else if (agent === "opencode") {
1563
+ return Promise.resolve(validateDaily(getDailyResponse3()));
1564
+ } else {
1565
+ return Promise.resolve(validateDaily(getDailyResponse4()));
1566
+ }
1567
+ }
1568
+ function refreshDailyCache(agent, cacheKey) {
1569
+ fetchDailyData(agent).then((data) => cache.set(cacheKey, data)).catch((err) => console.error("Background refresh failed (daily):", err));
1570
+ }
1571
+
1572
+ // src/server/routes/monthly.ts
1573
+ async function getMonthly(req, res) {
1574
+ const agent = req.query.agent || "claude";
1575
+ const cacheKey = `monthly:${agent}`;
1576
+ try {
1577
+ const cached = cache.get(cacheKey);
1578
+ if (cached) {
1579
+ res.json(cached);
1580
+ return;
1581
+ }
1582
+ const stale = cache.getStale(cacheKey);
1583
+ if (stale) {
1584
+ res.json(stale);
1585
+ return;
1586
+ }
1587
+ let data;
1588
+ if (agent === "codex") {
1589
+ data = validateDaily(getDailyResponse());
1590
+ } else if (agent === "openclaw") {
1591
+ data = validateDaily(getDailyResponse2());
1592
+ } else {
1593
+ data = validateDaily(getDailyResponse4());
1594
+ }
1595
+ cache.set(cacheKey, data);
1596
+ res.json(data);
1597
+ } catch (error) {
1598
+ const message = error instanceof Error ? error.message : "Unknown error";
1599
+ console.error("Error fetching monthly data:", error);
1600
+ res.status(502).json({
1601
+ error: "Failed to fetch monthly data",
1602
+ hint: message
1603
+ });
1604
+ }
1605
+ }
1606
+
1607
+ // src/server/routes/session.ts
1608
+ async function getSession(req, res) {
1609
+ const agent = req.query.agent || "claude";
1610
+ const cacheKey = `session:${agent}`;
1611
+ try {
1612
+ const cached = cache.get(cacheKey);
1613
+ if (cached) {
1614
+ res.json(cached);
1615
+ return;
1616
+ }
1617
+ const stale = cache.getStale(cacheKey);
1618
+ if (stale) {
1619
+ res.json(stale);
1620
+ return;
1621
+ }
1622
+ let data;
1623
+ if (agent === "codex") {
1624
+ data = validateDaily(getDailyResponse());
1625
+ } else if (agent === "openclaw") {
1626
+ data = validateDaily(getDailyResponse2());
1627
+ } else {
1628
+ data = validateDaily(getDailyResponse4());
1629
+ }
1630
+ cache.set(cacheKey, data);
1631
+ res.json(data);
1632
+ } catch (error) {
1633
+ const message = error instanceof Error ? error.message : "Unknown error";
1634
+ console.error("Error fetching session data:", error);
1635
+ res.status(502).json({
1636
+ error: "Failed to fetch session data",
1637
+ hint: message
1638
+ });
1639
+ }
1640
+ }
1641
+
1642
+ // src/server/routes/projects.ts
1643
+ async function getProjects(req, res) {
1644
+ const agent = req.query.agent || "claude";
1645
+ const cacheKey = `projects:${agent}`;
1646
+ try {
1647
+ const cached = cache.get(cacheKey);
1648
+ if (cached) {
1649
+ res.json(cached);
1650
+ return;
1651
+ }
1652
+ const stale = cache.getStale(cacheKey);
1653
+ if (stale) {
1654
+ refreshProjectsCache(agent, cacheKey);
1655
+ res.json(stale);
1656
+ return;
1657
+ }
1658
+ const data = fetchProjectsData(agent);
1659
+ cache.set(cacheKey, data);
1660
+ res.json(data);
1661
+ } catch (error) {
1662
+ const message = error instanceof Error ? error.message : "Unknown error";
1663
+ console.error("Error fetching projects data:", error);
1664
+ res.status(502).json({
1665
+ error: `Failed to fetch projects data from ${agent}`,
1666
+ hint: message
1667
+ });
1668
+ }
1669
+ }
1670
+ function fetchProjectsData(agent) {
1671
+ if (agent === "codex") {
1672
+ return getProjectsResponse();
1673
+ } else if (agent === "openclaw") {
1674
+ return validateProjects(getProjectsResponse2());
1675
+ } else if (agent === "opencode") {
1676
+ return validateProjects(getProjectsResponse3());
1677
+ } else {
1678
+ return validateProjects(getProjectsResponse4());
1679
+ }
1680
+ }
1681
+ function refreshProjectsCache(agent, cacheKey) {
1682
+ Promise.resolve().then(() => {
1683
+ const data = fetchProjectsData(agent);
1684
+ cache.set(cacheKey, data);
1685
+ }).catch((err) => console.error("Background refresh failed (projects):", err));
1686
+ }
1687
+
1688
+ // src/server/routes/blocks.ts
1689
+ async function getBlocks(req, res) {
1690
+ const agent = req.query.agent || "claude";
1691
+ const project = req.query.project || void 0;
1692
+ try {
1693
+ const cacheKey = `blocks:${agent}:${project || "all"}`;
1694
+ const cached = cache.get(cacheKey);
1695
+ if (cached) {
1696
+ res.json(cached);
1697
+ return;
1698
+ }
1699
+ const stale = cache.getStale(cacheKey);
1700
+ if (stale) {
1701
+ refreshBlocksCache(agent, project, cacheKey);
1702
+ res.json(stale);
1703
+ return;
1704
+ }
1705
+ const data = fetchBlocksData(agent, project);
1706
+ cache.set(cacheKey, data);
1707
+ res.json(data);
1708
+ } catch (error) {
1709
+ const message = error instanceof Error ? error.message : "Unknown error";
1710
+ console.error("Error fetching blocks data:", error);
1711
+ res.status(502).json({
1712
+ error: "Failed to fetch blocks data",
1713
+ hint: message
1714
+ });
1715
+ }
1716
+ }
1717
+ function fetchBlocksData(agent, project) {
1718
+ if (agent === "openclaw") {
1719
+ return validateBlocks(getBlocksResponse2({ project: project || null }));
1720
+ } else if (agent === "opencode") {
1721
+ return validateBlocks(getBlocksResponse3({ project: project || null }));
1722
+ } else if (agent === "codex") {
1723
+ return validateBlocks(getBlocksResponse({ project: project || null }));
1724
+ } else {
1725
+ return validateBlocks(getBlocksResponse4(project || null));
1726
+ }
1727
+ }
1728
+ function refreshBlocksCache(agent, project, cacheKey) {
1729
+ Promise.resolve().then(() => {
1730
+ const data = fetchBlocksData(agent, project);
1731
+ cache.set(cacheKey, data);
1732
+ }).catch((err) => console.error("Background refresh failed (blocks):", err));
1733
+ }
1734
+
1735
+ // src/server/analyticsParser.ts
1736
+ var import_node_fs6 = require("node:fs");
1737
+ var import_node_path6 = require("node:path");
1738
+ var import_node_os6 = require("node:os");
1739
+ var TZ_OFFSETS5 = {
1740
+ "Asia/Shanghai": 8,
1741
+ "Asia/Tokyo": 9,
1742
+ "America/New_York": -5,
1743
+ "America/Los_Angeles": -8,
1744
+ "Europe/London": 0,
1745
+ "UTC": 0
1746
+ };
1747
+ function getDateKey5(ms, tz) {
1748
+ const offset = (TZ_OFFSETS5[tz] ?? 8) * 36e5;
1749
+ const d = new Date(ms + offset);
1750
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
1751
+ }
1752
+ function normalizeToolName(name) {
1753
+ const lower = name.toLowerCase();
1754
+ if (lower.startsWith("mcp__")) {
1755
+ const parts = name.split("__");
1756
+ const serverPart = parts.length >= 3 ? parts[2] : "mcp";
1757
+ return `MCP:${serverPart}`;
1758
+ }
1759
+ const mapping = {
1760
+ "exec": "Bash",
1761
+ "read": "Read",
1762
+ "edit": "Edit",
1763
+ "write": "Write"
1764
+ };
1765
+ return mapping[lower] || name;
1766
+ }
1767
+ function countLines(text) {
1768
+ if (!text) return 0;
1769
+ return text.split("\n").length;
1770
+ }
1771
+ var CLAUDE_PROJECTS_DIR2 = (0, import_node_path6.join)((0, import_node_os6.homedir)(), ".claude", "projects");
1772
+ var projectNameCache2 = /* @__PURE__ */ new Map();
1773
+ function extractProjectName3(dirName) {
1774
+ if (!dirName.startsWith("-")) return dirName;
1775
+ const cached = projectNameCache2.get(dirName);
1776
+ if (cached) return cached;
1777
+ const segments = dirName.replace(/^-/, "").split("-").filter(Boolean);
1778
+ if (segments.length === 0) {
1779
+ projectNameCache2.set(dirName, dirName);
1780
+ return dirName;
1781
+ }
1782
+ if (segments.length === 1) {
1783
+ projectNameCache2.set(dirName, segments[0]);
1784
+ return segments[0];
1785
+ }
1786
+ let bestName = segments[segments.length - 1];
1787
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
1788
+ const parentSegments = segments.slice(0, splitAt);
1789
+ const candidateName = segments.slice(splitAt).join("-");
1790
+ let parentPath = "/";
1791
+ let valid = true;
1792
+ for (const seg of parentSegments) {
1793
+ const regular = (0, import_node_path6.join)(parentPath, seg);
1794
+ const hidden = (0, import_node_path6.join)(parentPath, "." + seg);
1795
+ if ((0, import_node_fs6.existsSync)(regular)) {
1796
+ parentPath = regular;
1797
+ } else if ((0, import_node_fs6.existsSync)(hidden)) {
1798
+ parentPath = hidden;
1799
+ } else {
1800
+ valid = false;
1801
+ break;
1802
+ }
1803
+ }
1804
+ if (!valid) continue;
1805
+ if ((0, import_node_fs6.existsSync)((0, import_node_path6.join)(parentPath, candidateName)) || (0, import_node_fs6.existsSync)((0, import_node_path6.join)(parentPath, "." + candidateName))) {
1806
+ bestName = candidateName;
1807
+ break;
1808
+ }
1809
+ }
1810
+ projectNameCache2.set(dirName, bestName);
1811
+ return bestName;
1812
+ }
1813
+ function matchesProject2(dirName, filter) {
1814
+ return extractProjectName3(dirName) === extractProjectName3(filter);
1815
+ }
1816
+ var claudeSessionCache = /* @__PURE__ */ new Map();
1817
+ function extractClaudeToolCalls(project) {
1818
+ if (!(0, import_node_fs6.existsSync)(CLAUDE_PROJECTS_DIR2)) return [];
1819
+ const results = [];
1820
+ const projectDirs = (0, import_node_fs6.readdirSync)(CLAUDE_PROJECTS_DIR2, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1821
+ for (const dirName of projectDirs) {
1822
+ if (project && !matchesProject2(dirName, project)) continue;
1823
+ const dirPath = (0, import_node_path6.join)(CLAUDE_PROJECTS_DIR2, dirName);
1824
+ let files;
1825
+ try {
1826
+ files = (0, import_node_fs6.readdirSync)(dirPath).filter((f) => f.endsWith(".jsonl"));
1827
+ } catch {
1828
+ continue;
1829
+ }
1830
+ for (const file of files) {
1831
+ const filePath = (0, import_node_path6.join)(dirPath, file);
1832
+ let mtime = 0;
1833
+ try {
1834
+ mtime = (0, import_node_fs6.statSync)(filePath).mtimeMs;
1835
+ } catch {
1836
+ }
1837
+ const cached = claudeSessionCache.get(filePath);
1838
+ if (cached && cached.mtime === mtime) {
1839
+ results.push(...cached.toolCalls);
1840
+ continue;
1841
+ }
1842
+ const toolCalls = [];
1843
+ let content;
1844
+ try {
1845
+ content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
1846
+ } catch {
1847
+ continue;
1848
+ }
1849
+ for (const line of content.split("\n")) {
1850
+ const trimmed = line.trim();
1851
+ if (!trimmed) continue;
1852
+ let obj;
1853
+ try {
1854
+ obj = JSON.parse(trimmed);
1855
+ } catch {
1856
+ continue;
1857
+ }
1858
+ if (obj.type !== "assistant" || !obj.message) continue;
1859
+ const msg = obj.message;
1860
+ const timestamp = new Date(obj.timestamp).getTime();
1861
+ const content_arr = msg.content;
1862
+ if (!content_arr) continue;
1863
+ for (const item of content_arr) {
1864
+ if (item.type !== "tool_use") continue;
1865
+ const toolName = normalizeToolName(item.name);
1866
+ const input = item.input || {};
1867
+ let linesAdded = 0;
1868
+ let linesDeleted = 0;
1869
+ const filePath2 = input.file_path || void 0;
1870
+ if (toolName === "Edit") {
1871
+ linesDeleted = countLines(input.old_string || "");
1872
+ linesAdded = countLines(input.new_string || "");
1873
+ } else if (toolName === "Write") {
1874
+ linesAdded = countLines(input.content || "");
1875
+ }
1876
+ toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
1877
+ }
1878
+ }
1879
+ claudeSessionCache.set(filePath, { mtime, toolCalls });
1880
+ results.push(...toolCalls);
1881
+ }
1882
+ }
1883
+ return results;
1884
+ }
1885
+ var openclawSessionCache = /* @__PURE__ */ new Map();
1886
+ function extractOpenClawToolCalls(project) {
1887
+ const results = [];
1888
+ const refs = scanOpenClawSessions();
1889
+ for (const ref of refs) {
1890
+ if (project && ref.agentId !== project) continue;
1891
+ let mtime = 0;
1892
+ try {
1893
+ mtime = (0, import_node_fs6.statSync)(ref.sessionFile).mtimeMs;
1894
+ } catch {
1895
+ }
1896
+ const cached = openclawSessionCache.get(ref.sessionFile);
1897
+ if (cached && cached.mtime === mtime) {
1898
+ results.push(...cached.toolCalls);
1899
+ continue;
1900
+ }
1901
+ const toolCalls = [];
1902
+ let content;
1903
+ try {
1904
+ content = (0, import_node_fs6.readFileSync)(ref.sessionFile, "utf-8");
1905
+ } catch {
1906
+ continue;
1907
+ }
1908
+ for (const line of content.split("\n")) {
1909
+ const trimmed = line.trim();
1910
+ if (!trimmed) continue;
1911
+ let obj;
1912
+ try {
1913
+ obj = JSON.parse(trimmed);
1914
+ } catch {
1915
+ continue;
1916
+ }
1917
+ if (obj.type !== "message") continue;
1918
+ const msg = obj.message;
1919
+ if (msg.role !== "assistant") continue;
1920
+ const timestamp = Number(msg.timestamp ?? 0);
1921
+ const content_arr = msg.content;
1922
+ if (!content_arr) continue;
1923
+ for (const item of content_arr) {
1924
+ if (item.type !== "toolCall") continue;
1925
+ const toolName = normalizeToolName(item.name);
1926
+ const args = item.arguments || {};
1927
+ let linesAdded = 0;
1928
+ let linesDeleted = 0;
1929
+ const filePath2 = args.path || void 0;
1930
+ if (toolName === "Edit") {
1931
+ linesDeleted = countLines(args.oldText || "");
1932
+ linesAdded = countLines(args.newText || "");
1933
+ } else if (toolName === "Write") {
1934
+ linesAdded = countLines(args.content || "");
1935
+ }
1936
+ toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
1937
+ }
1938
+ }
1939
+ openclawSessionCache.set(ref.sessionFile, { mtime, toolCalls });
1940
+ results.push(...toolCalls);
1941
+ }
1942
+ return results;
1943
+ }
1944
+ function computeAnalytics(toolCalls, timezone = "Asia/Shanghai") {
1945
+ const changeMap = /* @__PURE__ */ new Map();
1946
+ for (const tc of toolCalls) {
1947
+ if (tc.linesAdded === 0 && tc.linesDeleted === 0) continue;
1948
+ const key = getDateKey5(tc.timestamp, timezone);
1949
+ if (!changeMap.has(key)) changeMap.set(key, { added: 0, deleted: 0, files: /* @__PURE__ */ new Set() });
1950
+ const entry = changeMap.get(key);
1951
+ entry.added += tc.linesAdded;
1952
+ entry.deleted += tc.linesDeleted;
1953
+ if (tc.filePath) entry.files.add(tc.filePath);
1954
+ }
1955
+ const codeChangeTrend = [];
1956
+ for (const [date, { added, deleted, files }] of changeMap) {
1957
+ codeChangeTrend.push({ date, linesAdded: added, linesDeleted: deleted, netChange: added - deleted, filesModified: files.size });
1958
+ }
1959
+ codeChangeTrend.sort((a, b) => a.date.localeCompare(b.date));
1960
+ const toolCountMap = /* @__PURE__ */ new Map();
1961
+ for (const tc of toolCalls) {
1962
+ toolCountMap.set(tc.toolName, (toolCountMap.get(tc.toolName) || 0) + 1);
1963
+ }
1964
+ const toolUsageDistribution = [...toolCountMap.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
1965
+ const editCalls = toolCalls.filter((tc) => tc.toolName === "Edit" || tc.toolName === "Write");
1966
+ const totalEdits = editCalls.length;
1967
+ const totalLinesChanged = editCalls.reduce((s, tc) => s + tc.linesAdded + tc.linesDeleted, 0);
1968
+ const totalLinesAdded = editCalls.reduce((s, tc) => s + tc.linesAdded, 0);
1969
+ const totalLinesDeleted = editCalls.reduce((s, tc) => s + tc.linesDeleted, 0);
1970
+ const uniqueFiles = new Set(editCalls.filter((tc) => tc.filePath).map((tc) => tc.filePath));
1971
+ const editDates = new Set(editCalls.map((tc) => getDateKey5(tc.timestamp, timezone)));
1972
+ const productivityKPIs = {
1973
+ avgLinesPerEdit: totalEdits > 0 ? Math.round(totalLinesChanged / totalEdits) : 0,
1974
+ filesModifiedPerDay: editDates.size > 0 ? Math.round(uniqueFiles.size / editDates.size) : 0,
1975
+ addDeleteRatio: totalLinesDeleted > 0 ? Math.round(totalLinesAdded / totalLinesDeleted * 100) / 100 : totalLinesAdded > 0 ? 1 : 0,
1976
+ totalEdits,
1977
+ totalFilesModified: uniqueFiles.size,
1978
+ activeDaysWithEdits: editDates.size
1979
+ };
1980
+ const trendMap = /* @__PURE__ */ new Map();
1981
+ for (const tc of toolCalls) {
1982
+ const date = getDateKey5(tc.timestamp, timezone);
1983
+ if (!trendMap.has(date)) trendMap.set(date, /* @__PURE__ */ new Map());
1984
+ const dayMap = trendMap.get(date);
1985
+ dayMap.set(tc.toolName, (dayMap.get(tc.toolName) || 0) + 1);
1986
+ }
1987
+ const toolCallTrend = [];
1988
+ for (const [date, dayMap] of trendMap) {
1989
+ const entry = { date };
1990
+ for (const [tool, count] of dayMap) {
1991
+ entry[tool] = count;
1992
+ }
1993
+ toolCallTrend.push(entry);
1994
+ }
1995
+ toolCallTrend.sort((a, b) => a.date.localeCompare(b.date));
1996
+ if (toolCallTrend.length > 0) {
1997
+ const allTools = /* @__PURE__ */ new Set();
1998
+ for (const entry of toolCallTrend) {
1999
+ for (const key of Object.keys(entry)) {
2000
+ if (key !== "date") allTools.add(key);
2001
+ }
2002
+ }
2003
+ for (const entry of toolCallTrend) {
2004
+ for (const tool of allTools) {
2005
+ if (entry[tool] === void 0) {
2006
+ entry[tool] = 0;
2007
+ }
2008
+ }
2009
+ }
2010
+ }
2011
+ return { codeChangeTrend, toolUsageDistribution, productivityKPIs, toolCallTrend };
2012
+ }
2013
+
2014
+ // src/server/routes/analytics.ts
2015
+ var EMPTY_ANALYTICS = {
2016
+ codeChangeTrend: [],
2017
+ toolUsageDistribution: [],
2018
+ productivityKPIs: { avgLinesPerEdit: 0, filesModifiedPerDay: 0, addDeleteRatio: 0, totalEdits: 0, totalFilesModified: 0, activeDaysWithEdits: 0 },
2019
+ toolCallTrend: []
2020
+ };
2021
+ async function getAnalytics(req, res) {
2022
+ const agent = req.query.agent || "claude";
2023
+ const project = req.query.project || void 0;
2024
+ if (agent === "codex" || agent === "opencode") {
2025
+ res.json(EMPTY_ANALYTICS);
2026
+ return;
2027
+ }
2028
+ try {
2029
+ const cacheKey = `analytics:${agent}:${project || "all"}`;
2030
+ const cached = cache.get(cacheKey);
2031
+ if (cached) {
2032
+ res.json(cached);
2033
+ return;
2034
+ }
2035
+ const toolCalls = agent === "openclaw" ? extractOpenClawToolCalls(project || null) : extractClaudeToolCalls(project || null);
2036
+ const data = computeAnalytics(toolCalls);
2037
+ const validated = validateAnalytics(data);
2038
+ cache.set(cacheKey, validated);
2039
+ res.json(validated);
2040
+ } catch (error) {
2041
+ const message = error instanceof Error ? error.message : "Unknown error";
2042
+ console.error("Error fetching analytics:", error);
2043
+ res.status(502).json({
2044
+ error: `Failed to fetch analytics from ${agent}`,
2045
+ hint: message
2046
+ });
2047
+ }
2048
+ }
2049
+
2050
+ // src/server/agentDetection.ts
2051
+ var import_node_fs7 = require("node:fs");
2052
+ var import_node_path7 = require("node:path");
2053
+ var import_node_os7 = require("node:os");
2054
+ var CLAUDE_PROJECTS_DIR3 = (0, import_node_path7.join)((0, import_node_os7.homedir)(), ".claude", "projects");
2055
+ var CODEX_SESSIONS_DIR = (0, import_node_path7.join)((0, import_node_os7.homedir)(), ".codex", "sessions");
2056
+ function isClaudeCodeAvailable() {
2057
+ if (!(0, import_node_fs7.existsSync)(CLAUDE_PROJECTS_DIR3)) return false;
2058
+ try {
2059
+ const dirs = (0, import_node_fs7.readdirSync)(CLAUDE_PROJECTS_DIR3, { withFileTypes: true });
2060
+ return dirs.some((d) => d.isDirectory());
2061
+ } catch {
2062
+ return false;
2063
+ }
2064
+ }
2065
+ function isCodexAvailable() {
2066
+ return (0, import_node_fs7.existsSync)(CODEX_SESSIONS_DIR);
2067
+ }
2068
+ function isOpencodeAvailable() {
2069
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), ".local", "share", "opencode", "opencode.db"));
2070
+ }
2071
+ function detectAvailableAgents() {
2072
+ return {
2073
+ claude: isClaudeCodeAvailable(),
2074
+ codex: isCodexAvailable(),
2075
+ opencode: isOpencodeAvailable()
2076
+ };
2077
+ }
2078
+
2079
+ // src/server/quota/adapter.ts
2080
+ var QuotaError = class extends Error {
2081
+ status;
2082
+ constructor(status) {
2083
+ const msg = "message" in status && status.message ? status.message : "";
2084
+ super(msg ? `${status.state}: ${msg}` : status.state);
2085
+ this.name = "QuotaError";
2086
+ this.status = status;
2087
+ }
2088
+ };
2089
+ var QuotaAdapterRegistry = class {
2090
+ adapters = /* @__PURE__ */ new Map();
2091
+ register(adapter) {
2092
+ this.adapters.set(adapter.provider, adapter);
2093
+ }
2094
+ get(provider) {
2095
+ return this.adapters.get(provider);
2096
+ }
2097
+ list() {
2098
+ return Array.from(this.adapters.values());
2099
+ }
2100
+ };
2101
+ function baseSnapshot(provider, displayName, opts = {}) {
2102
+ return {
2103
+ provider,
2104
+ displayName,
2105
+ planName: opts.planName,
2106
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2107
+ freshness: "live",
2108
+ windows: opts.windows ?? []
2109
+ };
2110
+ }
2111
+
2112
+ // src/server/quota/cache.ts
2113
+ var QuotaCache = class {
2114
+ constructor(ttlMs = 6e4) {
2115
+ this.ttlMs = ttlMs;
2116
+ }
2117
+ store = /* @__PURE__ */ new Map();
2118
+ /** Fresh cached snapshot, or null if expired / absent. */
2119
+ getFresh(provider) {
2120
+ const entry = this.store.get(provider);
2121
+ if (!entry) return null;
2122
+ if (Date.now() > entry.expiresAt) return null;
2123
+ return { ...entry.snapshot, freshness: "cached" };
2124
+ }
2125
+ /** Last successful snapshot regardless of TTL (for stale-while-revalidate). */
2126
+ getStale(provider) {
2127
+ const entry = this.store.get(provider);
2128
+ if (!entry) return null;
2129
+ return { ...entry.snapshot, freshness: "stale" };
2130
+ }
2131
+ set(snapshot) {
2132
+ this.store.set(snapshot.provider, {
2133
+ snapshot,
2134
+ expiresAt: Date.now() + this.ttlMs,
2135
+ updatedAt: Date.now()
2136
+ });
2137
+ }
2138
+ clear(provider) {
2139
+ if (provider) this.store.delete(provider);
2140
+ else this.store.clear();
2141
+ }
2142
+ };
2143
+
2144
+ // src/server/quota/schemas.ts
2145
+ var import_zod3 = require("zod");
2146
+ var QuotaWindowSchema = import_zod3.z.object({
2147
+ id: import_zod3.z.string(),
2148
+ label: import_zod3.z.string(),
2149
+ usedPercent: import_zod3.z.number().min(0).max(100).default(0),
2150
+ remainingPercent: import_zod3.z.number().min(0).max(100).default(0),
2151
+ used: import_zod3.z.number().optional(),
2152
+ limit: import_zod3.z.number().optional(),
2153
+ durationMins: import_zod3.z.number().optional(),
2154
+ resetsAt: import_zod3.z.string().optional(),
2155
+ isUnlimited: import_zod3.z.boolean().optional(),
2156
+ modelName: import_zod3.z.string().optional()
2157
+ });
2158
+ var QuotaProviderStatusSchema = import_zod3.z.object({
2159
+ state: import_zod3.z.enum([
2160
+ "ok",
2161
+ "auth_failed",
2162
+ "not_configured",
2163
+ "upstream_unavailable",
2164
+ "rate_limited",
2165
+ "malformed_response",
2166
+ "timed_out",
2167
+ "error"
2168
+ ]),
2169
+ message: import_zod3.z.string().optional(),
2170
+ category: import_zod3.z.string().optional()
2171
+ });
2172
+ var QuotaSnapshotSchema = import_zod3.z.object({
2173
+ provider: import_zod3.z.enum(["codex", "claude", "glm", "minimax", "kimi"]),
2174
+ displayName: import_zod3.z.string(),
2175
+ planName: import_zod3.z.string().optional(),
2176
+ fetchedAt: import_zod3.z.string(),
2177
+ freshness: import_zod3.z.enum(["live", "cached", "stale"]),
2178
+ windows: import_zod3.z.array(QuotaWindowSchema).default([]),
2179
+ status: QuotaProviderStatusSchema
2180
+ });
2181
+ var QuotaResponseSchema = import_zod3.z.object({
2182
+ providers: import_zod3.z.array(QuotaSnapshotSchema).default([])
2183
+ });
2184
+ function validateQuotaSnapshot(data) {
2185
+ return QuotaSnapshotSchema.parse(data);
2186
+ }
2187
+
2188
+ // src/server/quota/quotaService.ts
2189
+ var QuotaService = class {
2190
+ constructor(registry2, cache2 = new QuotaCache(), configuredCache = null, fetchTimeoutMs = 8e3) {
2191
+ this.registry = registry2;
2192
+ this.cache = cache2;
2193
+ this.configuredCache = configuredCache;
2194
+ this.fetchTimeoutMs = fetchTimeoutMs;
2195
+ }
2196
+ /** Cap concurrent upstream calls so a slow provider can't block the others. */
2197
+ fetchTimeoutMs;
2198
+ /** In-flight promises keyed by provider id, to dedupe concurrent requests. */
2199
+ inflight = /* @__PURE__ */ new Map();
2200
+ /**
2201
+ * List provider ids that are configured locally. Cheap (no network).
2202
+ * The dashboard only shows these — not-configured providers are excluded.
2203
+ */
2204
+ async discover() {
2205
+ const all = this.registry.list();
2206
+ const checks = await Promise.all(
2207
+ all.map(async (a) => ({ id: a.provider, configured: await safeIsConfigured(a) }))
2208
+ );
2209
+ return checks.filter((c) => c.configured).map((c) => c.id);
2210
+ }
2211
+ /**
2212
+ * Fetch one provider's snapshot. Fresh if available; stale-but-retained
2213
+ * on failure; never throws (errors become structured statuses).
2214
+ */
2215
+ async fetchOne(provider) {
2216
+ const fresh = this.cache.getFresh(provider);
2217
+ if (fresh) return fresh;
2218
+ const adapter = this.registry.get(provider);
2219
+ if (!adapter) return null;
2220
+ let p = this.inflight.get(provider);
2221
+ if (!p) {
2222
+ p = this.fetchWithTimeout(adapter).finally(() => this.inflight.delete(provider));
2223
+ this.inflight.set(provider, p);
2224
+ }
2225
+ return p;
2226
+ }
2227
+ /**
2228
+ * Fetch all configured providers concurrently. Partial success: one
2229
+ * provider's failure never breaks the others. Order = registry order.
2230
+ */
2231
+ async fetchAll() {
2232
+ const ids = this.configuredCache ?? await this.discover();
2233
+ const byId = /* @__PURE__ */ new Map();
2234
+ const snapshots = await Promise.all(ids.map((id) => this.fetchOne(id)));
2235
+ for (const adapter of this.registry.list()) {
2236
+ const snap = snapshots.find((s) => s?.provider === adapter.provider);
2237
+ if (snap) byId.set(adapter.provider, snap);
2238
+ }
2239
+ return { providers: this.registry.list().map((a) => byId.get(a.provider)).filter((s) => !!s) };
2240
+ }
2241
+ /** Force a refresh of all configured providers, bypassing the cache. */
2242
+ async refreshAll() {
2243
+ this.configuredCache?.forEach(() => {
2244
+ });
2245
+ for (const adapter of this.registry.list()) {
2246
+ this.cache.clear(adapter.provider);
2247
+ }
2248
+ return this.fetchAll();
2249
+ }
2250
+ async fetchWithTimeout(adapter) {
2251
+ try {
2252
+ const snapshot = await withTimeout(adapter.fetch(), this.fetchTimeoutMs, adapter.provider);
2253
+ const validated = validateQuotaSnapshot(snapshot);
2254
+ this.cache.set(validated);
2255
+ return validated;
2256
+ } catch (err) {
2257
+ return this.handleFailure(adapter, err);
2258
+ }
2259
+ }
2260
+ handleFailure(adapter, err) {
2261
+ let status;
2262
+ if (err instanceof QuotaError) {
2263
+ status = err.status;
2264
+ } else if (err instanceof TimeoutError) {
2265
+ status = { state: "timed_out", message: `upstream did not respond within ${this.fetchTimeoutMs}ms` };
2266
+ } else {
2267
+ status = { state: "error", message: redact(err), category: "unexpected" };
2268
+ }
2269
+ const stale = this.cache.getStale(adapter.provider);
2270
+ if (stale) {
2271
+ return { ...stale, freshness: "stale", status };
2272
+ }
2273
+ return {
2274
+ provider: adapter.provider,
2275
+ displayName: adapter.displayName,
2276
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
2277
+ freshness: "stale",
2278
+ windows: [],
2279
+ status
2280
+ };
2281
+ }
2282
+ };
2283
+ var TimeoutError = class extends Error {
2284
+ constructor(provider) {
2285
+ super(`quota fetch timed out: ${provider}`);
2286
+ this.name = "TimeoutError";
2287
+ }
2288
+ };
2289
+ function withTimeout(p, ms, provider) {
2290
+ return new Promise((resolve2, reject) => {
2291
+ const timer = setTimeout(() => reject(new TimeoutError(provider)), ms);
2292
+ p.then(
2293
+ (v) => {
2294
+ clearTimeout(timer);
2295
+ resolve2(v);
2296
+ },
2297
+ (e) => {
2298
+ clearTimeout(timer);
2299
+ reject(e);
2300
+ }
2301
+ );
2302
+ });
2303
+ }
2304
+ async function safeIsConfigured(adapter) {
2305
+ try {
2306
+ return await adapter.isConfigured();
2307
+ } catch {
2308
+ return false;
2309
+ }
2310
+ }
2311
+ function redact(err) {
2312
+ const msg = err instanceof Error ? err.message : String(err);
2313
+ return msg.replace(/(sk-[A-Za-z0-9_-]{6,})[A-Za-z0-9_-]*/g, "$1\u2026").replace(/(Bearer\s+)[A-Za-z0-9._-]+/gi, "$1\u2026").slice(0, 200);
2314
+ }
2315
+
2316
+ // src/server/quota/adapters/codex.ts
2317
+ var import_node_fs8 = require("node:fs");
2318
+ var import_node_path8 = require("node:path");
2319
+ var import_node_os8 = require("node:os");
2320
+ var import_node_child_process2 = require("node:child_process");
2321
+
2322
+ // src/server/quota/helpers.ts
2323
+ function unixToIso(unix) {
2324
+ if (unix === null || unix === void 0 || unix === "") return void 0;
2325
+ const n = typeof unix === "string" ? parseInt(unix, 10) : unix;
2326
+ if (!Number.isFinite(n) || n <= 0) return void 0;
2327
+ const ms = n > 1e12 ? n : n * 1e3;
2328
+ return new Date(ms).toISOString();
2329
+ }
2330
+ function clampPercent(v) {
2331
+ if (!Number.isFinite(v)) return 0;
2332
+ return Math.max(0, Math.min(100, v));
2333
+ }
2334
+ function round1(v) {
2335
+ return Math.round(v * 10) / 10;
2336
+ }
2337
+ function windowFromPercent(id, label, usedPercent, opts = {}) {
2338
+ const used = round1(clampPercent(usedPercent));
2339
+ return {
2340
+ id,
2341
+ label,
2342
+ usedPercent: used,
2343
+ remainingPercent: round1(100 - used),
2344
+ durationMins: opts.durationMins,
2345
+ resetsAt: opts.resetsAt,
2346
+ used: opts.used,
2347
+ limit: opts.limit,
2348
+ modelName: opts.modelName,
2349
+ isUnlimited: opts.isUnlimited
2350
+ };
2351
+ }
2352
+ function windowFromCounts(id, label, used, limit, opts = {}) {
2353
+ if (limit <= 0) {
2354
+ return { id, label, usedPercent: 0, remainingPercent: 100, used, limit, isUnlimited: true, ...opts };
2355
+ }
2356
+ const pct = round1(clampPercent(used / limit * 100));
2357
+ return {
2358
+ id,
2359
+ label,
2360
+ usedPercent: pct,
2361
+ remainingPercent: round1(100 - pct),
2362
+ used,
2363
+ limit,
2364
+ ...opts
2365
+ };
2366
+ }
2367
+ async function fetchJsonWithTimeout(url, opts = {}) {
2368
+ const ctrl = new AbortController();
2369
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 8e3);
2370
+ try {
2371
+ const res = await fetch(url, { headers: opts.headers, signal: ctrl.signal });
2372
+ if (!res.ok) {
2373
+ const body = await res.text().catch(() => "");
2374
+ throw new HttpError(res.status, body.slice(0, 200));
2375
+ }
2376
+ return await res.json();
2377
+ } finally {
2378
+ clearTimeout(timer);
2379
+ }
2380
+ }
2381
+ var HttpError = class extends Error {
2382
+ constructor(status, body) {
2383
+ super(`HTTP ${status}`);
2384
+ this.status = status;
2385
+ this.body = body;
2386
+ this.name = "HttpError";
2387
+ }
2388
+ };
2389
+ function classifyHttpError(err) {
2390
+ if (err.status === 401 || err.status === 403) {
2391
+ return { state: "auth_failed", message: "credential rejected by provider" };
2392
+ }
2393
+ if (err.status === 429) {
2394
+ return { state: "rate_limited", message: "provider throttled the request" };
2395
+ }
2396
+ return { state: "upstream_unavailable", message: `provider returned HTTP ${err.status}` };
2397
+ }
2398
+
2399
+ // src/server/quota/adapters/codex.ts
2400
+ function codexHome() {
2401
+ return process.env.CODEX_HOME || (0, import_node_path8.join)((0, import_node_os8.homedir)(), ".codex");
2402
+ }
2403
+ var codexAdapter = {
2404
+ provider: "codex",
2405
+ displayName: "OpenAI Codex",
2406
+ async isConfigured() {
2407
+ if ((0, import_node_fs8.existsSync)((0, import_node_path8.join)(codexHome(), "auth.json"))) return true;
2408
+ return await codexBinaryAvailable();
2409
+ },
2410
+ async fetch() {
2411
+ const result = await queryRateLimits();
2412
+ const buckets = result.rateLimitsByLimitId ?? (result.rateLimits ? { primary: result.rateLimits } : {});
2413
+ const windows = [];
2414
+ for (const [key, bucket] of Object.entries(buckets)) {
2415
+ if (bucket.primary) {
2416
+ windows.push(windowFromPercent(`codex_${key}_primary`, labelForBucket(key, "primary", bucket), bucket.primary.usedPercent ?? 0, {
2417
+ durationMins: bucket.primary.windowDurationMins,
2418
+ resetsAt: unixToIso(bucket.primary.resetsAt)
2419
+ }));
2420
+ }
2421
+ if (bucket.secondary) {
2422
+ windows.push(windowFromPercent(`codex_${key}_secondary`, labelForBucket(key, "secondary", bucket), bucket.secondary.usedPercent ?? 0, {
2423
+ durationMins: bucket.secondary.windowDurationMins,
2424
+ resetsAt: unixToIso(bucket.secondary.resetsAt)
2425
+ }));
2426
+ }
2427
+ }
2428
+ const snap = baseSnapshot("codex", "OpenAI Codex", {
2429
+ planName: result.planType ? capitalize(result.planType) : void 0,
2430
+ windows
2431
+ });
2432
+ return { ...snap, status: { state: "ok" } };
2433
+ }
2434
+ };
2435
+ function labelForBucket(key, tier, bucket) {
2436
+ const dur = tier === "primary" ? bucket.primary?.windowDurationMins : bucket.secondary?.windowDurationMins;
2437
+ const durLabel = dur ? durationLabel(dur) : capitalize(tier);
2438
+ const who = bucket.limitName || (key && key !== "primary" ? key : "");
2439
+ return who ? `${capitalize(who)} \xB7 ${durLabel}` : durLabel;
2440
+ }
2441
+ function durationLabel(mins) {
2442
+ if (mins >= 10080) return "Weekly";
2443
+ if (mins >= 1440) return `${Math.round(mins / 1440)}-Day`;
2444
+ if (mins >= 60) return `${Math.round(mins / 60)}-Hour`;
2445
+ return `${mins}m`;
2446
+ }
2447
+ function capitalize(s) {
2448
+ return s.charAt(0).toUpperCase() + s.slice(1);
2449
+ }
2450
+ async function queryRateLimits() {
2451
+ if (!await codexBinaryAvailable()) {
2452
+ throw new QuotaError({ state: "not_configured", message: "codex CLI not found on PATH" });
2453
+ }
2454
+ const proc = (0, import_node_child_process2.spawn)("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
2455
+ const client = new JsonRpcClient(proc);
2456
+ try {
2457
+ await client.request("initialize", {
2458
+ protocolVersion: "2025-03-26",
2459
+ clientInfo: { name: "tokendash", version: "1.0.0" }
2460
+ });
2461
+ client.notify("initialized", {});
2462
+ const res = await client.request("account/rateLimits/read", {});
2463
+ return res;
2464
+ } catch (err) {
2465
+ throw toQuotaError(err);
2466
+ } finally {
2467
+ client.dispose();
2468
+ try {
2469
+ proc.kill("SIGKILL");
2470
+ } catch {
2471
+ }
2472
+ }
2473
+ }
2474
+ function toQuotaError(err) {
2475
+ const msg = err instanceof Error ? err.message : String(err);
2476
+ if (/not found|ENOENT|spawn/i.test(msg)) {
2477
+ return new QuotaError({ state: "not_configured", message: "codex app-server unavailable" });
2478
+ }
2479
+ if (/401|403|unauthor|auth/i.test(msg)) {
2480
+ return new QuotaError({ state: "auth_failed", message: "codex session not authenticated" });
2481
+ }
2482
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2483
+ }
2484
+ var JsonRpcClient = class {
2485
+ constructor(proc) {
2486
+ this.proc = proc;
2487
+ proc.stdout.setEncoding("utf8");
2488
+ proc.stdout.on("data", (chunk) => this.onData(chunk));
2489
+ proc.on("error", (err) => this.failAll(err));
2490
+ proc.on("close", () => this.failAll(new Error("codex app-server closed unexpectedly")));
2491
+ }
2492
+ id = 0;
2493
+ buffer = "";
2494
+ pending = /* @__PURE__ */ new Map();
2495
+ disposed = false;
2496
+ request(method, params) {
2497
+ if (this.disposed) return Promise.reject(new Error("client disposed"));
2498
+ const id = ++this.id;
2499
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
2500
+ return new Promise((resolve2, reject) => {
2501
+ this.pending.set(id, { resolve: resolve2, reject });
2502
+ this.proc.stdin.write(msg, (err) => {
2503
+ if (err) reject(err instanceof Error ? err : new Error(String(err)));
2504
+ });
2505
+ });
2506
+ }
2507
+ notify(method, params) {
2508
+ if (this.disposed) return;
2509
+ const msg = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
2510
+ this.proc.stdin.write(msg, () => {
2511
+ });
2512
+ }
2513
+ dispose() {
2514
+ this.disposed = true;
2515
+ this.failAll(new Error("disposed"));
2516
+ }
2517
+ onData(chunk) {
2518
+ this.buffer += chunk;
2519
+ let idx;
2520
+ while ((idx = this.buffer.indexOf("\n")) >= 0) {
2521
+ const line = this.buffer.slice(0, idx).trim();
2522
+ this.buffer = this.buffer.slice(idx + 1);
2523
+ if (!line) continue;
2524
+ let msg;
2525
+ try {
2526
+ msg = JSON.parse(line);
2527
+ } catch {
2528
+ continue;
2529
+ }
2530
+ if (msg.id === void 0) continue;
2531
+ const entry = this.pending.get(msg.id);
2532
+ if (!entry) continue;
2533
+ this.pending.delete(msg.id);
2534
+ if (msg.error) entry.reject(new Error(msg.error.message || "codex JSON-RPC error"));
2535
+ else entry.resolve(msg.result);
2536
+ }
2537
+ }
2538
+ failAll(err) {
2539
+ for (const [, entry] of this.pending) entry.reject(err);
2540
+ this.pending.clear();
2541
+ }
2542
+ };
2543
+ var cachedCodexPath;
2544
+ async function codexBinaryAvailable() {
2545
+ if (cachedCodexPath !== void 0) return cachedCodexPath !== null;
2546
+ return new Promise((resolve2) => {
2547
+ const proc = (0, import_node_child_process2.spawn)("which", ["codex"], { stdio: ["ignore", "pipe", "ignore"] });
2548
+ let out = "";
2549
+ proc.stdout.on("data", (c) => {
2550
+ out += c;
2551
+ });
2552
+ proc.on("close", () => {
2553
+ cachedCodexPath = out.trim() || null;
2554
+ resolve2(cachedCodexPath !== null);
2555
+ });
2556
+ proc.on("error", () => {
2557
+ cachedCodexPath = null;
2558
+ resolve2(false);
2559
+ });
2560
+ });
2561
+ }
2562
+
2563
+ // src/server/quota/adapters/claude.ts
2564
+ var import_node_fs9 = require("node:fs");
2565
+ var import_node_path9 = require("node:path");
2566
+ var import_node_os9 = require("node:os");
2567
+ var import_node_child_process3 = require("node:child_process");
2568
+ var claudeAdapter = {
2569
+ provider: "claude",
2570
+ displayName: "Claude Code",
2571
+ async isConfigured() {
2572
+ const token = readClaudeToken();
2573
+ return !!token;
2574
+ },
2575
+ async fetch() {
2576
+ const token = readClaudeToken();
2577
+ if (!token) {
2578
+ throw new QuotaError({ state: "not_configured", message: "no Claude Code OAuth credential found" });
2579
+ }
2580
+ let data;
2581
+ try {
2582
+ data = await fetchJsonWithTimeout("https://api.anthropic.com/api/oauth/usage", {
2583
+ headers: {
2584
+ Authorization: `Bearer ${token}`,
2585
+ "anthropic-beta": "oauth-2025-04-20",
2586
+ "Content-Type": "application/json"
2587
+ }
2588
+ });
2589
+ } catch (err) {
2590
+ throw classifyFetchError(err);
2591
+ }
2592
+ const windows = [];
2593
+ if (data.five_hour) {
2594
+ windows.push(windowFromPercent("five_hour", "5-Hour Window", data.five_hour.utilization ?? 0, {
2595
+ durationMins: 300,
2596
+ resetsAt: normalizeIso(data.five_hour.resets_at)
2597
+ }));
2598
+ }
2599
+ if (data.seven_day) {
2600
+ windows.push(windowFromPercent("seven_day", "Weekly", data.seven_day.utilization ?? 0, {
2601
+ durationMins: 10080,
2602
+ resetsAt: normalizeIso(data.seven_day.resets_at)
2603
+ }));
2604
+ }
2605
+ if (data.seven_day_opus?.utilization !== void 0 && data.seven_day_opus?.utilization !== null) {
2606
+ windows.push(windowFromPercent("seven_day_opus", "Weekly \xB7 Opus", data.seven_day_opus.utilization, {
2607
+ durationMins: 10080,
2608
+ resetsAt: normalizeIso(data.seven_day_opus.resets_at)
2609
+ }));
2610
+ }
2611
+ const snap = baseSnapshot("claude", "Claude Code", { windows });
2612
+ return { ...snap, status: { state: "ok" } };
2613
+ }
2614
+ };
2615
+ function classifyFetchError(err) {
2616
+ if (err instanceof HttpError) {
2617
+ const c = classifyHttpError(err);
2618
+ return new QuotaError(c);
2619
+ }
2620
+ const msg = err instanceof Error ? err.message : String(err);
2621
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2622
+ }
2623
+ function normalizeIso(s) {
2624
+ return s ? new Date(s).toISOString() : void 0;
2625
+ }
2626
+ function readClaudeToken() {
2627
+ if (process.platform === "darwin") {
2628
+ const token = readFromKeychain();
2629
+ if (token) return token;
2630
+ }
2631
+ const configDir = process.env.CLAUDE_CONFIG_DIR || (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".claude");
2632
+ const credPath = (0, import_node_path9.join)(configDir, ".credentials.json");
2633
+ if ((0, import_node_fs9.existsSync)(credPath)) {
2634
+ try {
2635
+ const parsed = JSON.parse((0, import_node_fs9.readFileSync)(credPath, "utf8"));
2636
+ return parsed?.claudeAiOauth?.accessToken ?? null;
2637
+ } catch {
2638
+ return null;
2639
+ }
2640
+ }
2641
+ return null;
2642
+ }
2643
+ function readFromKeychain() {
2644
+ const candidates = ["Claude Code-credentials"];
2645
+ try {
2646
+ const list = (0, import_node_child_process3.execFileSync)("security", ["dump-keychain"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
2647
+ for (const m of list.matchAll(/"srvname"<blob>="([^"]*Claude Code-credentials[^"]*)"/g)) {
2648
+ if (m[1] && !candidates.includes(m[1])) candidates.push(m[1]);
2649
+ }
2650
+ } catch {
2651
+ }
2652
+ for (const name of candidates) {
2653
+ try {
2654
+ const raw = (0, import_node_child_process3.execFileSync)("security", ["find-generic-password", "-s", name, "-w"], {
2655
+ stdio: ["ignore", "pipe", "ignore"],
2656
+ encoding: "utf8"
2657
+ }).trim();
2658
+ if (!raw) continue;
2659
+ try {
2660
+ const parsed = JSON.parse(raw);
2661
+ return parsed?.claudeAiOauth?.accessToken ?? null;
2662
+ } catch {
2663
+ return raw;
2664
+ }
2665
+ } catch {
2666
+ continue;
2667
+ }
2668
+ }
2669
+ return null;
2670
+ }
2671
+
2672
+ // src/server/quota/adapters/glm.ts
2673
+ var import_node_fs11 = require("node:fs");
2674
+ var import_node_path11 = require("node:path");
2675
+ var import_node_os11 = require("node:os");
2676
+
2677
+ // src/server/quota/credentialsFile.ts
2678
+ var import_node_fs10 = require("node:fs");
2679
+ var import_node_path10 = require("node:path");
2680
+ var import_node_os10 = require("node:os");
2681
+ function readStoredCredential(provider) {
2682
+ try {
2683
+ const path = (0, import_node_path10.join)((0, import_node_os10.homedir)(), ".tokendash", "credentials.json");
2684
+ if (!(0, import_node_fs10.existsSync)(path)) return null;
2685
+ const all = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
2686
+ const entry = all?.[provider];
2687
+ if (entry && typeof entry === "object" && typeof entry.apiKey === "string") {
2688
+ const apiKey = entry.apiKey;
2689
+ if (!apiKey) return null;
2690
+ const baseUrl = entry.baseUrl;
2691
+ return { apiKey, baseUrl: typeof baseUrl === "string" ? baseUrl : void 0 };
2692
+ }
2693
+ return null;
2694
+ } catch {
2695
+ return null;
2696
+ }
2697
+ }
2698
+
2699
+ // src/server/quota/adapters/glm.ts
2700
+ var glmAdapter = {
2701
+ provider: "glm",
2702
+ displayName: "GLM Coding Plan",
2703
+ async isConfigured() {
2704
+ return !!resolveCredential();
2705
+ },
2706
+ async fetch() {
2707
+ const cred = resolveCredential();
2708
+ if (!cred) {
2709
+ throw new QuotaError({ state: "not_configured", message: "set ZAI_API_KEY or ZHIPU_API_KEY" });
2710
+ }
2711
+ let data;
2712
+ try {
2713
+ data = await fetchJsonWithTimeout(`${cred.base}/api/monitor/usage/quota/limit`, {
2714
+ headers: {
2715
+ // GLM wants the raw key, NOT "Bearer <key>".
2716
+ Authorization: cred.key,
2717
+ "Accept-Language": "en-US,en",
2718
+ "Content-Type": "application/json"
2719
+ }
2720
+ });
2721
+ } catch (err) {
2722
+ throw classifyFetchError2(err);
2723
+ }
2724
+ if (!data?.success && data?.code !== 200) {
2725
+ throw new QuotaError({ state: "upstream_unavailable", message: data?.msg || "GLM quota request failed" });
2726
+ }
2727
+ const limits = data.data?.limits ?? [];
2728
+ const windows = [];
2729
+ const tokenLimits = limits.filter((l) => l.type === "TOKENS_LIMIT" && typeof l.percentage === "number").sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
2730
+ tokenLimits.forEach((l, i) => {
2731
+ const isShort = i === 0;
2732
+ windows.push(windowFromPercent(
2733
+ isShort ? "glm_5h" : "glm_weekly",
2734
+ isShort ? "5-Hour Window" : "Weekly",
2735
+ l.percentage ?? 0,
2736
+ { durationMins: isShort ? 300 : 10080, resetsAt: unixToIso(l.nextResetTime) }
2737
+ ));
2738
+ });
2739
+ const timeLimit = limits.find((l) => l.type === "TIME_LIMIT");
2740
+ if (timeLimit && typeof timeLimit.usage === "number") {
2741
+ windows.push(windowFromCounts(
2742
+ "glm_mcp_monthly",
2743
+ "MCP \xB7 Monthly",
2744
+ timeLimit.currentValue ?? 0,
2745
+ timeLimit.usage
2746
+ ));
2747
+ }
2748
+ const snap = baseSnapshot("glm", "GLM Coding Plan", {
2749
+ planName: data.data?.level ? capitalize2(data.data.level) : void 0,
2750
+ windows
2751
+ });
2752
+ return { ...snap, status: { state: "ok" } };
2753
+ }
2754
+ };
2755
+ function classifyFetchError2(err) {
2756
+ if (err instanceof HttpError) {
2757
+ const c = classifyHttpError(err);
2758
+ return new QuotaError(c);
2759
+ }
2760
+ const msg = err instanceof Error ? err.message : String(err);
2761
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2762
+ }
2763
+ function resolveCredential() {
2764
+ const stored = readStoredCredential("glm");
2765
+ if (stored) return { key: stored.apiKey, base: stored.baseUrl || "https://open.bigmodel.cn" };
2766
+ const zai = envOrConfig("ZAI_API_KEY");
2767
+ if (zai) return { key: zai, base: envOrConfig("ZAI_BASE_URL") || "https://api.z.ai" };
2768
+ const zhipu = envOrConfig("ZHIPU_API_KEY");
2769
+ if (zhipu) return { key: zhipu, base: envOrConfig("ZHIPU_BASE_URL") || "https://open.bigmodel.cn" };
2770
+ const anthropicBase = envOrConfig("ANTHROPIC_BASE_URL");
2771
+ if (anthropicBase && isGlmHost(anthropicBase)) {
2772
+ const origin = originOf(anthropicBase);
2773
+ const token = envOrConfig("ANTHROPIC_AUTH_TOKEN") || envOrConfig("ANTHROPIC_API_KEY");
2774
+ if (origin && token) return { key: token, base: origin };
2775
+ }
2776
+ return null;
2777
+ }
2778
+ function isGlmHost(url) {
2779
+ const h = url.toLowerCase();
2780
+ return h.includes("bigmodel.cn") || h.includes("z.ai");
2781
+ }
2782
+ function originOf(url) {
2783
+ try {
2784
+ const u = new URL(url);
2785
+ return `${u.protocol}//${u.host}`;
2786
+ } catch {
2787
+ return null;
2788
+ }
2789
+ }
2790
+ var claudeSettingsEnv;
2791
+ function envOrConfig(key) {
2792
+ if (process.env[key]) return process.env[key];
2793
+ if (claudeSettingsEnv === void 0) claudeSettingsEnv = loadClaudeSettingsEnv();
2794
+ return claudeSettingsEnv?.[key];
2795
+ }
2796
+ function loadClaudeSettingsEnv() {
2797
+ const configDir = process.env.CLAUDE_CONFIG_DIR || (0, import_node_path11.join)((0, import_node_os11.homedir)(), ".claude");
2798
+ try {
2799
+ const path = (0, import_node_path11.join)(configDir, "settings.json");
2800
+ if (!(0, import_node_fs11.existsSync)(path)) return null;
2801
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
2802
+ return parsed?.env && typeof parsed.env === "object" ? parsed.env : null;
2803
+ } catch {
2804
+ return null;
2805
+ }
2806
+ }
2807
+ function capitalize2(s) {
2808
+ return s.charAt(0).toUpperCase() + s.slice(1);
2809
+ }
2810
+
2811
+ // src/server/quota/adapters/minimax.ts
2812
+ var minimaxAdapter = {
2813
+ provider: "minimax",
2814
+ displayName: "MiniMax Coding Plan",
2815
+ async isConfigured() {
2816
+ return !!resolveCredential2();
2817
+ },
2818
+ async fetch() {
2819
+ const cred = resolveCredential2();
2820
+ if (!cred) {
2821
+ throw new QuotaError({ state: "not_configured", message: "set MINIMAX_API_KEY (Subscription Key)" });
2822
+ }
2823
+ let data;
2824
+ try {
2825
+ data = await fetchJsonWithTimeout(`${cred.base}/v1/token_plan/remains`, {
2826
+ headers: {
2827
+ Authorization: `Bearer ${cred.key}`,
2828
+ "Content-Type": "application/json"
2829
+ }
2830
+ });
2831
+ } catch (err) {
2832
+ throw classifyFetchError3(err);
2833
+ }
2834
+ if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
2835
+ throw new QuotaError({
2836
+ state: "upstream_unavailable",
2837
+ message: data.base_resp.status_msg || `MiniMax error ${data.base_resp.status_code}`
2838
+ });
2839
+ }
2840
+ const windows = [];
2841
+ for (const m of data.model_remains ?? []) {
2842
+ const model = m.model_name ?? "MiniMax";
2843
+ const intervalTotal = m.current_interval_total_count ?? 0;
2844
+ const intervalRemaining = m.current_interval_usage_count ?? 0;
2845
+ if (intervalTotal > 0) {
2846
+ windows.push(windowFromCounts(
2847
+ `minimax_5h_${model}`,
2848
+ `5-Hour \xB7 ${model}`,
2849
+ Math.max(0, intervalTotal - intervalRemaining),
2850
+ intervalTotal,
2851
+ { durationMins: 300, resetsAt: unixToIso(m.end_time) }
2852
+ ));
2853
+ }
2854
+ const weeklyTotal = m.current_weekly_total_count ?? 0;
2855
+ const weeklyRemaining = m.current_weekly_usage_count ?? 0;
2856
+ if (weeklyTotal > 0) {
2857
+ windows.push(windowFromCounts(
2858
+ `minimax_weekly_${model}`,
2859
+ `Weekly \xB7 ${model}`,
2860
+ Math.max(0, weeklyTotal - weeklyRemaining),
2861
+ weeklyTotal,
2862
+ { durationMins: 10080 }
2863
+ ));
2864
+ }
2865
+ }
2866
+ const snap = baseSnapshot("minimax", "MiniMax Coding Plan", { windows });
2867
+ return { ...snap, status: { state: "ok" } };
2868
+ }
2869
+ };
2870
+ function classifyFetchError3(err) {
2871
+ if (err instanceof HttpError) {
2872
+ const c = classifyHttpError(err);
2873
+ return new QuotaError(c);
2874
+ }
2875
+ const msg = err instanceof Error ? err.message : String(err);
2876
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2877
+ }
2878
+ function resolveCredential2() {
2879
+ const stored = readStoredCredential("minimax");
2880
+ if (stored) {
2881
+ const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
2882
+ const base2 = stored.baseUrl || (region2 === "cn" ? "https://www.minimaxi.com" : "https://www.minimax.io");
2883
+ return { key: stored.apiKey, base: base2 };
2884
+ }
2885
+ const key = process.env.MINIMAX_API_KEY || process.env.MINIMAX_SUBSCRIPTION_KEY;
2886
+ if (!key) return null;
2887
+ const region = (process.env.MINIMAX_REGION || "").toLowerCase();
2888
+ const base = region === "cn" ? process.env.MINIMAX_BASE_URL || "https://www.minimaxi.com" : process.env.MINIMAX_BASE_URL || "https://www.minimax.io";
2889
+ return { key, base };
2890
+ }
2891
+
2892
+ // src/server/quota/adapters/kimi.ts
2893
+ var import_node_fs12 = require("node:fs");
2894
+ var import_node_path12 = require("node:path");
2895
+ var import_node_os12 = require("node:os");
2896
+ var KIMI_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
2897
+ var KIMI_BASE = "https://api.kimi.com/coding/v1";
2898
+ var KIMI_AUTH = "https://auth.kimi.com/api/oauth/token";
2899
+ var kimiAdapter = {
2900
+ provider: "kimi",
2901
+ displayName: "Kimi Code",
2902
+ async isConfigured() {
2903
+ const cred = readCredentials();
2904
+ return !!cred && !!cred.access_token;
2905
+ },
2906
+ async fetch() {
2907
+ const credPath = credentialsPath();
2908
+ let cred = readCredentials();
2909
+ if (!cred || !cred.access_token) {
2910
+ throw new QuotaError({ state: "not_configured", message: "run `kimi` to log in first" });
2911
+ }
2912
+ const nowSec = Math.floor(Date.now() / 1e3);
2913
+ if (cred.expires_at && cred.expires_at - nowSec < 300 && cred.refresh_token) {
2914
+ cred = await refreshToken(credPath, cred.refresh_token).catch(() => cred);
2915
+ }
2916
+ let data;
2917
+ try {
2918
+ data = await fetchJsonWithTimeout(`${KIMI_BASE}/usages`, {
2919
+ headers: {
2920
+ Authorization: `Bearer ${cred.access_token}`,
2921
+ Accept: "application/json"
2922
+ }
2923
+ });
2924
+ } catch (err) {
2925
+ throw classifyFetchError4(err);
2926
+ }
2927
+ const windows = [];
2928
+ if (data.usage) {
2929
+ const { used, limit } = toCounts(data.usage);
2930
+ if (limit > 0) {
2931
+ windows.push(windowFromCounts("kimi_weekly", "Weekly", used, limit, {
2932
+ durationMins: 10080,
2933
+ resetsAt: normalizeIso2(data.usage.resetTime)
2934
+ }));
2935
+ }
2936
+ }
2937
+ for (let i = 0; i < (data.limits?.length ?? 0); i++) {
2938
+ const entry = data.limits[i];
2939
+ const detail = entry?.detail;
2940
+ if (!detail) continue;
2941
+ const { used, limit } = toCounts(detail);
2942
+ if (limit <= 0) continue;
2943
+ const mins = minutesForWindow(entry?.window);
2944
+ windows.push(windowFromCounts(`kimi_limit_${i}`, mins ? `${durationLabel2(mins)} Window` : `Window ${i + 1}`, used, limit, {
2945
+ durationMins: mins,
2946
+ resetsAt: normalizeIso2(detail.resetTime)
2947
+ }));
2948
+ }
2949
+ const snap = baseSnapshot("kimi", "Kimi Code", {
2950
+ planName: data.user?.membership?.level ? prettifyLevel(data.user.membership.level) : void 0,
2951
+ windows
2952
+ });
2953
+ return { ...snap, status: { state: "ok" } };
2954
+ }
2955
+ };
2956
+ function classifyFetchError4(err) {
2957
+ if (err instanceof HttpError) {
2958
+ const c = classifyHttpError(err);
2959
+ return new QuotaError(c);
2960
+ }
2961
+ const msg = err instanceof Error ? err.message : String(err);
2962
+ return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2963
+ }
2964
+ function toCounts(detail) {
2965
+ const limit = toNumber(detail.limit);
2966
+ const remaining = toNumber(detail.remaining);
2967
+ if (limit <= 0) return { used: 0, limit: 0 };
2968
+ return { used: Math.max(0, limit - remaining), limit };
2969
+ }
2970
+ function toNumber(v) {
2971
+ if (v === void 0 || v === null) return 0;
2972
+ const n = typeof v === "string" ? parseFloat(v) : v;
2973
+ return Number.isFinite(n) ? n : 0;
2974
+ }
2975
+ function minutesForWindow(window) {
2976
+ if (!window?.duration) return void 0;
2977
+ const unit = (window.timeUnit || "").toUpperCase();
2978
+ if (unit.includes("HOUR")) return window.duration * 60;
2979
+ if (unit.includes("DAY")) return window.duration * 1440;
2980
+ return window.duration;
2981
+ }
2982
+ function durationLabel2(mins) {
2983
+ if (mins >= 10080) return "Weekly";
2984
+ if (mins >= 1440) return `${Math.round(mins / 1440)}-Day`;
2985
+ if (mins >= 60) return `${Math.round(mins / 60)}-Hour`;
2986
+ return `${mins}m`;
2987
+ }
2988
+ function normalizeIso2(s) {
2989
+ return s ? new Date(s).toISOString() : void 0;
2990
+ }
2991
+ function prettifyLevel(level) {
2992
+ return level.replace(/^LEVEL_/, "").toLowerCase().replace(/(^|_)(\w)/g, (_, __, c) => " " + c.toUpperCase()).trim();
2993
+ }
2994
+ function kimiDataDir() {
2995
+ return process.env.KIMI_DATA_DIR || (0, import_node_path12.join)((0, import_node_os12.homedir)(), ".kimi");
2996
+ }
2997
+ function credentialsPath() {
2998
+ return (0, import_node_path12.join)(kimiDataDir(), "credentials", "kimi-code.json");
2999
+ }
3000
+ function readCredentials() {
3001
+ const stored = readStoredCredential("kimi");
3002
+ if (stored?.apiKey) {
3003
+ return { access_token: stored.apiKey, token_type: "Bearer" };
3004
+ }
3005
+ const path = credentialsPath();
3006
+ if (!(0, import_node_fs12.existsSync)(path)) return null;
3007
+ try {
3008
+ return JSON.parse((0, import_node_fs12.readFileSync)(path, "utf8"));
3009
+ } catch {
3010
+ return null;
3011
+ }
3012
+ }
3013
+ async function refreshToken(credPath, refreshToken2) {
3014
+ const body = new URLSearchParams({
3015
+ client_id: KIMI_CLIENT_ID,
3016
+ grant_type: "refresh_token",
3017
+ refresh_token: refreshToken2
3018
+ });
3019
+ const res = await fetch(KIMI_AUTH, {
3020
+ method: "POST",
3021
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3022
+ body
3023
+ });
3024
+ if (!res.ok) throw new Error(`token refresh failed: HTTP ${res.status}`);
3025
+ const tokens = await res.json();
3026
+ const updated = {
3027
+ access_token: tokens.access_token,
3028
+ refresh_token: tokens.refresh_token || refreshToken2,
3029
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 3600),
3030
+ token_type: tokens.token_type || "Bearer"
3031
+ };
3032
+ try {
3033
+ (0, import_node_fs12.writeFileSync)(credPath, JSON.stringify(updated, null, 2), "utf8");
3034
+ } catch {
3035
+ }
3036
+ return updated;
3037
+ }
3038
+
3039
+ // src/server/quota/index.ts
3040
+ var registry = new QuotaAdapterRegistry();
3041
+ registry.register(claudeAdapter);
3042
+ registry.register(codexAdapter);
3043
+ registry.register(glmAdapter);
3044
+ registry.register(minimaxAdapter);
3045
+ registry.register(kimiAdapter);
3046
+ var quotaCache = new QuotaCache();
3047
+ var quotaService = new QuotaService(registry, quotaCache);
3048
+
3049
+ // src/server/routes/api.ts
3050
+ async function getQuota(_req, res) {
3051
+ const force = _req.query.refresh === "1" || _req.query.refresh === "true";
3052
+ try {
3053
+ const data = force ? await quotaService.refreshAll() : await quotaService.fetchAll();
3054
+ res.json(data);
3055
+ } catch (error) {
3056
+ const message = error instanceof Error ? error.message : "Unknown error";
3057
+ res.status(500).json({ error: "Failed to fetch quota", hint: message });
3058
+ }
3059
+ }
3060
+ function getAgents(_req, res) {
3061
+ try {
3062
+ const agents = detectAvailableAgents();
3063
+ const available = [];
3064
+ if (agents.claude) available.push("claude");
3065
+ if (agents.codex) available.push("codex");
3066
+ if (isOpenClawAccessible()) available.push("openclaw");
3067
+ if (isOpencodeAccessible()) available.push("opencode");
3068
+ res.json({ available, default: available[0] || null });
3069
+ } catch (error) {
3070
+ const message = error instanceof Error ? error.message : "Unknown error";
3071
+ res.status(500).json({ error: "Failed to detect agents", hint: message });
3072
+ }
3073
+ }
3074
+ function getAppInfo(info) {
3075
+ return (req, res) => {
3076
+ const host = req.get("host");
3077
+ res.json({
3078
+ ...info,
3079
+ dashboardUrl: host ? `${req.protocol}://${host}` : info.dashboardUrl
3080
+ });
3081
+ };
3082
+ }
3083
+ function registerApiRoutes(router, appInfo) {
3084
+ router.get("/app-info", getAppInfo(appInfo));
3085
+ router.get("/agents", getAgents);
3086
+ router.get("/daily", getDaily);
3087
+ router.get("/monthly", getMonthly);
3088
+ router.get("/session", getSession);
3089
+ router.get("/projects", getProjects);
3090
+ router.get("/blocks", getBlocks);
3091
+ router.get("/analytics", getAnalytics);
3092
+ router.get("/quota", getQuota);
3093
+ }
3094
+
3095
+ // src/server/index.ts
3096
+ var CLI_USAGE = [
3097
+ "Usage:",
3098
+ " tokendash",
3099
+ " tokendash --version",
3100
+ " tokendash --port <number> [--no-open]",
3101
+ " tokendash --tray [--port <number>]"
3102
+ ].join("\n");
3103
+ var PACKAGE_NAME = "@zhangferry-dev/tokendash";
3104
+ function getPackageVersion() {
3105
+ const __filename = (0, import_node_url.fileURLToPath)(__esbuild_import_meta_url);
3106
+ const __dirname = (0, import_node_path13.dirname)(__filename);
3107
+ const packageJsonPaths = [
3108
+ (0, import_node_path13.join)(__dirname, "..", "..", "package.json"),
3109
+ // dist/server/index.js
3110
+ (0, import_node_path13.join)(__dirname, "..", "package.json")
3111
+ // bundled server entrypoint
3112
+ ];
3113
+ for (const packageJsonPath of packageJsonPaths) {
3114
+ if (!(0, import_node_fs13.existsSync)(packageJsonPath)) continue;
3115
+ const packageJson = JSON.parse((0, import_node_fs13.readFileSync)(packageJsonPath, "utf8"));
3116
+ if (packageJson.version) return packageJson.version;
3117
+ }
3118
+ return "unknown";
3119
+ }
3120
+ function resolvePort(value) {
3121
+ return Number.isInteger(value) && value && value > 0 ? value : 3456;
3122
+ }
3123
+ function resolveStaticAssetBaseDir(moduleUrl = __esbuild_import_meta_url, baseDir) {
3124
+ if (baseDir) return { baseDir: (0, import_node_path13.resolve)(baseDir), isProduction: true };
3125
+ const moduleDir = (0, import_node_path13.dirname)((0, import_node_url.fileURLToPath)(moduleUrl));
3126
+ const isProduction = moduleUrl.includes("/dist/");
3127
+ if (!isProduction) return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: false };
3128
+ if ((0, import_node_path13.basename)(moduleDir) === "server") {
3129
+ return { baseDir: (0, import_node_path13.resolve)((0, import_node_path13.dirname)(moduleDir)), isProduction: true };
3130
+ }
3131
+ return { baseDir: (0, import_node_path13.resolve)(moduleDir), isProduction: true };
3132
+ }
3133
+ function createApp(_port, baseDir) {
3134
+ const app = (0, import_express.default)();
3135
+ const router = import_express.default.Router();
3136
+ registerApiRoutes(router, {
3137
+ packageName: PACKAGE_NAME,
3138
+ version: getPackageVersion(),
3139
+ dashboardUrl: `http://localhost:${resolvePort(_port)}`
3140
+ });
3141
+ app.use("/api", router);
3142
+ const { baseDir: _baseDir, isProduction } = resolveStaticAssetBaseDir(__esbuild_import_meta_url, baseDir);
3143
+ const popoverPath = isProduction ? (0, import_node_path13.join)(_baseDir, "client", "popover.html") : (0, import_node_path13.join)(_baseDir, "..", "..", "public", "popover.html");
3144
+ app.get("/popover.html", (_req, res, next) => {
3145
+ if (!(0, import_node_fs13.existsSync)(popoverPath)) {
3146
+ next();
3147
+ return;
3148
+ }
3149
+ res.type("html").send((0, import_node_fs13.readFileSync)(popoverPath, "utf8"));
3150
+ });
3151
+ if (isProduction) {
3152
+ const clientPath = (0, import_node_path13.join)(_baseDir, "client");
3153
+ const clientIndexPath = (0, import_node_path13.join)(clientPath, "index.html");
3154
+ app.use(import_express.default.static(clientPath));
3155
+ app.use("{*path}", (_req, res) => {
3156
+ res.sendFile(clientIndexPath);
3157
+ });
3158
+ }
3159
+ return app;
3160
+ }
3161
+
3162
+ // src/server/daemon.ts
3163
+ var import_node_http = __toESM(require("node:http"), 1);
3164
+ var import_node_fs14 = require("node:fs");
3165
+ var import_node_path14 = require("node:path");
3166
+ var import_node_os13 = require("node:os");
3167
+ var DATA_DIR = (0, import_node_path14.join)((0, import_node_os13.homedir)(), ".tokendash");
3168
+ var PID_FILE = (0, import_node_path14.join)(DATA_DIR, "daemon.pid");
3169
+ var PORT_FILE = (0, import_node_path14.join)(DATA_DIR, "daemon.port");
3170
+ function ensureDataDir() {
3171
+ if (!(0, import_node_fs14.existsSync)(DATA_DIR)) (0, import_node_fs14.mkdirSync)(DATA_DIR, { recursive: true });
3172
+ }
3173
+ function writePidFile() {
3174
+ ensureDataDir();
3175
+ (0, import_node_fs14.writeFileSync)(PID_FILE, String(process.pid), "utf8");
3176
+ }
3177
+ function writePortFile(port) {
3178
+ ensureDataDir();
3179
+ (0, import_node_fs14.writeFileSync)(PORT_FILE, String(port), "utf8");
3180
+ }
3181
+ function cleanupFiles() {
3182
+ try {
3183
+ if ((0, import_node_fs14.existsSync)(PID_FILE)) (0, import_node_fs14.unlinkSync)(PID_FILE);
3184
+ } catch (_) {
3185
+ }
3186
+ try {
3187
+ if ((0, import_node_fs14.existsSync)(PORT_FILE)) (0, import_node_fs14.unlinkSync)(PORT_FILE);
3188
+ } catch (_) {
3189
+ }
3190
+ }
3191
+ function resolvePort2() {
3192
+ const args = process.argv.slice(2);
3193
+ for (let i = 0; i < args.length; i++) {
3194
+ if (args[i] === "--port" && i + 1 < args.length) {
3195
+ const v = parseInt(args[i + 1], 10);
3196
+ if (Number.isInteger(v) && v > 0) return v;
3197
+ }
3198
+ }
3199
+ const envPort = process.env.TOKENDASH_PORT ? parseInt(process.env.TOKENDASH_PORT, 10) : 0;
3200
+ if (Number.isInteger(envPort) && envPort > 0) return envPort;
3201
+ return 3456;
3202
+ }
3203
+ function listen(app, port) {
3204
+ return new Promise((resolve2, reject) => {
3205
+ const server = app.listen(port);
3206
+ const onListen = () => {
3207
+ cleanup();
3208
+ resolve2(server);
3209
+ };
3210
+ const onError = (err) => {
3211
+ cleanup();
3212
+ reject(err);
3213
+ };
3214
+ const cleanup = () => {
3215
+ server.off("listening", onListen);
3216
+ server.off("error", onError);
3217
+ };
3218
+ server.once("listening", onListen);
3219
+ server.once("error", onError);
3220
+ });
3221
+ }
3222
+ async function listenWithFallback(app, preferredPort) {
3223
+ let port = preferredPort;
3224
+ for (let attempt = 0; attempt < 20; attempt++, port++) {
3225
+ try {
3226
+ const server = await listen(app, port);
3227
+ return { server, port };
3228
+ } catch (error) {
3229
+ const err = error;
3230
+ if (err.code !== "EADDRINUSE") throw error;
3231
+ }
3232
+ }
3233
+ throw new Error(`No available port starting from ${preferredPort}`);
3234
+ }
3235
+ function killStaleDaemon() {
3236
+ if (!(0, import_node_fs14.existsSync)(PID_FILE)) return false;
3237
+ try {
3238
+ const pid = parseInt((0, import_node_fs14.readFileSync)(PID_FILE, "utf8").trim(), 10);
3239
+ if (!Number.isInteger(pid) || pid <= 0) return false;
3240
+ process.kill(pid, 0);
3241
+ process.kill(pid, "SIGTERM");
3242
+ const deadline = Date.now() + 2e3;
3243
+ while (Date.now() < deadline) {
3244
+ try {
3245
+ process.kill(pid, 0);
3246
+ } catch {
3247
+ break;
3248
+ }
3249
+ }
3250
+ return true;
3251
+ } catch {
3252
+ cleanupFiles();
3253
+ return false;
3254
+ }
3255
+ }
3256
+ async function main() {
3257
+ killStaleDaemon();
3258
+ const preferredPort = resolvePort2();
3259
+ const distDir = (0, import_node_path14.join)(__esbuild_import_meta_url.replace("file://", ""), "..");
3260
+ const app = createApp(preferredPort, distDir);
3261
+ const { server, port } = await listenWithFallback(app, preferredPort);
3262
+ writePidFile();
3263
+ writePortFile(port);
3264
+ try {
3265
+ const agentsRes = await new Promise((resolve2, reject) => {
3266
+ import_node_http.default.get(`http://127.0.0.1:${port}/api/agents`, (res) => {
3267
+ let body = "";
3268
+ res.on("data", (chunk) => {
3269
+ body += chunk;
3270
+ });
3271
+ res.on("end", () => {
3272
+ try {
3273
+ resolve2(JSON.parse(body));
3274
+ } catch (e) {
3275
+ reject(e);
3276
+ }
3277
+ });
3278
+ }).on("error", reject);
3279
+ });
3280
+ const agents = agentsRes?.available ?? ["claude"];
3281
+ await Promise.all(agents.map((agent) => new Promise((resolve2) => {
3282
+ import_node_http.default.get(`http://127.0.0.1:${port}/api/daily?agent=${agent}`, (res) => {
3283
+ res.resume();
3284
+ res.on("end", () => resolve2());
3285
+ }).on("error", () => resolve2());
3286
+ })));
3287
+ } catch (_) {
3288
+ }
3289
+ const shutdown = () => {
3290
+ cleanupFiles();
3291
+ server.close(() => process.exit(0));
3292
+ setTimeout(() => process.exit(0), 5e3);
3293
+ };
3294
+ process.on("SIGTERM", shutdown);
3295
+ process.on("SIGINT", shutdown);
3296
+ process.on("uncaughtException", (err) => {
3297
+ console.error("[tokendash-daemon] uncaught:", err);
3298
+ shutdown();
3299
+ });
3300
+ }
3301
+ main().catch((err) => {
3302
+ console.error("[tokendash-daemon] fatal:", err);
3303
+ cleanupFiles();
3304
+ process.exit(1);
3305
+ });
3306
+ //# sourceMappingURL=daemon.cjs.map