@zhangferry-dev/tokendash 1.3.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -20
- package/bin/tokendash.js +5 -1
- package/dist/client/assets/{index-D-RErhSy.js → index-B4YgU_cb.js} +42 -42
- package/dist/client/assets/index-iYDpTV63.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/client/popover.html +1132 -0
- package/dist/electron-server.cjs +2175 -0
- package/dist/electron-server.cjs.map +7 -0
- package/dist/server/codexParser.d.ts +4 -5
- package/dist/server/codexParser.js +18 -6
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.js +59 -22
- package/electron/main.cjs +505 -0
- package/electron/main.js +291 -0
- package/electron/preload.cjs +25 -0
- package/electron/trayBadge.cjs +27 -0
- package/electron/trayBadge.js +30 -0
- package/electron/trayHelper +0 -0
- package/electron/trayHelper.swift +152 -0
- package/electron-builder.yml +20 -0
- package/package.json +13 -4
- package/resources/entitlements.mac.plist +10 -0
- package/resources/icon.icns +0 -0
- package/resources/icon.png +0 -0
- package/resources/product_menu.png +0 -0
- package/resources/product_screenshoot.png +0 -0
- package/dist/client/assets/index-x7K7fQX4.css +0 -1
|
@@ -0,0 +1,2175 @@
|
|
|
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 __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/server/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
createApp: () => createApp,
|
|
35
|
+
main: () => main
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
var import_express = __toESM(require("express"), 1);
|
|
39
|
+
var import_node_fs8 = require("node:fs");
|
|
40
|
+
var import_node_url = require("node:url");
|
|
41
|
+
var import_node_path8 = require("node:path");
|
|
42
|
+
|
|
43
|
+
// src/server/cache.ts
|
|
44
|
+
var import_node_fs = require("node:fs");
|
|
45
|
+
var import_node_path = require("node:path");
|
|
46
|
+
var import_node_os = require("node:os");
|
|
47
|
+
var DEFAULT_TTL = 5 * 60 * 1e3;
|
|
48
|
+
var DISK_TTL = 60 * 60 * 1e3;
|
|
49
|
+
var CACHE_DIR = (0, import_node_path.join)((0, import_node_os.tmpdir)(), "tokendash-cache");
|
|
50
|
+
function diskPath(key) {
|
|
51
|
+
const safe = key.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
52
|
+
return (0, import_node_path.join)(CACHE_DIR, `${safe}.json`);
|
|
53
|
+
}
|
|
54
|
+
var Cache = class {
|
|
55
|
+
store = /* @__PURE__ */ new Map();
|
|
56
|
+
get(key) {
|
|
57
|
+
const entry = this.store.get(key);
|
|
58
|
+
if (entry && Date.now() <= entry.expiresAt) {
|
|
59
|
+
return entry.data;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Get data even if stale (for stale-while-revalidate) */
|
|
64
|
+
getStale(key) {
|
|
65
|
+
const entry = this.store.get(key);
|
|
66
|
+
if (entry) return entry.data;
|
|
67
|
+
return this.readFromDisk(key);
|
|
68
|
+
}
|
|
69
|
+
set(key, data, ttl = DEFAULT_TTL) {
|
|
70
|
+
const entry = {
|
|
71
|
+
data,
|
|
72
|
+
expiresAt: Date.now() + ttl,
|
|
73
|
+
updatedAt: Date.now()
|
|
74
|
+
};
|
|
75
|
+
this.store.set(key, entry);
|
|
76
|
+
this.writeToDisk(key, entry);
|
|
77
|
+
}
|
|
78
|
+
clear() {
|
|
79
|
+
this.store.clear();
|
|
80
|
+
}
|
|
81
|
+
delete(key) {
|
|
82
|
+
return this.store.delete(key);
|
|
83
|
+
}
|
|
84
|
+
has(key) {
|
|
85
|
+
const entry = this.store.get(key);
|
|
86
|
+
if (!entry) return false;
|
|
87
|
+
if (Date.now() > entry.expiresAt) {
|
|
88
|
+
this.store.delete(key);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
writeToDisk(key, entry) {
|
|
94
|
+
try {
|
|
95
|
+
if (!(0, import_node_fs.existsSync)(CACHE_DIR)) (0, import_node_fs.mkdirSync)(CACHE_DIR, { recursive: true });
|
|
96
|
+
(0, import_node_fs.writeFileSync)(diskPath(key), JSON.stringify(entry), "utf-8");
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
readFromDisk(key) {
|
|
101
|
+
try {
|
|
102
|
+
const path = diskPath(key);
|
|
103
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
104
|
+
const raw = (0, import_node_fs.readFileSync)(path, "utf-8");
|
|
105
|
+
const entry = JSON.parse(raw);
|
|
106
|
+
if (Date.now() - entry.updatedAt < DISK_TTL) {
|
|
107
|
+
this.store.set(key, { ...entry, expiresAt: 0 });
|
|
108
|
+
return entry.data;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var cache = new Cache();
|
|
116
|
+
|
|
117
|
+
// src/shared/schemas.ts
|
|
118
|
+
var import_zod = require("zod");
|
|
119
|
+
var ModelBreakdownSchema = import_zod.z.object({
|
|
120
|
+
modelName: import_zod.z.string(),
|
|
121
|
+
inputTokens: import_zod.z.number().default(0),
|
|
122
|
+
outputTokens: import_zod.z.number().default(0),
|
|
123
|
+
cacheCreationTokens: import_zod.z.number().default(0),
|
|
124
|
+
cacheReadTokens: import_zod.z.number().default(0),
|
|
125
|
+
cost: import_zod.z.number().default(0)
|
|
126
|
+
});
|
|
127
|
+
var DailyEntrySchema = import_zod.z.object({
|
|
128
|
+
date: import_zod.z.string(),
|
|
129
|
+
inputTokens: import_zod.z.number().default(0),
|
|
130
|
+
outputTokens: import_zod.z.number().default(0),
|
|
131
|
+
cacheCreationTokens: import_zod.z.number().default(0),
|
|
132
|
+
cacheReadTokens: import_zod.z.number().default(0),
|
|
133
|
+
totalTokens: import_zod.z.number().default(0),
|
|
134
|
+
totalCost: import_zod.z.number().default(0),
|
|
135
|
+
modelsUsed: import_zod.z.array(import_zod.z.string()).default([]),
|
|
136
|
+
modelBreakdowns: import_zod.z.array(ModelBreakdownSchema).default([])
|
|
137
|
+
});
|
|
138
|
+
var TotalsSchema = import_zod.z.object({
|
|
139
|
+
inputTokens: import_zod.z.number().default(0),
|
|
140
|
+
outputTokens: import_zod.z.number().default(0),
|
|
141
|
+
cacheCreationTokens: import_zod.z.number().default(0),
|
|
142
|
+
cacheReadTokens: import_zod.z.number().default(0),
|
|
143
|
+
totalTokens: import_zod.z.number().default(0),
|
|
144
|
+
totalCost: import_zod.z.number().default(0)
|
|
145
|
+
});
|
|
146
|
+
var DailyResponseSchema = import_zod.z.object({
|
|
147
|
+
daily: import_zod.z.array(DailyEntrySchema).default([]),
|
|
148
|
+
totals: TotalsSchema
|
|
149
|
+
});
|
|
150
|
+
var ProjectEntrySchema = import_zod.z.object({
|
|
151
|
+
projectPath: import_zod.z.string(),
|
|
152
|
+
instances: import_zod.z.array(DailyEntrySchema).default([])
|
|
153
|
+
});
|
|
154
|
+
var ProjectsResponseSchema = import_zod.z.object({
|
|
155
|
+
projects: import_zod.z.record(import_zod.z.array(DailyEntrySchema).default([])).default({})
|
|
156
|
+
});
|
|
157
|
+
function validateDaily(data) {
|
|
158
|
+
return DailyResponseSchema.parse(data);
|
|
159
|
+
}
|
|
160
|
+
function validateProjects(data) {
|
|
161
|
+
return ProjectsResponseSchema.parse(data);
|
|
162
|
+
}
|
|
163
|
+
var BlockEntrySchema = import_zod.z.object({
|
|
164
|
+
id: import_zod.z.string(),
|
|
165
|
+
startTime: import_zod.z.string(),
|
|
166
|
+
endTime: import_zod.z.string(),
|
|
167
|
+
actualEndTime: import_zod.z.string().nullable().default(null),
|
|
168
|
+
isActive: import_zod.z.boolean().default(false),
|
|
169
|
+
isGap: import_zod.z.boolean().default(false),
|
|
170
|
+
entries: import_zod.z.number().default(0),
|
|
171
|
+
tokenCounts: import_zod.z.object({
|
|
172
|
+
inputTokens: import_zod.z.number().default(0),
|
|
173
|
+
outputTokens: import_zod.z.number().default(0),
|
|
174
|
+
cacheCreationInputTokens: import_zod.z.number().default(0),
|
|
175
|
+
cacheReadInputTokens: import_zod.z.number().default(0)
|
|
176
|
+
}).default({ inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 }),
|
|
177
|
+
totalTokens: import_zod.z.number().default(0),
|
|
178
|
+
costUSD: import_zod.z.number().default(0),
|
|
179
|
+
models: import_zod.z.array(import_zod.z.string()).default([])
|
|
180
|
+
});
|
|
181
|
+
var BlocksResponseSchema = import_zod.z.object({
|
|
182
|
+
blocks: import_zod.z.array(BlockEntrySchema).default([])
|
|
183
|
+
});
|
|
184
|
+
function validateBlocks(data) {
|
|
185
|
+
return BlocksResponseSchema.parse(data);
|
|
186
|
+
}
|
|
187
|
+
var DailyCodeChangeSchema = import_zod.z.object({
|
|
188
|
+
date: import_zod.z.string(),
|
|
189
|
+
linesAdded: import_zod.z.number().default(0),
|
|
190
|
+
linesDeleted: import_zod.z.number().default(0),
|
|
191
|
+
netChange: import_zod.z.number().default(0),
|
|
192
|
+
filesModified: import_zod.z.number().default(0)
|
|
193
|
+
});
|
|
194
|
+
var ToolUsageEntrySchema = import_zod.z.object({
|
|
195
|
+
name: import_zod.z.string(),
|
|
196
|
+
count: import_zod.z.number().default(0)
|
|
197
|
+
});
|
|
198
|
+
var ProductivityKPIsSchema = import_zod.z.object({
|
|
199
|
+
avgLinesPerEdit: import_zod.z.number().default(0),
|
|
200
|
+
filesModifiedPerDay: import_zod.z.number().default(0),
|
|
201
|
+
addDeleteRatio: import_zod.z.number().default(0),
|
|
202
|
+
totalEdits: import_zod.z.number().default(0),
|
|
203
|
+
totalFilesModified: import_zod.z.number().default(0),
|
|
204
|
+
activeDaysWithEdits: import_zod.z.number().default(0)
|
|
205
|
+
});
|
|
206
|
+
var AnalyticsResponseSchema = import_zod.z.object({
|
|
207
|
+
codeChangeTrend: import_zod.z.array(DailyCodeChangeSchema).default([]),
|
|
208
|
+
toolUsageDistribution: import_zod.z.array(ToolUsageEntrySchema).default([]),
|
|
209
|
+
productivityKPIs: ProductivityKPIsSchema,
|
|
210
|
+
toolCallTrend: import_zod.z.array(import_zod.z.record(import_zod.z.union([import_zod.z.string(), import_zod.z.number()]))).default([])
|
|
211
|
+
});
|
|
212
|
+
function validateAnalytics(data) {
|
|
213
|
+
return AnalyticsResponseSchema.parse(data);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/server/codexParser.ts
|
|
217
|
+
var import_node_fs2 = require("node:fs");
|
|
218
|
+
var import_node_path2 = require("node:path");
|
|
219
|
+
var import_node_os2 = require("node:os");
|
|
220
|
+
var import_zod2 = require("zod");
|
|
221
|
+
|
|
222
|
+
// src/server/codexPricing.ts
|
|
223
|
+
var MODEL_PRICING = {
|
|
224
|
+
"gpt-5.4": {
|
|
225
|
+
inputPer1M: 2.5,
|
|
226
|
+
cachedInputPer1M: 0.25,
|
|
227
|
+
outputPer1M: 15
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var DEFAULT_PRICING = {
|
|
231
|
+
inputPer1M: 2.5,
|
|
232
|
+
cachedInputPer1M: 0.25,
|
|
233
|
+
outputPer1M: 15
|
|
234
|
+
};
|
|
235
|
+
function calculateCost(tokens, models) {
|
|
236
|
+
const model = [...models][0] ?? "";
|
|
237
|
+
const pricing = MODEL_PRICING[model] ?? DEFAULT_PRICING;
|
|
238
|
+
const nonCachedInput = Math.max(tokens.inputTokens - tokens.cachedInputTokens, 0);
|
|
239
|
+
const cachedInput = Math.min(tokens.cachedInputTokens, tokens.inputTokens);
|
|
240
|
+
const outputTokens = tokens.outputTokens;
|
|
241
|
+
const inputCost = nonCachedInput / 1e6 * pricing.inputPer1M;
|
|
242
|
+
const cachedCost = cachedInput / 1e6 * pricing.cachedInputPer1M;
|
|
243
|
+
const outputCost = outputTokens / 1e6 * pricing.outputPer1M;
|
|
244
|
+
return inputCost + cachedCost + outputCost;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/server/codexParser.ts
|
|
248
|
+
var TokenUsageSchema = import_zod2.z.object({
|
|
249
|
+
input_tokens: import_zod2.z.number().default(0),
|
|
250
|
+
cached_input_tokens: import_zod2.z.number().default(0),
|
|
251
|
+
output_tokens: import_zod2.z.number().default(0),
|
|
252
|
+
reasoning_output_tokens: import_zod2.z.number().default(0),
|
|
253
|
+
total_tokens: import_zod2.z.number().default(0)
|
|
254
|
+
}).default({ input_tokens: 0, cached_input_tokens: 0, output_tokens: 0, reasoning_output_tokens: 0, total_tokens: 0 });
|
|
255
|
+
var TokenCountInfoSchema = import_zod2.z.object({
|
|
256
|
+
total_token_usage: TokenUsageSchema,
|
|
257
|
+
last_token_usage: TokenUsageSchema
|
|
258
|
+
}).nullable().default(null);
|
|
259
|
+
var TokenCountPayloadSchema = import_zod2.z.object({
|
|
260
|
+
type: import_zod2.z.literal("token_count"),
|
|
261
|
+
info: TokenCountInfoSchema
|
|
262
|
+
});
|
|
263
|
+
function tokenUsageKey(usage) {
|
|
264
|
+
return [
|
|
265
|
+
usage.input_tokens,
|
|
266
|
+
usage.cached_input_tokens,
|
|
267
|
+
usage.output_tokens,
|
|
268
|
+
usage.reasoning_output_tokens,
|
|
269
|
+
usage.total_tokens
|
|
270
|
+
].join(":");
|
|
271
|
+
}
|
|
272
|
+
function getSessionsDir() {
|
|
273
|
+
return (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".codex", "sessions");
|
|
274
|
+
}
|
|
275
|
+
function scanCodexSessions() {
|
|
276
|
+
const sessionsDir = getSessionsDir();
|
|
277
|
+
const results = [];
|
|
278
|
+
function walk(dir) {
|
|
279
|
+
let entries;
|
|
280
|
+
try {
|
|
281
|
+
entries = (0, import_node_fs2.readdirSync)(dir);
|
|
282
|
+
} catch {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
const full = (0, import_node_path2.join)(dir, entry);
|
|
287
|
+
let st;
|
|
288
|
+
try {
|
|
289
|
+
st = (0, import_node_fs2.statSync)(full);
|
|
290
|
+
} catch {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (st.isDirectory()) {
|
|
294
|
+
walk(full);
|
|
295
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
296
|
+
results.push(full);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
walk(sessionsDir);
|
|
301
|
+
return results.sort();
|
|
302
|
+
}
|
|
303
|
+
function parseCodexSession(filepath) {
|
|
304
|
+
let content;
|
|
305
|
+
try {
|
|
306
|
+
content = (0, import_node_fs2.readFileSync)(filepath, "utf-8");
|
|
307
|
+
} catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const lines = content.split("\n");
|
|
311
|
+
let sessionId = "";
|
|
312
|
+
let cwd = "";
|
|
313
|
+
let model = "";
|
|
314
|
+
let createdAt = "";
|
|
315
|
+
const tokenEvents = [];
|
|
316
|
+
const seenTotalUsageSnapshots = /* @__PURE__ */ new Set();
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
if (!trimmed) continue;
|
|
320
|
+
let obj;
|
|
321
|
+
try {
|
|
322
|
+
obj = JSON.parse(trimmed);
|
|
323
|
+
} catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const type = obj.type;
|
|
327
|
+
if (type === "session_meta") {
|
|
328
|
+
const payload = obj.payload || {};
|
|
329
|
+
sessionId = payload.id || "";
|
|
330
|
+
cwd = payload.cwd || "";
|
|
331
|
+
createdAt = payload.timestamp || "";
|
|
332
|
+
}
|
|
333
|
+
if (type === "turn_context") {
|
|
334
|
+
const payload = obj.payload || {};
|
|
335
|
+
if (!model && payload.model) {
|
|
336
|
+
model = payload.model;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (type === "event_msg") {
|
|
340
|
+
const payload = obj.payload || {};
|
|
341
|
+
if (payload.type === "token_count") {
|
|
342
|
+
const timestamp = obj.timestamp || "";
|
|
343
|
+
const parseResult = TokenCountPayloadSchema.safeParse(payload);
|
|
344
|
+
if (!parseResult.success) {
|
|
345
|
+
console.warn(`[codexParser] Schema validation failed in ${filepath}:`, parseResult.error.message);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const info = parseResult.data.info;
|
|
349
|
+
if (!info) continue;
|
|
350
|
+
const totalUsageKey = tokenUsageKey(info.total_token_usage);
|
|
351
|
+
if (seenTotalUsageSnapshots.has(totalUsageKey)) continue;
|
|
352
|
+
seenTotalUsageSnapshots.add(totalUsageKey);
|
|
353
|
+
const last = info.last_token_usage;
|
|
354
|
+
tokenEvents.push({
|
|
355
|
+
timestamp,
|
|
356
|
+
inputTokens: last.input_tokens,
|
|
357
|
+
cachedInputTokens: last.cached_input_tokens,
|
|
358
|
+
outputTokens: last.output_tokens,
|
|
359
|
+
reasoningOutputTokens: last.reasoning_output_tokens,
|
|
360
|
+
totalTokens: last.total_tokens
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!sessionId) return null;
|
|
366
|
+
return { id: sessionId, cwd, model, createdAt, tokenEvents };
|
|
367
|
+
}
|
|
368
|
+
function parseAllSessions() {
|
|
369
|
+
return scanCodexSessions().map(parseCodexSession).filter((s) => s !== null);
|
|
370
|
+
}
|
|
371
|
+
var TZ_OFFSETS = {
|
|
372
|
+
"Asia/Shanghai": 8,
|
|
373
|
+
"Asia/Tokyo": 9,
|
|
374
|
+
"America/New_York": -5,
|
|
375
|
+
"America/Los_Angeles": -8,
|
|
376
|
+
"Europe/London": 0,
|
|
377
|
+
"UTC": 0
|
|
378
|
+
};
|
|
379
|
+
function getTzOffsetHours(tz) {
|
|
380
|
+
return TZ_OFFSETS[tz] ?? 8;
|
|
381
|
+
}
|
|
382
|
+
function toLocalISO(ts, tz) {
|
|
383
|
+
const d = new Date(ts);
|
|
384
|
+
return new Date(d.getTime() + getTzOffsetHours(tz) * 36e5);
|
|
385
|
+
}
|
|
386
|
+
function getDateKey(ts, tz) {
|
|
387
|
+
return toLocalISO(ts, tz).toISOString().slice(0, 10);
|
|
388
|
+
}
|
|
389
|
+
function getHourKey(ts, tz) {
|
|
390
|
+
const local = toLocalISO(ts, tz);
|
|
391
|
+
return local.toISOString().slice(0, 13).replace("T", " ") + ":00";
|
|
392
|
+
}
|
|
393
|
+
function getMonthKey(ts, tz) {
|
|
394
|
+
return getDateKey(ts, tz).slice(0, 7);
|
|
395
|
+
}
|
|
396
|
+
function extractProjectName(cwd) {
|
|
397
|
+
if (!cwd) return "unknown";
|
|
398
|
+
const parts = cwd.replace(/\/+$/, "").split("/");
|
|
399
|
+
return parts[parts.length - 1] || "unknown";
|
|
400
|
+
}
|
|
401
|
+
function emptyAcc() {
|
|
402
|
+
return { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0, totalTokens: 0 };
|
|
403
|
+
}
|
|
404
|
+
function addAcc(a, ev) {
|
|
405
|
+
a.inputTokens += ev.inputTokens;
|
|
406
|
+
a.cachedInputTokens += ev.cachedInputTokens;
|
|
407
|
+
a.outputTokens += ev.outputTokens;
|
|
408
|
+
a.reasoningOutputTokens += ev.reasoningOutputTokens;
|
|
409
|
+
a.totalTokens += ev.totalTokens;
|
|
410
|
+
}
|
|
411
|
+
function mergeAcc(a, b) {
|
|
412
|
+
a.inputTokens += b.inputTokens;
|
|
413
|
+
a.cachedInputTokens += b.cachedInputTokens;
|
|
414
|
+
a.outputTokens += b.outputTokens;
|
|
415
|
+
a.reasoningOutputTokens += b.reasoningOutputTokens;
|
|
416
|
+
a.totalTokens += b.totalTokens;
|
|
417
|
+
}
|
|
418
|
+
function accToEntry(date, acc, models) {
|
|
419
|
+
const cost = calculateCost(acc, models);
|
|
420
|
+
return {
|
|
421
|
+
date,
|
|
422
|
+
inputTokens: acc.inputTokens,
|
|
423
|
+
outputTokens: acc.outputTokens,
|
|
424
|
+
cacheCreationTokens: 0,
|
|
425
|
+
cacheReadTokens: acc.cachedInputTokens,
|
|
426
|
+
totalTokens: acc.totalTokens,
|
|
427
|
+
totalCost: cost,
|
|
428
|
+
modelsUsed: [...models],
|
|
429
|
+
modelBreakdowns: buildModelBreakdowns(acc, models, cost)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function buildModelBreakdowns(acc, models, totalCost) {
|
|
433
|
+
const modelList = [...models];
|
|
434
|
+
if (modelList.length === 0) return [];
|
|
435
|
+
const costPerModel = totalCost / modelList.length;
|
|
436
|
+
return modelList.map((name) => ({
|
|
437
|
+
modelName: name,
|
|
438
|
+
inputTokens: acc.inputTokens,
|
|
439
|
+
outputTokens: acc.outputTokens,
|
|
440
|
+
cacheCreationTokens: 0,
|
|
441
|
+
cacheReadTokens: acc.cachedInputTokens,
|
|
442
|
+
cost: costPerModel
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
function groupSessions(sessions, options) {
|
|
446
|
+
const tz = options.timezone || "Asia/Shanghai";
|
|
447
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
448
|
+
for (const session of sessions) {
|
|
449
|
+
if (options.project && extractProjectName(session.cwd) !== options.project) continue;
|
|
450
|
+
for (const ev of session.tokenEvents) {
|
|
451
|
+
const evDate = new Date(ev.timestamp);
|
|
452
|
+
if (options.since && evDate < options.since) continue;
|
|
453
|
+
if (options.until && evDate > options.until) continue;
|
|
454
|
+
let key;
|
|
455
|
+
switch (options.groupBy) {
|
|
456
|
+
case "hour":
|
|
457
|
+
key = getHourKey(ev.timestamp, tz);
|
|
458
|
+
break;
|
|
459
|
+
case "month":
|
|
460
|
+
key = getMonthKey(ev.timestamp, tz);
|
|
461
|
+
break;
|
|
462
|
+
case "session":
|
|
463
|
+
key = session.id;
|
|
464
|
+
break;
|
|
465
|
+
case "project":
|
|
466
|
+
key = extractProjectName(session.cwd);
|
|
467
|
+
break;
|
|
468
|
+
default:
|
|
469
|
+
key = getDateKey(ev.timestamp, tz);
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
if (!grouped.has(key)) {
|
|
473
|
+
grouped.set(key, { acc: emptyAcc(), models: /* @__PURE__ */ new Set() });
|
|
474
|
+
}
|
|
475
|
+
const entry = grouped.get(key);
|
|
476
|
+
addAcc(entry.acc, ev);
|
|
477
|
+
if (session.model) entry.models.add(session.model);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return grouped;
|
|
481
|
+
}
|
|
482
|
+
function getDailyResponse(options) {
|
|
483
|
+
const sessions = parseAllSessions();
|
|
484
|
+
const grouped = groupSessions(sessions, { groupBy: "day", ...options });
|
|
485
|
+
const daily = [];
|
|
486
|
+
const totalsAcc = emptyAcc();
|
|
487
|
+
for (const [date, { acc, models: models2 }] of grouped) {
|
|
488
|
+
daily.push(accToEntry(date, acc, models2));
|
|
489
|
+
mergeAcc(totalsAcc, acc);
|
|
490
|
+
}
|
|
491
|
+
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
492
|
+
const models = /* @__PURE__ */ new Set();
|
|
493
|
+
for (const s of sessions) if (s.model) models.add(s.model);
|
|
494
|
+
const totalCost = calculateCost(totalsAcc, models);
|
|
495
|
+
return {
|
|
496
|
+
daily,
|
|
497
|
+
totals: {
|
|
498
|
+
inputTokens: totalsAcc.inputTokens,
|
|
499
|
+
outputTokens: totalsAcc.outputTokens,
|
|
500
|
+
cacheCreationTokens: 0,
|
|
501
|
+
cacheReadTokens: totalsAcc.cachedInputTokens,
|
|
502
|
+
totalTokens: totalsAcc.totalTokens,
|
|
503
|
+
totalCost
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function getProjectsResponse(options) {
|
|
508
|
+
const sessions = parseAllSessions();
|
|
509
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
510
|
+
const projects = {};
|
|
511
|
+
for (const session of sessions) {
|
|
512
|
+
const projectName = extractProjectName(session.cwd);
|
|
513
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const ev of session.tokenEvents) {
|
|
515
|
+
const evDate = new Date(ev.timestamp);
|
|
516
|
+
if (options?.since && evDate < options.since) continue;
|
|
517
|
+
if (options?.until && evDate > options.until) continue;
|
|
518
|
+
const dayKey = getDateKey(ev.timestamp, tz);
|
|
519
|
+
if (!dailyMap.has(dayKey)) {
|
|
520
|
+
dailyMap.set(dayKey, { acc: emptyAcc(), models: /* @__PURE__ */ new Set() });
|
|
521
|
+
}
|
|
522
|
+
addAcc(dailyMap.get(dayKey).acc, ev);
|
|
523
|
+
if (session.model) dailyMap.get(dayKey).models.add(session.model);
|
|
524
|
+
}
|
|
525
|
+
if (!projects[projectName]) projects[projectName] = [];
|
|
526
|
+
for (const [date, { acc, models }] of dailyMap) {
|
|
527
|
+
projects[projectName].push(accToEntry(date, acc, models));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const key of Object.keys(projects)) {
|
|
531
|
+
projects[key].sort((a, b) => a.date.localeCompare(b.date));
|
|
532
|
+
}
|
|
533
|
+
return { projects };
|
|
534
|
+
}
|
|
535
|
+
function getBlocksResponse(options) {
|
|
536
|
+
const sessions = parseAllSessions();
|
|
537
|
+
const grouped = groupSessions(sessions, { groupBy: "hour", ...options });
|
|
538
|
+
const blocks = [];
|
|
539
|
+
let idx = 0;
|
|
540
|
+
for (const [hourKey, { acc, models }] of grouped) {
|
|
541
|
+
const cost = calculateCost(acc, models);
|
|
542
|
+
const [datePart, timePart] = hourKey.split(" ");
|
|
543
|
+
const hour = timePart.split(":")[0];
|
|
544
|
+
blocks.push({
|
|
545
|
+
id: `codex-hour-${idx}`,
|
|
546
|
+
startTime: `${datePart}T${hour}:00:00`,
|
|
547
|
+
endTime: `${datePart}T${hour}:59:59`,
|
|
548
|
+
actualEndTime: null,
|
|
549
|
+
isActive: false,
|
|
550
|
+
isGap: false,
|
|
551
|
+
entries: acc.totalTokens > 0 ? 1 : 0,
|
|
552
|
+
tokenCounts: {
|
|
553
|
+
inputTokens: acc.inputTokens,
|
|
554
|
+
outputTokens: acc.outputTokens,
|
|
555
|
+
cacheCreationInputTokens: 0,
|
|
556
|
+
cacheReadInputTokens: acc.cachedInputTokens
|
|
557
|
+
},
|
|
558
|
+
totalTokens: acc.totalTokens,
|
|
559
|
+
costUSD: cost,
|
|
560
|
+
models: [...models]
|
|
561
|
+
});
|
|
562
|
+
idx++;
|
|
563
|
+
}
|
|
564
|
+
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
565
|
+
return { blocks };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/server/openclawParser.ts
|
|
569
|
+
var import_node_fs3 = require("node:fs");
|
|
570
|
+
var import_node_path3 = require("node:path");
|
|
571
|
+
var import_node_os3 = require("node:os");
|
|
572
|
+
function getOpenClawDirs() {
|
|
573
|
+
const home = (0, import_node_os3.homedir)();
|
|
574
|
+
return [
|
|
575
|
+
(0, import_node_path3.join)(home, ".openclaw"),
|
|
576
|
+
(0, import_node_path3.join)(home, ".clawdbot"),
|
|
577
|
+
// legacy name 1
|
|
578
|
+
(0, import_node_path3.join)(home, ".moltbot"),
|
|
579
|
+
// legacy name 2
|
|
580
|
+
(0, import_node_path3.join)(home, ".moldbot")
|
|
581
|
+
// legacy name 3
|
|
582
|
+
];
|
|
583
|
+
}
|
|
584
|
+
function isOpenClawAccessible() {
|
|
585
|
+
for (const dir of getOpenClawDirs()) {
|
|
586
|
+
try {
|
|
587
|
+
(0, import_node_fs3.accessSync)((0, import_node_path3.join)(dir, "agents"), import_node_fs3.constants.R_OK);
|
|
588
|
+
return true;
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
function scanOpenClawSessions() {
|
|
595
|
+
const refs = [];
|
|
596
|
+
for (const baseDir of getOpenClawDirs()) {
|
|
597
|
+
const agentsDir = (0, import_node_path3.join)(baseDir, "agents");
|
|
598
|
+
let agentEntries;
|
|
599
|
+
try {
|
|
600
|
+
agentEntries = (0, import_node_fs3.readdirSync)(agentsDir);
|
|
601
|
+
} catch {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
for (const agentEntry of agentEntries) {
|
|
605
|
+
const sessionsDir = (0, import_node_path3.join)(agentsDir, agentEntry, "sessions");
|
|
606
|
+
const indexedPaths = /* @__PURE__ */ new Set();
|
|
607
|
+
const indexPath = (0, import_node_path3.join)(sessionsDir, "sessions.json");
|
|
608
|
+
try {
|
|
609
|
+
const raw = (0, import_node_fs3.readFileSync)(indexPath, "utf-8");
|
|
610
|
+
const index = JSON.parse(raw);
|
|
611
|
+
for (const entry of Object.values(index)) {
|
|
612
|
+
if (!entry.sessionId) continue;
|
|
613
|
+
let sessionPath;
|
|
614
|
+
if (entry.sessionFile) {
|
|
615
|
+
const filePath = entry.sessionFile;
|
|
616
|
+
if (filePath.startsWith("/")) {
|
|
617
|
+
if (!getOpenClawDirs().some((dir) => filePath.startsWith(dir))) continue;
|
|
618
|
+
sessionPath = filePath;
|
|
619
|
+
} else {
|
|
620
|
+
sessionPath = (0, import_node_path3.join)(sessionsDir, filePath);
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
sessionPath = (0, import_node_path3.join)(sessionsDir, `${entry.sessionId}.jsonl`);
|
|
624
|
+
}
|
|
625
|
+
indexedPaths.add(sessionPath);
|
|
626
|
+
refs.push({ sessionId: entry.sessionId, sessionFile: sessionPath, agentId: agentEntry });
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
let files;
|
|
631
|
+
try {
|
|
632
|
+
files = (0, import_node_fs3.readdirSync)(sessionsDir);
|
|
633
|
+
} catch {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
for (const f of files) {
|
|
637
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
638
|
+
const fullPath = (0, import_node_path3.join)(sessionsDir, f);
|
|
639
|
+
if (indexedPaths.has(fullPath)) continue;
|
|
640
|
+
const sessionId = f.replace(/\.jsonl.*$/, "");
|
|
641
|
+
refs.push({ sessionId, sessionFile: fullPath, agentId: agentEntry });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return refs;
|
|
646
|
+
}
|
|
647
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
648
|
+
function parseOpenClawSession(ref) {
|
|
649
|
+
let fileMtimeMs = 0;
|
|
650
|
+
try {
|
|
651
|
+
fileMtimeMs = (0, import_node_fs3.statSync)(ref.sessionFile).mtimeMs;
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
const cached = sessionCache.get(ref.sessionFile);
|
|
655
|
+
if (cached && cached.mtime === fileMtimeMs) {
|
|
656
|
+
return cached.result;
|
|
657
|
+
}
|
|
658
|
+
let content;
|
|
659
|
+
try {
|
|
660
|
+
content = (0, import_node_fs3.readFileSync)(ref.sessionFile, "utf-8");
|
|
661
|
+
} catch {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
const tokenEvents = [];
|
|
665
|
+
let currentModel = "";
|
|
666
|
+
let currentProvider = "";
|
|
667
|
+
for (const line of content.split("\n")) {
|
|
668
|
+
const trimmed = line.trim();
|
|
669
|
+
if (!trimmed) continue;
|
|
670
|
+
let obj;
|
|
671
|
+
try {
|
|
672
|
+
obj = JSON.parse(trimmed);
|
|
673
|
+
} catch {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const type = obj.type;
|
|
677
|
+
if (type === "model_change") {
|
|
678
|
+
if (obj.modelId) currentModel = obj.modelId;
|
|
679
|
+
if (obj.provider) currentProvider = obj.provider;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (type === "custom" && obj.customType === "model-snapshot") {
|
|
683
|
+
const data = obj.data || {};
|
|
684
|
+
if (data.modelId) currentModel = data.modelId;
|
|
685
|
+
if (data.provider) currentProvider = data.provider;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (type === "message") {
|
|
689
|
+
const msg = obj.message || {};
|
|
690
|
+
if (msg.role !== "assistant") continue;
|
|
691
|
+
const usage = msg.usage || {};
|
|
692
|
+
if (!usage) continue;
|
|
693
|
+
const model = (msg.model || currentModel || "").trim();
|
|
694
|
+
const provider = (msg.provider || currentProvider || "").trim();
|
|
695
|
+
if (!model) continue;
|
|
696
|
+
if (model) currentModel = model;
|
|
697
|
+
if (provider) currentProvider = provider;
|
|
698
|
+
const input = Number(usage.input ?? 0);
|
|
699
|
+
const output = Number(usage.output ?? 0);
|
|
700
|
+
const cacheRead = Number(usage.cacheRead ?? 0);
|
|
701
|
+
const cacheWrite = Number(usage.cacheWrite ?? 0);
|
|
702
|
+
const costObj = usage.cost || {};
|
|
703
|
+
const cost = Number(costObj.total ?? 0);
|
|
704
|
+
const timestampMs = Number(msg.timestamp ?? fileMtimeMs);
|
|
705
|
+
tokenEvents.push({
|
|
706
|
+
timestampMs,
|
|
707
|
+
inputTokens: Math.max(0, input),
|
|
708
|
+
outputTokens: Math.max(0, output),
|
|
709
|
+
cacheReadTokens: Math.max(0, cacheRead),
|
|
710
|
+
cacheWriteTokens: Math.max(0, cacheWrite),
|
|
711
|
+
totalTokens: Math.max(0, input + output + cacheRead),
|
|
712
|
+
cost: Math.max(0, cost),
|
|
713
|
+
model: `${provider}/${model}`
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (tokenEvents.length === 0) {
|
|
718
|
+
sessionCache.set(ref.sessionFile, { mtime: fileMtimeMs, result: null });
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const result = { id: ref.sessionId, agentId: ref.agentId, tokenEvents };
|
|
722
|
+
sessionCache.set(ref.sessionFile, { mtime: fileMtimeMs, result });
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
function parseAllOpenClawSessions() {
|
|
726
|
+
return scanOpenClawSessions().map(parseOpenClawSession).filter((s) => s !== null);
|
|
727
|
+
}
|
|
728
|
+
var TZ_OFFSETS2 = {
|
|
729
|
+
"Asia/Shanghai": 8,
|
|
730
|
+
"Asia/Tokyo": 9,
|
|
731
|
+
"America/New_York": -5,
|
|
732
|
+
"America/Los_Angeles": -8,
|
|
733
|
+
"Europe/London": 0,
|
|
734
|
+
"UTC": 0
|
|
735
|
+
};
|
|
736
|
+
function getTzOffsetHours2(tz) {
|
|
737
|
+
return TZ_OFFSETS2[tz] ?? 8;
|
|
738
|
+
}
|
|
739
|
+
function msToLocalDate(ms, tz) {
|
|
740
|
+
return new Date(ms + getTzOffsetHours2(tz) * 36e5);
|
|
741
|
+
}
|
|
742
|
+
function getDateKey2(ms, tz) {
|
|
743
|
+
return msToLocalDate(ms, tz).toISOString().slice(0, 10);
|
|
744
|
+
}
|
|
745
|
+
function getHourKey2(ms, tz) {
|
|
746
|
+
const d = msToLocalDate(ms, tz);
|
|
747
|
+
return d.toISOString().slice(0, 13).replace("T", " ") + ":00";
|
|
748
|
+
}
|
|
749
|
+
function emptyAcc2() {
|
|
750
|
+
return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, cost: 0 };
|
|
751
|
+
}
|
|
752
|
+
function addEvent(acc, ev) {
|
|
753
|
+
acc.inputTokens += ev.inputTokens;
|
|
754
|
+
acc.outputTokens += ev.outputTokens;
|
|
755
|
+
acc.cacheReadTokens += ev.cacheReadTokens;
|
|
756
|
+
acc.cacheWriteTokens += ev.cacheWriteTokens;
|
|
757
|
+
acc.totalTokens += ev.totalTokens;
|
|
758
|
+
acc.cost += ev.cost;
|
|
759
|
+
}
|
|
760
|
+
function mergeAcc2(a, b) {
|
|
761
|
+
a.inputTokens += b.inputTokens;
|
|
762
|
+
a.outputTokens += b.outputTokens;
|
|
763
|
+
a.cacheReadTokens += b.cacheReadTokens;
|
|
764
|
+
a.cacheWriteTokens += b.cacheWriteTokens;
|
|
765
|
+
a.totalTokens += b.totalTokens;
|
|
766
|
+
a.cost += b.cost;
|
|
767
|
+
}
|
|
768
|
+
function accToEntry2(date, acc, models) {
|
|
769
|
+
const modelList = [...models];
|
|
770
|
+
const costPerModel = modelList.length > 0 ? acc.cost / modelList.length : 0;
|
|
771
|
+
return {
|
|
772
|
+
date,
|
|
773
|
+
inputTokens: acc.inputTokens,
|
|
774
|
+
outputTokens: acc.outputTokens,
|
|
775
|
+
cacheCreationTokens: acc.cacheWriteTokens,
|
|
776
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
777
|
+
totalTokens: acc.totalTokens,
|
|
778
|
+
totalCost: acc.cost,
|
|
779
|
+
modelsUsed: modelList,
|
|
780
|
+
modelBreakdowns: modelList.map((name) => ({
|
|
781
|
+
modelName: name,
|
|
782
|
+
inputTokens: acc.inputTokens,
|
|
783
|
+
outputTokens: acc.outputTokens,
|
|
784
|
+
cacheCreationTokens: acc.cacheWriteTokens,
|
|
785
|
+
cacheReadTokens: acc.cacheReadTokens,
|
|
786
|
+
cost: costPerModel
|
|
787
|
+
}))
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function getDailyResponse2(options) {
|
|
791
|
+
const sessions = parseAllOpenClawSessions();
|
|
792
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
793
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
794
|
+
const totalsAcc = emptyAcc2();
|
|
795
|
+
for (const session of sessions) {
|
|
796
|
+
if (options?.project && session.agentId !== options.project) continue;
|
|
797
|
+
for (const ev of session.tokenEvents) {
|
|
798
|
+
if (options?.since && ev.timestampMs < options.since.getTime()) continue;
|
|
799
|
+
if (options?.until && ev.timestampMs > options.until.getTime()) continue;
|
|
800
|
+
const key = getDateKey2(ev.timestampMs, tz);
|
|
801
|
+
if (!grouped.has(key)) grouped.set(key, { acc: emptyAcc2(), models: /* @__PURE__ */ new Set() });
|
|
802
|
+
const entry = grouped.get(key);
|
|
803
|
+
addEvent(entry.acc, ev);
|
|
804
|
+
entry.models.add(ev.model);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const daily = [];
|
|
808
|
+
for (const [date, { acc, models }] of grouped) {
|
|
809
|
+
daily.push(accToEntry2(date, acc, models));
|
|
810
|
+
mergeAcc2(totalsAcc, acc);
|
|
811
|
+
}
|
|
812
|
+
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
813
|
+
return {
|
|
814
|
+
daily,
|
|
815
|
+
totals: {
|
|
816
|
+
inputTokens: totalsAcc.inputTokens,
|
|
817
|
+
outputTokens: totalsAcc.outputTokens,
|
|
818
|
+
cacheCreationTokens: totalsAcc.cacheWriteTokens,
|
|
819
|
+
cacheReadTokens: totalsAcc.cacheReadTokens,
|
|
820
|
+
totalTokens: totalsAcc.totalTokens,
|
|
821
|
+
totalCost: totalsAcc.cost
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function getProjectsResponse2(options) {
|
|
826
|
+
const sessions = parseAllOpenClawSessions();
|
|
827
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
828
|
+
const projects = {};
|
|
829
|
+
for (const session of sessions) {
|
|
830
|
+
const projectName = session.agentId;
|
|
831
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const ev of session.tokenEvents) {
|
|
833
|
+
if (options?.since && ev.timestampMs < options.since.getTime()) continue;
|
|
834
|
+
if (options?.until && ev.timestampMs > options.until.getTime()) continue;
|
|
835
|
+
const dayKey = getDateKey2(ev.timestampMs, tz);
|
|
836
|
+
if (!dailyMap.has(dayKey)) dailyMap.set(dayKey, { acc: emptyAcc2(), models: /* @__PURE__ */ new Set() });
|
|
837
|
+
addEvent(dailyMap.get(dayKey).acc, ev);
|
|
838
|
+
dailyMap.get(dayKey).models.add(ev.model);
|
|
839
|
+
}
|
|
840
|
+
if (!projects[projectName]) projects[projectName] = [];
|
|
841
|
+
for (const [date, { acc, models }] of dailyMap) {
|
|
842
|
+
projects[projectName].push(accToEntry2(date, acc, models));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
for (const key of Object.keys(projects)) {
|
|
846
|
+
projects[key].sort((a, b) => a.date.localeCompare(b.date));
|
|
847
|
+
}
|
|
848
|
+
return { projects };
|
|
849
|
+
}
|
|
850
|
+
function getBlocksResponse2(options) {
|
|
851
|
+
const sessions = parseAllOpenClawSessions();
|
|
852
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
853
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
854
|
+
for (const session of sessions) {
|
|
855
|
+
if (options?.project && session.agentId !== options.project) continue;
|
|
856
|
+
for (const ev of session.tokenEvents) {
|
|
857
|
+
if (options?.since && ev.timestampMs < options.since.getTime()) continue;
|
|
858
|
+
if (options?.until && ev.timestampMs > options.until.getTime()) continue;
|
|
859
|
+
const key = getHourKey2(ev.timestampMs, tz);
|
|
860
|
+
if (!grouped.has(key)) grouped.set(key, { acc: emptyAcc2(), models: /* @__PURE__ */ new Set() });
|
|
861
|
+
addEvent(grouped.get(key).acc, ev);
|
|
862
|
+
grouped.get(key).models.add(ev.model);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const blocks = [];
|
|
866
|
+
let idx = 0;
|
|
867
|
+
for (const [hourKey, { acc, models }] of grouped) {
|
|
868
|
+
const [datePart, timePart] = hourKey.split(" ");
|
|
869
|
+
const hour = timePart.split(":")[0];
|
|
870
|
+
blocks.push({
|
|
871
|
+
id: `openclaw-hour-${idx}`,
|
|
872
|
+
startTime: `${datePart}T${hour}:00:00`,
|
|
873
|
+
endTime: `${datePart}T${hour}:59:59`,
|
|
874
|
+
actualEndTime: null,
|
|
875
|
+
isActive: false,
|
|
876
|
+
isGap: false,
|
|
877
|
+
entries: acc.totalTokens > 0 ? 1 : 0,
|
|
878
|
+
tokenCounts: {
|
|
879
|
+
inputTokens: acc.inputTokens,
|
|
880
|
+
outputTokens: acc.outputTokens,
|
|
881
|
+
cacheCreationInputTokens: acc.cacheWriteTokens,
|
|
882
|
+
cacheReadInputTokens: acc.cacheReadTokens
|
|
883
|
+
},
|
|
884
|
+
totalTokens: acc.totalTokens,
|
|
885
|
+
costUSD: acc.cost,
|
|
886
|
+
models: [...models]
|
|
887
|
+
});
|
|
888
|
+
idx++;
|
|
889
|
+
}
|
|
890
|
+
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
891
|
+
return { blocks };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/server/opencodeParser.ts
|
|
895
|
+
var import_node_fs4 = require("node:fs");
|
|
896
|
+
var import_node_child_process = require("node:child_process");
|
|
897
|
+
var import_node_path4 = require("node:path");
|
|
898
|
+
var import_node_os4 = require("node:os");
|
|
899
|
+
var OPENCODE_DB = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".local", "share", "opencode", "opencode.db");
|
|
900
|
+
function isOpencodeAccessible() {
|
|
901
|
+
return (0, import_node_fs4.existsSync)(OPENCODE_DB);
|
|
902
|
+
}
|
|
903
|
+
function queryOpenCodeDB(sql) {
|
|
904
|
+
return (0, import_node_child_process.execSync)(`sqlite3 -json "${OPENCODE_DB}" "${sql}"`, {
|
|
905
|
+
encoding: "utf-8",
|
|
906
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
907
|
+
timeout: 1e4
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
function parseAllOpenCodeEvents(project) {
|
|
911
|
+
let sql = `SELECT data FROM message WHERE json_extract(data, '$.role') = 'assistant'`;
|
|
912
|
+
if (project) {
|
|
913
|
+
sql += ` AND json_extract(data, '$.path.cwd') = '${project.replace(/'/g, "''")}'`;
|
|
914
|
+
}
|
|
915
|
+
let raw;
|
|
916
|
+
try {
|
|
917
|
+
raw = queryOpenCodeDB(sql);
|
|
918
|
+
} catch {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
let rows;
|
|
922
|
+
try {
|
|
923
|
+
rows = JSON.parse(raw);
|
|
924
|
+
} catch {
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
const events = [];
|
|
928
|
+
for (const row of rows) {
|
|
929
|
+
let data;
|
|
930
|
+
try {
|
|
931
|
+
data = JSON.parse(row.data);
|
|
932
|
+
} catch {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const tokens = data.tokens || {};
|
|
936
|
+
const cache2 = tokens.cache || {};
|
|
937
|
+
const time = data.time || {};
|
|
938
|
+
const path = data.path || {};
|
|
939
|
+
const input = Number(tokens.input ?? 0);
|
|
940
|
+
const output = Number(tokens.output ?? 0);
|
|
941
|
+
const cacheRead = Number(cache2.read ?? 0);
|
|
942
|
+
const cacheWrite = Number(cache2.write ?? 0);
|
|
943
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) continue;
|
|
944
|
+
events.push({
|
|
945
|
+
timestampMs: Number(time.created ?? 0),
|
|
946
|
+
inputTokens: Math.max(0, input),
|
|
947
|
+
outputTokens: Math.max(0, output),
|
|
948
|
+
cacheReadTokens: Math.max(0, cacheRead),
|
|
949
|
+
cacheWriteTokens: Math.max(0, cacheWrite),
|
|
950
|
+
totalTokens: Math.max(0, input + output + cacheRead),
|
|
951
|
+
cost: Math.max(0, Number(data.cost ?? 0)),
|
|
952
|
+
model: String(data.modelID ?? "unknown"),
|
|
953
|
+
project: String(path.cwd ?? "")
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return events;
|
|
957
|
+
}
|
|
958
|
+
var TZ_OFFSETS3 = {
|
|
959
|
+
"Asia/Shanghai": 8,
|
|
960
|
+
"Asia/Tokyo": 9,
|
|
961
|
+
"America/New_York": -5,
|
|
962
|
+
"America/Los_Angeles": -8,
|
|
963
|
+
"Europe/London": 0,
|
|
964
|
+
"UTC": 0
|
|
965
|
+
};
|
|
966
|
+
function getTzOffsetHours3(tz) {
|
|
967
|
+
return TZ_OFFSETS3[tz] ?? 8;
|
|
968
|
+
}
|
|
969
|
+
function msToLocalDate2(ms, tz) {
|
|
970
|
+
return new Date(ms + getTzOffsetHours3(tz) * 36e5);
|
|
971
|
+
}
|
|
972
|
+
function getDateKey3(ms, tz) {
|
|
973
|
+
return msToLocalDate2(ms, tz).toISOString().slice(0, 10);
|
|
974
|
+
}
|
|
975
|
+
function getHourKey3(ms, tz) {
|
|
976
|
+
const d = msToLocalDate2(ms, tz);
|
|
977
|
+
return d.toISOString().slice(0, 13).replace("T", " ") + ":00";
|
|
978
|
+
}
|
|
979
|
+
function emptyAcc3() {
|
|
980
|
+
return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, cost: 0 };
|
|
981
|
+
}
|
|
982
|
+
function addEvent2(acc, ev) {
|
|
983
|
+
acc.inputTokens += ev.inputTokens;
|
|
984
|
+
acc.outputTokens += ev.outputTokens;
|
|
985
|
+
acc.cacheReadTokens += ev.cacheReadTokens;
|
|
986
|
+
acc.cacheWriteTokens += ev.cacheWriteTokens;
|
|
987
|
+
acc.totalTokens += ev.totalTokens;
|
|
988
|
+
acc.cost += ev.cost;
|
|
989
|
+
}
|
|
990
|
+
function getDailyResponse3(options) {
|
|
991
|
+
const events = parseAllOpenCodeEvents(options?.project);
|
|
992
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
993
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const ev of events) {
|
|
995
|
+
const key = getDateKey3(ev.timestampMs, tz);
|
|
996
|
+
if (!grouped.has(key)) grouped.set(key, { totals: emptyAcc3(), models: /* @__PURE__ */ new Map() });
|
|
997
|
+
const g = grouped.get(key);
|
|
998
|
+
addEvent2(g.totals, ev);
|
|
999
|
+
if (!g.models.has(ev.model)) {
|
|
1000
|
+
g.models.set(ev.model, { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, cost: 0 });
|
|
1001
|
+
}
|
|
1002
|
+
const m = g.models.get(ev.model);
|
|
1003
|
+
m.inputTokens += ev.inputTokens;
|
|
1004
|
+
m.outputTokens += ev.outputTokens;
|
|
1005
|
+
m.cacheCreationTokens += ev.cacheWriteTokens;
|
|
1006
|
+
m.cacheReadTokens += ev.cacheReadTokens;
|
|
1007
|
+
m.cost += ev.cost;
|
|
1008
|
+
}
|
|
1009
|
+
const totalsAcc = emptyAcc3();
|
|
1010
|
+
const daily = [];
|
|
1011
|
+
for (const [date, g] of grouped) {
|
|
1012
|
+
mergeAcc3(totalsAcc, g.totals);
|
|
1013
|
+
const modelList = [...g.models.keys()];
|
|
1014
|
+
daily.push({
|
|
1015
|
+
date,
|
|
1016
|
+
inputTokens: g.totals.inputTokens,
|
|
1017
|
+
outputTokens: g.totals.outputTokens,
|
|
1018
|
+
cacheCreationTokens: g.totals.cacheWriteTokens,
|
|
1019
|
+
cacheReadTokens: g.totals.cacheReadTokens,
|
|
1020
|
+
totalTokens: g.totals.totalTokens,
|
|
1021
|
+
totalCost: g.totals.cost,
|
|
1022
|
+
modelsUsed: modelList,
|
|
1023
|
+
modelBreakdowns: modelList.map((name) => {
|
|
1024
|
+
const m = g.models.get(name);
|
|
1025
|
+
return {
|
|
1026
|
+
modelName: name,
|
|
1027
|
+
inputTokens: m.inputTokens,
|
|
1028
|
+
outputTokens: m.outputTokens,
|
|
1029
|
+
cacheCreationTokens: m.cacheCreationTokens,
|
|
1030
|
+
cacheReadTokens: m.cacheReadTokens,
|
|
1031
|
+
cost: m.cost
|
|
1032
|
+
};
|
|
1033
|
+
})
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
1037
|
+
return {
|
|
1038
|
+
daily,
|
|
1039
|
+
totals: {
|
|
1040
|
+
inputTokens: totalsAcc.inputTokens,
|
|
1041
|
+
outputTokens: totalsAcc.outputTokens,
|
|
1042
|
+
cacheCreationTokens: totalsAcc.cacheWriteTokens,
|
|
1043
|
+
cacheReadTokens: totalsAcc.cacheReadTokens,
|
|
1044
|
+
totalTokens: totalsAcc.totalTokens,
|
|
1045
|
+
totalCost: totalsAcc.cost
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function mergeAcc3(a, b) {
|
|
1050
|
+
a.inputTokens += b.inputTokens;
|
|
1051
|
+
a.outputTokens += b.outputTokens;
|
|
1052
|
+
a.cacheReadTokens += b.cacheReadTokens;
|
|
1053
|
+
a.cacheWriteTokens += b.cacheWriteTokens;
|
|
1054
|
+
a.totalTokens += b.totalTokens;
|
|
1055
|
+
a.cost += b.cost;
|
|
1056
|
+
}
|
|
1057
|
+
function getProjectsResponse3(options) {
|
|
1058
|
+
const events = parseAllOpenCodeEvents();
|
|
1059
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
1060
|
+
const projects = {};
|
|
1061
|
+
for (const ev of events) {
|
|
1062
|
+
const projectName = ev.project || "unknown";
|
|
1063
|
+
const dayKey = getDateKey3(ev.timestampMs, tz);
|
|
1064
|
+
if (!projects[projectName]) projects[projectName] = [];
|
|
1065
|
+
let dayEntry = projects[projectName].find((d) => d.date === dayKey);
|
|
1066
|
+
if (!dayEntry) {
|
|
1067
|
+
dayEntry = {
|
|
1068
|
+
date: dayKey,
|
|
1069
|
+
inputTokens: 0,
|
|
1070
|
+
outputTokens: 0,
|
|
1071
|
+
cacheCreationTokens: 0,
|
|
1072
|
+
cacheReadTokens: 0,
|
|
1073
|
+
totalTokens: 0,
|
|
1074
|
+
totalCost: 0,
|
|
1075
|
+
modelsUsed: [],
|
|
1076
|
+
modelBreakdowns: []
|
|
1077
|
+
};
|
|
1078
|
+
projects[projectName].push(dayEntry);
|
|
1079
|
+
}
|
|
1080
|
+
dayEntry.inputTokens += ev.inputTokens;
|
|
1081
|
+
dayEntry.outputTokens += ev.outputTokens;
|
|
1082
|
+
dayEntry.cacheCreationTokens += ev.cacheWriteTokens;
|
|
1083
|
+
dayEntry.cacheReadTokens += ev.cacheReadTokens;
|
|
1084
|
+
dayEntry.totalTokens += ev.totalTokens;
|
|
1085
|
+
dayEntry.totalCost += ev.cost;
|
|
1086
|
+
if (!dayEntry.modelsUsed.includes(ev.model)) {
|
|
1087
|
+
dayEntry.modelsUsed.push(ev.model);
|
|
1088
|
+
}
|
|
1089
|
+
let breakdown = dayEntry.modelBreakdowns.find((b) => b.modelName === ev.model);
|
|
1090
|
+
if (!breakdown) {
|
|
1091
|
+
breakdown = {
|
|
1092
|
+
modelName: ev.model,
|
|
1093
|
+
inputTokens: 0,
|
|
1094
|
+
outputTokens: 0,
|
|
1095
|
+
cacheCreationTokens: 0,
|
|
1096
|
+
cacheReadTokens: 0,
|
|
1097
|
+
cost: 0
|
|
1098
|
+
};
|
|
1099
|
+
dayEntry.modelBreakdowns.push(breakdown);
|
|
1100
|
+
}
|
|
1101
|
+
breakdown.inputTokens += ev.inputTokens;
|
|
1102
|
+
breakdown.outputTokens += ev.outputTokens;
|
|
1103
|
+
breakdown.cacheCreationTokens += ev.cacheWriteTokens;
|
|
1104
|
+
breakdown.cacheReadTokens += ev.cacheReadTokens;
|
|
1105
|
+
breakdown.cost += ev.cost;
|
|
1106
|
+
}
|
|
1107
|
+
for (const key of Object.keys(projects)) {
|
|
1108
|
+
projects[key].sort((a, b) => a.date.localeCompare(b.date));
|
|
1109
|
+
}
|
|
1110
|
+
return { projects };
|
|
1111
|
+
}
|
|
1112
|
+
function getBlocksResponse3(options) {
|
|
1113
|
+
const events = parseAllOpenCodeEvents(options?.project);
|
|
1114
|
+
const tz = options?.timezone || "Asia/Shanghai";
|
|
1115
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1116
|
+
for (const ev of events) {
|
|
1117
|
+
const key = getHourKey3(ev.timestampMs, tz);
|
|
1118
|
+
if (!grouped.has(key)) grouped.set(key, { acc: emptyAcc3(), models: /* @__PURE__ */ new Set() });
|
|
1119
|
+
addEvent2(grouped.get(key).acc, ev);
|
|
1120
|
+
grouped.get(key).models.add(ev.model);
|
|
1121
|
+
}
|
|
1122
|
+
const blocks = [];
|
|
1123
|
+
let idx = 0;
|
|
1124
|
+
for (const [hourKey, { acc, models }] of grouped) {
|
|
1125
|
+
const [datePart, timePart] = hourKey.split(" ");
|
|
1126
|
+
const hour = timePart.split(":")[0];
|
|
1127
|
+
blocks.push({
|
|
1128
|
+
id: `opencode-hour-${idx}`,
|
|
1129
|
+
startTime: `${datePart}T${hour}:00:00`,
|
|
1130
|
+
endTime: `${datePart}T${hour}:59:59`,
|
|
1131
|
+
actualEndTime: null,
|
|
1132
|
+
isActive: false,
|
|
1133
|
+
isGap: false,
|
|
1134
|
+
entries: acc.totalTokens > 0 ? 1 : 0,
|
|
1135
|
+
tokenCounts: {
|
|
1136
|
+
inputTokens: acc.inputTokens,
|
|
1137
|
+
outputTokens: acc.outputTokens,
|
|
1138
|
+
cacheCreationInputTokens: acc.cacheWriteTokens,
|
|
1139
|
+
cacheReadInputTokens: acc.cacheReadTokens
|
|
1140
|
+
},
|
|
1141
|
+
totalTokens: acc.totalTokens,
|
|
1142
|
+
costUSD: acc.cost,
|
|
1143
|
+
models: [...models]
|
|
1144
|
+
});
|
|
1145
|
+
idx++;
|
|
1146
|
+
}
|
|
1147
|
+
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
1148
|
+
return { blocks };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/server/claudeJsonlParser.ts
|
|
1152
|
+
var import_node_fs5 = require("node:fs");
|
|
1153
|
+
var import_node_path5 = require("node:path");
|
|
1154
|
+
var import_node_os5 = require("node:os");
|
|
1155
|
+
var MODEL_PRICING2 = {
|
|
1156
|
+
// Claude 4.6
|
|
1157
|
+
"claude-opus-4-6": { inputPer1M: 15, cacheReadPer1M: 1.5, outputPer1M: 75 },
|
|
1158
|
+
"claude-sonnet-4-6": { inputPer1M: 3, cacheReadPer1M: 0.3, outputPer1M: 15 },
|
|
1159
|
+
// Claude 4.5
|
|
1160
|
+
"claude-sonnet-4-5-20250514": { inputPer1M: 3, cacheReadPer1M: 0.3, outputPer1M: 15 },
|
|
1161
|
+
"claude-haiku-4-5-20251001": { inputPer1M: 0.8, cacheReadPer1M: 0.08, outputPer1M: 4 },
|
|
1162
|
+
// Older Claude models
|
|
1163
|
+
"claude-3-5-sonnet-20241022": { inputPer1M: 3, cacheReadPer1M: 0.3, outputPer1M: 15 },
|
|
1164
|
+
"claude-3-5-haiku-20241022": { inputPer1M: 0.8, cacheReadPer1M: 0.08, outputPer1M: 4 },
|
|
1165
|
+
"claude-3-opus-20240229": { inputPer1M: 15, cacheReadPer1M: 1.5, outputPer1M: 75 },
|
|
1166
|
+
"claude-3-haiku-20240307": { inputPer1M: 0.25, cacheReadPer1M: 0.03, outputPer1M: 1.25 }
|
|
1167
|
+
};
|
|
1168
|
+
var DEFAULT_PRICING2 = { inputPer1M: 3, cacheReadPer1M: 0.3, outputPer1M: 15 };
|
|
1169
|
+
function getPricing(model) {
|
|
1170
|
+
if (MODEL_PRICING2[model]) return MODEL_PRICING2[model];
|
|
1171
|
+
const lower = model.toLowerCase();
|
|
1172
|
+
for (const key of Object.keys(MODEL_PRICING2)) {
|
|
1173
|
+
if (lower.startsWith(key) || lower.includes(key)) return MODEL_PRICING2[key];
|
|
1174
|
+
}
|
|
1175
|
+
return DEFAULT_PRICING2;
|
|
1176
|
+
}
|
|
1177
|
+
function calculateCost2(inputTokens, cacheReadTokens, outputTokens, model) {
|
|
1178
|
+
const p = getPricing(model);
|
|
1179
|
+
const nonCachedInput = Math.max(inputTokens - cacheReadTokens, 0);
|
|
1180
|
+
return nonCachedInput / 1e6 * p.inputPer1M + cacheReadTokens / 1e6 * p.cacheReadPer1M + outputTokens / 1e6 * p.outputPer1M;
|
|
1181
|
+
}
|
|
1182
|
+
var CLAUDE_PROJECTS_DIR = (0, import_node_path5.join)((0, import_node_os5.homedir)(), ".claude", "projects");
|
|
1183
|
+
var fileCache = /* @__PURE__ */ new Map();
|
|
1184
|
+
function extractProjectName2(dirName) {
|
|
1185
|
+
const parts = dirName.replace(/^-/, "").split("-");
|
|
1186
|
+
return parts[parts.length - 1] || dirName;
|
|
1187
|
+
}
|
|
1188
|
+
function matchesProject(dirName, filter) {
|
|
1189
|
+
return extractProjectName2(dirName) === extractProjectName2(filter);
|
|
1190
|
+
}
|
|
1191
|
+
function findJsonlFiles(dir) {
|
|
1192
|
+
const results = [];
|
|
1193
|
+
try {
|
|
1194
|
+
const entries = (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true });
|
|
1195
|
+
for (const entry of entries) {
|
|
1196
|
+
if (entry.isDirectory()) {
|
|
1197
|
+
results.push(...findJsonlFiles((0, import_node_path5.join)(dir, entry.name)));
|
|
1198
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
1199
|
+
results.push((0, import_node_path5.join)(dir, entry.name));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
} catch {
|
|
1203
|
+
}
|
|
1204
|
+
return results;
|
|
1205
|
+
}
|
|
1206
|
+
function parseAllSessions2(project) {
|
|
1207
|
+
if (!(0, import_node_fs5.existsSync)(CLAUDE_PROJECTS_DIR)) return [];
|
|
1208
|
+
const results = [];
|
|
1209
|
+
const projectDirs = (0, import_node_fs5.readdirSync)(CLAUDE_PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1210
|
+
for (const dirName of projectDirs) {
|
|
1211
|
+
if (project && !matchesProject(dirName, project)) continue;
|
|
1212
|
+
const dirPath = (0, import_node_path5.join)(CLAUDE_PROJECTS_DIR, dirName);
|
|
1213
|
+
const files = findJsonlFiles(dirPath);
|
|
1214
|
+
for (const filePath of files) {
|
|
1215
|
+
let mtime = 0;
|
|
1216
|
+
try {
|
|
1217
|
+
mtime = (0, import_node_fs5.statSync)(filePath).mtimeMs;
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
const cached = fileCache.get(filePath);
|
|
1221
|
+
if (cached && cached.mtime === mtime) {
|
|
1222
|
+
results.push(...cached.entries);
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const entries = [];
|
|
1226
|
+
let content;
|
|
1227
|
+
try {
|
|
1228
|
+
content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
|
|
1229
|
+
} catch {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
for (const line of content.split("\n")) {
|
|
1233
|
+
const trimmed = line.trim();
|
|
1234
|
+
if (!trimmed) continue;
|
|
1235
|
+
let obj;
|
|
1236
|
+
try {
|
|
1237
|
+
obj = JSON.parse(trimmed);
|
|
1238
|
+
} catch {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
1242
|
+
const msg = obj.message;
|
|
1243
|
+
const usage = msg.usage || {};
|
|
1244
|
+
const inputTokens = usage.input_tokens || 0;
|
|
1245
|
+
const outputTokens = usage.output_tokens || 0;
|
|
1246
|
+
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
1247
|
+
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
1248
|
+
const totalTokens = inputTokens + outputTokens + cacheReadTokens;
|
|
1249
|
+
if (totalTokens === 0) continue;
|
|
1250
|
+
entries.push({
|
|
1251
|
+
timestamp: obj.timestamp,
|
|
1252
|
+
model: msg.model || "unknown",
|
|
1253
|
+
inputTokens,
|
|
1254
|
+
outputTokens,
|
|
1255
|
+
cacheCreationTokens,
|
|
1256
|
+
cacheReadTokens,
|
|
1257
|
+
projectDir: dirName
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
fileCache.set(filePath, { mtime, entries });
|
|
1261
|
+
results.push(...entries);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return results;
|
|
1265
|
+
}
|
|
1266
|
+
var TZ_OFFSETS4 = {
|
|
1267
|
+
"Asia/Shanghai": 8,
|
|
1268
|
+
"Asia/Tokyo": 9,
|
|
1269
|
+
"America/New_York": -5,
|
|
1270
|
+
"America/Los_Angeles": -8,
|
|
1271
|
+
"Europe/London": 0,
|
|
1272
|
+
"UTC": 0
|
|
1273
|
+
};
|
|
1274
|
+
function getDateKey4(timestamp, tz) {
|
|
1275
|
+
const offset = (TZ_OFFSETS4[tz] ?? 8) * 36e5;
|
|
1276
|
+
const d = new Date(new Date(timestamp).getTime() + offset);
|
|
1277
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
1278
|
+
}
|
|
1279
|
+
function getHourKey4(timestamp, tz) {
|
|
1280
|
+
const offset = (TZ_OFFSETS4[tz] ?? 8) * 36e5;
|
|
1281
|
+
const d = new Date(new Date(timestamp).getTime() + offset);
|
|
1282
|
+
const yyyy = d.getUTCFullYear();
|
|
1283
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
1284
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
1285
|
+
const hh = String(d.getUTCHours()).padStart(2, "0");
|
|
1286
|
+
return `${yyyy}-${mm}-${dd}T${hh}`;
|
|
1287
|
+
}
|
|
1288
|
+
function toDailyEntry(agg) {
|
|
1289
|
+
const modelBreakdowns = [...agg.models.entries()].map(([modelName, m]) => ({
|
|
1290
|
+
modelName,
|
|
1291
|
+
inputTokens: m.input,
|
|
1292
|
+
outputTokens: m.output,
|
|
1293
|
+
cacheCreationTokens: m.cacheCreation,
|
|
1294
|
+
cacheReadTokens: m.cacheRead,
|
|
1295
|
+
cost: m.cost
|
|
1296
|
+
}));
|
|
1297
|
+
return {
|
|
1298
|
+
date: agg.date,
|
|
1299
|
+
inputTokens: agg.inputTokens,
|
|
1300
|
+
outputTokens: agg.outputTokens,
|
|
1301
|
+
cacheCreationTokens: agg.cacheCreationTokens,
|
|
1302
|
+
cacheReadTokens: agg.cacheReadTokens,
|
|
1303
|
+
totalTokens: agg.totalTokens,
|
|
1304
|
+
totalCost: Math.round(agg.totalCost * 1e4) / 1e4,
|
|
1305
|
+
modelsUsed: [...agg.models.keys()],
|
|
1306
|
+
modelBreakdowns
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
var DEFAULT_TZ = "Asia/Shanghai";
|
|
1310
|
+
function getDailyResponse4(project, tz = DEFAULT_TZ) {
|
|
1311
|
+
const entries = parseAllSessions2(project);
|
|
1312
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
1313
|
+
for (const e of entries) {
|
|
1314
|
+
const date = getDateKey4(e.timestamp, tz);
|
|
1315
|
+
if (!dayMap.has(date)) {
|
|
1316
|
+
dayMap.set(date, {
|
|
1317
|
+
date,
|
|
1318
|
+
inputTokens: 0,
|
|
1319
|
+
outputTokens: 0,
|
|
1320
|
+
cacheCreationTokens: 0,
|
|
1321
|
+
cacheReadTokens: 0,
|
|
1322
|
+
totalTokens: 0,
|
|
1323
|
+
totalCost: 0,
|
|
1324
|
+
models: /* @__PURE__ */ new Map()
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
const agg = dayMap.get(date);
|
|
1328
|
+
agg.inputTokens += e.inputTokens;
|
|
1329
|
+
agg.outputTokens += e.outputTokens;
|
|
1330
|
+
agg.cacheCreationTokens += e.cacheCreationTokens;
|
|
1331
|
+
agg.cacheReadTokens += e.cacheReadTokens;
|
|
1332
|
+
agg.totalTokens += e.inputTokens + e.outputTokens + e.cacheReadTokens;
|
|
1333
|
+
const cost = calculateCost2(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model);
|
|
1334
|
+
agg.totalCost += cost;
|
|
1335
|
+
if (!agg.models.has(e.model)) {
|
|
1336
|
+
agg.models.set(e.model, { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, cost: 0 });
|
|
1337
|
+
}
|
|
1338
|
+
const m = agg.models.get(e.model);
|
|
1339
|
+
m.input += e.inputTokens;
|
|
1340
|
+
m.output += e.outputTokens;
|
|
1341
|
+
m.cacheCreation += e.cacheCreationTokens;
|
|
1342
|
+
m.cacheRead += e.cacheReadTokens;
|
|
1343
|
+
m.cost += cost;
|
|
1344
|
+
}
|
|
1345
|
+
const daily = [...dayMap.values()].sort((a, b) => a.date.localeCompare(b.date)).map(toDailyEntry);
|
|
1346
|
+
const totals = daily.reduce((acc, d) => ({
|
|
1347
|
+
inputTokens: acc.inputTokens + d.inputTokens,
|
|
1348
|
+
outputTokens: acc.outputTokens + d.outputTokens,
|
|
1349
|
+
cacheCreationTokens: acc.cacheCreationTokens + d.cacheCreationTokens,
|
|
1350
|
+
cacheReadTokens: acc.cacheReadTokens + d.cacheReadTokens,
|
|
1351
|
+
totalTokens: acc.totalTokens + d.totalTokens,
|
|
1352
|
+
totalCost: acc.totalCost + d.totalCost
|
|
1353
|
+
}), { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, totalTokens: 0, totalCost: 0 });
|
|
1354
|
+
return { daily, totals };
|
|
1355
|
+
}
|
|
1356
|
+
function getProjectsResponse4(tz = DEFAULT_TZ) {
|
|
1357
|
+
const entries = parseAllSessions2();
|
|
1358
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
1359
|
+
for (const e of entries) {
|
|
1360
|
+
const date = getDateKey4(e.timestamp, tz);
|
|
1361
|
+
const projectName = e.projectDir;
|
|
1362
|
+
if (!projectMap.has(projectName)) {
|
|
1363
|
+
projectMap.set(projectName, /* @__PURE__ */ new Map());
|
|
1364
|
+
}
|
|
1365
|
+
const dayMap = projectMap.get(projectName);
|
|
1366
|
+
if (!dayMap.has(date)) {
|
|
1367
|
+
dayMap.set(date, {
|
|
1368
|
+
date,
|
|
1369
|
+
inputTokens: 0,
|
|
1370
|
+
outputTokens: 0,
|
|
1371
|
+
cacheCreationTokens: 0,
|
|
1372
|
+
cacheReadTokens: 0,
|
|
1373
|
+
totalTokens: 0,
|
|
1374
|
+
totalCost: 0,
|
|
1375
|
+
models: /* @__PURE__ */ new Map()
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
const agg = dayMap.get(date);
|
|
1379
|
+
agg.inputTokens += e.inputTokens;
|
|
1380
|
+
agg.outputTokens += e.outputTokens;
|
|
1381
|
+
agg.cacheCreationTokens += e.cacheCreationTokens;
|
|
1382
|
+
agg.cacheReadTokens += e.cacheReadTokens;
|
|
1383
|
+
agg.totalTokens += e.inputTokens + e.outputTokens + e.cacheReadTokens;
|
|
1384
|
+
const cost = calculateCost2(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model);
|
|
1385
|
+
agg.totalCost += cost;
|
|
1386
|
+
if (!agg.models.has(e.model)) {
|
|
1387
|
+
agg.models.set(e.model, { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, cost: 0 });
|
|
1388
|
+
}
|
|
1389
|
+
const m = agg.models.get(e.model);
|
|
1390
|
+
m.input += e.inputTokens;
|
|
1391
|
+
m.output += e.outputTokens;
|
|
1392
|
+
m.cacheCreation += e.cacheCreationTokens;
|
|
1393
|
+
m.cacheRead += e.cacheReadTokens;
|
|
1394
|
+
m.cost += cost;
|
|
1395
|
+
}
|
|
1396
|
+
const projects = {};
|
|
1397
|
+
for (const [projectName, dayMap] of projectMap) {
|
|
1398
|
+
projects[projectName] = [...dayMap.values()].sort((a, b) => a.date.localeCompare(b.date)).map(toDailyEntry);
|
|
1399
|
+
}
|
|
1400
|
+
return { projects };
|
|
1401
|
+
}
|
|
1402
|
+
function getBlocksResponse4(project, tz = DEFAULT_TZ) {
|
|
1403
|
+
const entries = parseAllSessions2(project);
|
|
1404
|
+
const hourMap = /* @__PURE__ */ new Map();
|
|
1405
|
+
for (const e of entries) {
|
|
1406
|
+
const hourKey = getHourKey4(e.timestamp, tz);
|
|
1407
|
+
if (!hourMap.has(hourKey)) {
|
|
1408
|
+
hourMap.set(hourKey, {
|
|
1409
|
+
inputTokens: 0,
|
|
1410
|
+
outputTokens: 0,
|
|
1411
|
+
cacheCreationTokens: 0,
|
|
1412
|
+
cacheReadTokens: 0,
|
|
1413
|
+
costUSD: 0,
|
|
1414
|
+
models: /* @__PURE__ */ new Set()
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
const bucket = hourMap.get(hourKey);
|
|
1418
|
+
bucket.inputTokens += e.inputTokens;
|
|
1419
|
+
bucket.outputTokens += e.outputTokens;
|
|
1420
|
+
bucket.cacheCreationTokens += e.cacheCreationTokens;
|
|
1421
|
+
bucket.cacheReadTokens += e.cacheReadTokens;
|
|
1422
|
+
bucket.costUSD += calculateCost2(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model);
|
|
1423
|
+
bucket.models.add(e.model);
|
|
1424
|
+
}
|
|
1425
|
+
const blocks = [];
|
|
1426
|
+
let idx = 0;
|
|
1427
|
+
for (const [hourKey, bucket] of hourMap) {
|
|
1428
|
+
const totalTokens = bucket.inputTokens + bucket.outputTokens + bucket.cacheReadTokens;
|
|
1429
|
+
blocks.push({
|
|
1430
|
+
id: `claude-${idx}`,
|
|
1431
|
+
startTime: `${hourKey}:00:00`,
|
|
1432
|
+
endTime: `${hourKey}:59:59`,
|
|
1433
|
+
actualEndTime: null,
|
|
1434
|
+
isActive: false,
|
|
1435
|
+
isGap: false,
|
|
1436
|
+
entries: totalTokens > 0 ? 1 : 0,
|
|
1437
|
+
tokenCounts: {
|
|
1438
|
+
inputTokens: bucket.inputTokens,
|
|
1439
|
+
outputTokens: bucket.outputTokens,
|
|
1440
|
+
cacheCreationInputTokens: bucket.cacheCreationTokens,
|
|
1441
|
+
cacheReadInputTokens: bucket.cacheReadTokens
|
|
1442
|
+
},
|
|
1443
|
+
totalTokens,
|
|
1444
|
+
costUSD: Math.round(bucket.costUSD * 1e4) / 1e4,
|
|
1445
|
+
models: [...bucket.models]
|
|
1446
|
+
});
|
|
1447
|
+
idx++;
|
|
1448
|
+
}
|
|
1449
|
+
blocks.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
1450
|
+
return { blocks };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/server/routes/daily.ts
|
|
1454
|
+
async function getDaily(req, res) {
|
|
1455
|
+
const agent = req.query.agent || "claude";
|
|
1456
|
+
const cacheKey = `daily:${agent}`;
|
|
1457
|
+
try {
|
|
1458
|
+
const cached = cache.get(cacheKey);
|
|
1459
|
+
if (cached) {
|
|
1460
|
+
res.json(cached);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
const stale = cache.getStale(cacheKey);
|
|
1464
|
+
if (stale) {
|
|
1465
|
+
refreshDailyCache(agent, cacheKey);
|
|
1466
|
+
res.json(stale);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const data = await fetchDailyData(agent);
|
|
1470
|
+
cache.set(cacheKey, data);
|
|
1471
|
+
res.json(data);
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1474
|
+
console.error("Error fetching daily data:", error);
|
|
1475
|
+
res.status(502).json({
|
|
1476
|
+
error: `Failed to fetch daily data from ${agent}`,
|
|
1477
|
+
hint: message
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function fetchDailyData(agent) {
|
|
1482
|
+
if (agent === "codex") {
|
|
1483
|
+
return Promise.resolve(getDailyResponse());
|
|
1484
|
+
} else if (agent === "openclaw") {
|
|
1485
|
+
return Promise.resolve(validateDaily(getDailyResponse2()));
|
|
1486
|
+
} else if (agent === "opencode") {
|
|
1487
|
+
return Promise.resolve(validateDaily(getDailyResponse3()));
|
|
1488
|
+
} else {
|
|
1489
|
+
return Promise.resolve(validateDaily(getDailyResponse4()));
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
function refreshDailyCache(agent, cacheKey) {
|
|
1493
|
+
fetchDailyData(agent).then((data) => cache.set(cacheKey, data)).catch((err) => console.error("Background refresh failed (daily):", err));
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/server/routes/monthly.ts
|
|
1497
|
+
async function getMonthly(req, res) {
|
|
1498
|
+
const agent = req.query.agent || "claude";
|
|
1499
|
+
const cacheKey = `monthly:${agent}`;
|
|
1500
|
+
try {
|
|
1501
|
+
const cached = cache.get(cacheKey);
|
|
1502
|
+
if (cached) {
|
|
1503
|
+
res.json(cached);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const stale = cache.getStale(cacheKey);
|
|
1507
|
+
if (stale) {
|
|
1508
|
+
res.json(stale);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
let data;
|
|
1512
|
+
if (agent === "codex") {
|
|
1513
|
+
data = validateDaily(getDailyResponse());
|
|
1514
|
+
} else if (agent === "openclaw") {
|
|
1515
|
+
data = validateDaily(getDailyResponse2());
|
|
1516
|
+
} else {
|
|
1517
|
+
data = validateDaily(getDailyResponse4());
|
|
1518
|
+
}
|
|
1519
|
+
cache.set(cacheKey, data);
|
|
1520
|
+
res.json(data);
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1523
|
+
console.error("Error fetching monthly data:", error);
|
|
1524
|
+
res.status(502).json({
|
|
1525
|
+
error: "Failed to fetch monthly data",
|
|
1526
|
+
hint: message
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/server/routes/session.ts
|
|
1532
|
+
async function getSession(req, res) {
|
|
1533
|
+
const agent = req.query.agent || "claude";
|
|
1534
|
+
const cacheKey = `session:${agent}`;
|
|
1535
|
+
try {
|
|
1536
|
+
const cached = cache.get(cacheKey);
|
|
1537
|
+
if (cached) {
|
|
1538
|
+
res.json(cached);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const stale = cache.getStale(cacheKey);
|
|
1542
|
+
if (stale) {
|
|
1543
|
+
res.json(stale);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
let data;
|
|
1547
|
+
if (agent === "codex") {
|
|
1548
|
+
data = validateDaily(getDailyResponse());
|
|
1549
|
+
} else if (agent === "openclaw") {
|
|
1550
|
+
data = validateDaily(getDailyResponse2());
|
|
1551
|
+
} else {
|
|
1552
|
+
data = validateDaily(getDailyResponse4());
|
|
1553
|
+
}
|
|
1554
|
+
cache.set(cacheKey, data);
|
|
1555
|
+
res.json(data);
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1558
|
+
console.error("Error fetching session data:", error);
|
|
1559
|
+
res.status(502).json({
|
|
1560
|
+
error: "Failed to fetch session data",
|
|
1561
|
+
hint: message
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/server/routes/projects.ts
|
|
1567
|
+
async function getProjects(req, res) {
|
|
1568
|
+
const agent = req.query.agent || "claude";
|
|
1569
|
+
const cacheKey = `projects:${agent}`;
|
|
1570
|
+
try {
|
|
1571
|
+
const cached = cache.get(cacheKey);
|
|
1572
|
+
if (cached) {
|
|
1573
|
+
res.json(cached);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const stale = cache.getStale(cacheKey);
|
|
1577
|
+
if (stale) {
|
|
1578
|
+
refreshProjectsCache(agent, cacheKey);
|
|
1579
|
+
res.json(stale);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const data = fetchProjectsData(agent);
|
|
1583
|
+
cache.set(cacheKey, data);
|
|
1584
|
+
res.json(data);
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1587
|
+
console.error("Error fetching projects data:", error);
|
|
1588
|
+
res.status(502).json({
|
|
1589
|
+
error: `Failed to fetch projects data from ${agent}`,
|
|
1590
|
+
hint: message
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function fetchProjectsData(agent) {
|
|
1595
|
+
if (agent === "codex") {
|
|
1596
|
+
return getProjectsResponse();
|
|
1597
|
+
} else if (agent === "openclaw") {
|
|
1598
|
+
return validateProjects(getProjectsResponse2());
|
|
1599
|
+
} else if (agent === "opencode") {
|
|
1600
|
+
return validateProjects(getProjectsResponse3());
|
|
1601
|
+
} else {
|
|
1602
|
+
return validateProjects(getProjectsResponse4());
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function refreshProjectsCache(agent, cacheKey) {
|
|
1606
|
+
Promise.resolve().then(() => {
|
|
1607
|
+
const data = fetchProjectsData(agent);
|
|
1608
|
+
cache.set(cacheKey, data);
|
|
1609
|
+
}).catch((err) => console.error("Background refresh failed (projects):", err));
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/server/routes/blocks.ts
|
|
1613
|
+
async function getBlocks(req, res) {
|
|
1614
|
+
const agent = req.query.agent || "claude";
|
|
1615
|
+
const project = req.query.project || void 0;
|
|
1616
|
+
try {
|
|
1617
|
+
const cacheKey = `blocks:${agent}:${project || "all"}`;
|
|
1618
|
+
const cached = cache.get(cacheKey);
|
|
1619
|
+
if (cached) {
|
|
1620
|
+
res.json(cached);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const stale = cache.getStale(cacheKey);
|
|
1624
|
+
if (stale) {
|
|
1625
|
+
refreshBlocksCache(agent, project, cacheKey);
|
|
1626
|
+
res.json(stale);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const data = fetchBlocksData(agent, project);
|
|
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 blocks data:", error);
|
|
1635
|
+
res.status(502).json({
|
|
1636
|
+
error: "Failed to fetch blocks data",
|
|
1637
|
+
hint: message
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function fetchBlocksData(agent, project) {
|
|
1642
|
+
if (agent === "openclaw") {
|
|
1643
|
+
return validateBlocks(getBlocksResponse2({ project: project || null }));
|
|
1644
|
+
} else if (agent === "opencode") {
|
|
1645
|
+
return validateBlocks(getBlocksResponse3({ project: project || null }));
|
|
1646
|
+
} else if (agent === "codex") {
|
|
1647
|
+
return validateBlocks(getBlocksResponse({ project: project || null }));
|
|
1648
|
+
} else {
|
|
1649
|
+
return validateBlocks(getBlocksResponse4(project || null));
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function refreshBlocksCache(agent, project, cacheKey) {
|
|
1653
|
+
Promise.resolve().then(() => {
|
|
1654
|
+
const data = fetchBlocksData(agent, project);
|
|
1655
|
+
cache.set(cacheKey, data);
|
|
1656
|
+
}).catch((err) => console.error("Background refresh failed (blocks):", err));
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/server/analyticsParser.ts
|
|
1660
|
+
var import_node_fs6 = require("node:fs");
|
|
1661
|
+
var import_node_path6 = require("node:path");
|
|
1662
|
+
var import_node_os6 = require("node:os");
|
|
1663
|
+
var TZ_OFFSETS5 = {
|
|
1664
|
+
"Asia/Shanghai": 8,
|
|
1665
|
+
"Asia/Tokyo": 9,
|
|
1666
|
+
"America/New_York": -5,
|
|
1667
|
+
"America/Los_Angeles": -8,
|
|
1668
|
+
"Europe/London": 0,
|
|
1669
|
+
"UTC": 0
|
|
1670
|
+
};
|
|
1671
|
+
function getDateKey5(ms, tz) {
|
|
1672
|
+
const offset = (TZ_OFFSETS5[tz] ?? 8) * 36e5;
|
|
1673
|
+
const d = new Date(ms + offset);
|
|
1674
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
1675
|
+
}
|
|
1676
|
+
function normalizeToolName(name) {
|
|
1677
|
+
const lower = name.toLowerCase();
|
|
1678
|
+
if (lower.startsWith("mcp__")) {
|
|
1679
|
+
const parts = name.split("__");
|
|
1680
|
+
const serverPart = parts.length >= 3 ? parts[2] : "mcp";
|
|
1681
|
+
return `MCP:${serverPart}`;
|
|
1682
|
+
}
|
|
1683
|
+
const mapping = {
|
|
1684
|
+
"exec": "Bash",
|
|
1685
|
+
"read": "Read",
|
|
1686
|
+
"edit": "Edit",
|
|
1687
|
+
"write": "Write"
|
|
1688
|
+
};
|
|
1689
|
+
return mapping[lower] || name;
|
|
1690
|
+
}
|
|
1691
|
+
function countLines(text) {
|
|
1692
|
+
if (!text) return 0;
|
|
1693
|
+
return text.split("\n").length;
|
|
1694
|
+
}
|
|
1695
|
+
var CLAUDE_PROJECTS_DIR2 = (0, import_node_path6.join)((0, import_node_os6.homedir)(), ".claude", "projects");
|
|
1696
|
+
function extractProjectName3(dirName) {
|
|
1697
|
+
const parts = dirName.replace(/^-/, "").split("-");
|
|
1698
|
+
return parts[parts.length - 1] || dirName;
|
|
1699
|
+
}
|
|
1700
|
+
function matchesProject2(dirName, filter) {
|
|
1701
|
+
return extractProjectName3(dirName) === extractProjectName3(filter);
|
|
1702
|
+
}
|
|
1703
|
+
var claudeSessionCache = /* @__PURE__ */ new Map();
|
|
1704
|
+
function extractClaudeToolCalls(project) {
|
|
1705
|
+
if (!(0, import_node_fs6.existsSync)(CLAUDE_PROJECTS_DIR2)) return [];
|
|
1706
|
+
const results = [];
|
|
1707
|
+
const projectDirs = (0, import_node_fs6.readdirSync)(CLAUDE_PROJECTS_DIR2, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1708
|
+
for (const dirName of projectDirs) {
|
|
1709
|
+
if (project && !matchesProject2(dirName, project)) continue;
|
|
1710
|
+
const dirPath = (0, import_node_path6.join)(CLAUDE_PROJECTS_DIR2, dirName);
|
|
1711
|
+
let files;
|
|
1712
|
+
try {
|
|
1713
|
+
files = (0, import_node_fs6.readdirSync)(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
1714
|
+
} catch {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
for (const file of files) {
|
|
1718
|
+
const filePath = (0, import_node_path6.join)(dirPath, file);
|
|
1719
|
+
let mtime = 0;
|
|
1720
|
+
try {
|
|
1721
|
+
mtime = (0, import_node_fs6.statSync)(filePath).mtimeMs;
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
const cached = claudeSessionCache.get(filePath);
|
|
1725
|
+
if (cached && cached.mtime === mtime) {
|
|
1726
|
+
results.push(...cached.toolCalls);
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
const toolCalls = [];
|
|
1730
|
+
let content;
|
|
1731
|
+
try {
|
|
1732
|
+
content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
|
|
1733
|
+
} catch {
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
for (const line of content.split("\n")) {
|
|
1737
|
+
const trimmed = line.trim();
|
|
1738
|
+
if (!trimmed) continue;
|
|
1739
|
+
let obj;
|
|
1740
|
+
try {
|
|
1741
|
+
obj = JSON.parse(trimmed);
|
|
1742
|
+
} catch {
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
if (obj.type !== "assistant" || !obj.message) continue;
|
|
1746
|
+
const msg = obj.message;
|
|
1747
|
+
const timestamp = new Date(obj.timestamp).getTime();
|
|
1748
|
+
const content_arr = msg.content;
|
|
1749
|
+
if (!content_arr) continue;
|
|
1750
|
+
for (const item of content_arr) {
|
|
1751
|
+
if (item.type !== "tool_use") continue;
|
|
1752
|
+
const toolName = normalizeToolName(item.name);
|
|
1753
|
+
const input = item.input || {};
|
|
1754
|
+
let linesAdded = 0;
|
|
1755
|
+
let linesDeleted = 0;
|
|
1756
|
+
const filePath2 = input.file_path || void 0;
|
|
1757
|
+
if (toolName === "Edit") {
|
|
1758
|
+
linesDeleted = countLines(input.old_string || "");
|
|
1759
|
+
linesAdded = countLines(input.new_string || "");
|
|
1760
|
+
} else if (toolName === "Write") {
|
|
1761
|
+
linesAdded = countLines(input.content || "");
|
|
1762
|
+
}
|
|
1763
|
+
toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
claudeSessionCache.set(filePath, { mtime, toolCalls });
|
|
1767
|
+
results.push(...toolCalls);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return results;
|
|
1771
|
+
}
|
|
1772
|
+
var openclawSessionCache = /* @__PURE__ */ new Map();
|
|
1773
|
+
function extractOpenClawToolCalls(project) {
|
|
1774
|
+
const results = [];
|
|
1775
|
+
const refs = scanOpenClawSessions();
|
|
1776
|
+
for (const ref of refs) {
|
|
1777
|
+
if (project && ref.agentId !== project) continue;
|
|
1778
|
+
let mtime = 0;
|
|
1779
|
+
try {
|
|
1780
|
+
mtime = (0, import_node_fs6.statSync)(ref.sessionFile).mtimeMs;
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
const cached = openclawSessionCache.get(ref.sessionFile);
|
|
1784
|
+
if (cached && cached.mtime === mtime) {
|
|
1785
|
+
results.push(...cached.toolCalls);
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
const toolCalls = [];
|
|
1789
|
+
let content;
|
|
1790
|
+
try {
|
|
1791
|
+
content = (0, import_node_fs6.readFileSync)(ref.sessionFile, "utf-8");
|
|
1792
|
+
} catch {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
for (const line of content.split("\n")) {
|
|
1796
|
+
const trimmed = line.trim();
|
|
1797
|
+
if (!trimmed) continue;
|
|
1798
|
+
let obj;
|
|
1799
|
+
try {
|
|
1800
|
+
obj = JSON.parse(trimmed);
|
|
1801
|
+
} catch {
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
if (obj.type !== "message") continue;
|
|
1805
|
+
const msg = obj.message;
|
|
1806
|
+
if (msg.role !== "assistant") continue;
|
|
1807
|
+
const timestamp = Number(msg.timestamp ?? 0);
|
|
1808
|
+
const content_arr = msg.content;
|
|
1809
|
+
if (!content_arr) continue;
|
|
1810
|
+
for (const item of content_arr) {
|
|
1811
|
+
if (item.type !== "toolCall") continue;
|
|
1812
|
+
const toolName = normalizeToolName(item.name);
|
|
1813
|
+
const args = item.arguments || {};
|
|
1814
|
+
let linesAdded = 0;
|
|
1815
|
+
let linesDeleted = 0;
|
|
1816
|
+
const filePath2 = args.path || void 0;
|
|
1817
|
+
if (toolName === "Edit") {
|
|
1818
|
+
linesDeleted = countLines(args.oldText || "");
|
|
1819
|
+
linesAdded = countLines(args.newText || "");
|
|
1820
|
+
} else if (toolName === "Write") {
|
|
1821
|
+
linesAdded = countLines(args.content || "");
|
|
1822
|
+
}
|
|
1823
|
+
toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
openclawSessionCache.set(ref.sessionFile, { mtime, toolCalls });
|
|
1827
|
+
results.push(...toolCalls);
|
|
1828
|
+
}
|
|
1829
|
+
return results;
|
|
1830
|
+
}
|
|
1831
|
+
function computeAnalytics(toolCalls, timezone = "Asia/Shanghai") {
|
|
1832
|
+
const changeMap = /* @__PURE__ */ new Map();
|
|
1833
|
+
for (const tc of toolCalls) {
|
|
1834
|
+
if (tc.linesAdded === 0 && tc.linesDeleted === 0) continue;
|
|
1835
|
+
const key = getDateKey5(tc.timestamp, timezone);
|
|
1836
|
+
if (!changeMap.has(key)) changeMap.set(key, { added: 0, deleted: 0, files: /* @__PURE__ */ new Set() });
|
|
1837
|
+
const entry = changeMap.get(key);
|
|
1838
|
+
entry.added += tc.linesAdded;
|
|
1839
|
+
entry.deleted += tc.linesDeleted;
|
|
1840
|
+
if (tc.filePath) entry.files.add(tc.filePath);
|
|
1841
|
+
}
|
|
1842
|
+
const codeChangeTrend = [];
|
|
1843
|
+
for (const [date, { added, deleted, files }] of changeMap) {
|
|
1844
|
+
codeChangeTrend.push({ date, linesAdded: added, linesDeleted: deleted, netChange: added - deleted, filesModified: files.size });
|
|
1845
|
+
}
|
|
1846
|
+
codeChangeTrend.sort((a, b) => a.date.localeCompare(b.date));
|
|
1847
|
+
const toolCountMap = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const tc of toolCalls) {
|
|
1849
|
+
toolCountMap.set(tc.toolName, (toolCountMap.get(tc.toolName) || 0) + 1);
|
|
1850
|
+
}
|
|
1851
|
+
const toolUsageDistribution = [...toolCountMap.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
|
|
1852
|
+
const editCalls = toolCalls.filter((tc) => tc.toolName === "Edit" || tc.toolName === "Write");
|
|
1853
|
+
const totalEdits = editCalls.length;
|
|
1854
|
+
const totalLinesChanged = editCalls.reduce((s, tc) => s + tc.linesAdded + tc.linesDeleted, 0);
|
|
1855
|
+
const totalLinesAdded = editCalls.reduce((s, tc) => s + tc.linesAdded, 0);
|
|
1856
|
+
const totalLinesDeleted = editCalls.reduce((s, tc) => s + tc.linesDeleted, 0);
|
|
1857
|
+
const uniqueFiles = new Set(editCalls.filter((tc) => tc.filePath).map((tc) => tc.filePath));
|
|
1858
|
+
const editDates = new Set(editCalls.map((tc) => getDateKey5(tc.timestamp, timezone)));
|
|
1859
|
+
const productivityKPIs = {
|
|
1860
|
+
avgLinesPerEdit: totalEdits > 0 ? Math.round(totalLinesChanged / totalEdits) : 0,
|
|
1861
|
+
filesModifiedPerDay: editDates.size > 0 ? Math.round(uniqueFiles.size / editDates.size) : 0,
|
|
1862
|
+
addDeleteRatio: totalLinesDeleted > 0 ? Math.round(totalLinesAdded / totalLinesDeleted * 100) / 100 : totalLinesAdded > 0 ? 1 : 0,
|
|
1863
|
+
totalEdits,
|
|
1864
|
+
totalFilesModified: uniqueFiles.size,
|
|
1865
|
+
activeDaysWithEdits: editDates.size
|
|
1866
|
+
};
|
|
1867
|
+
const trendMap = /* @__PURE__ */ new Map();
|
|
1868
|
+
for (const tc of toolCalls) {
|
|
1869
|
+
const date = getDateKey5(tc.timestamp, timezone);
|
|
1870
|
+
if (!trendMap.has(date)) trendMap.set(date, /* @__PURE__ */ new Map());
|
|
1871
|
+
const dayMap = trendMap.get(date);
|
|
1872
|
+
dayMap.set(tc.toolName, (dayMap.get(tc.toolName) || 0) + 1);
|
|
1873
|
+
}
|
|
1874
|
+
const toolCallTrend = [];
|
|
1875
|
+
for (const [date, dayMap] of trendMap) {
|
|
1876
|
+
const entry = { date };
|
|
1877
|
+
for (const [tool, count] of dayMap) {
|
|
1878
|
+
entry[tool] = count;
|
|
1879
|
+
}
|
|
1880
|
+
toolCallTrend.push(entry);
|
|
1881
|
+
}
|
|
1882
|
+
toolCallTrend.sort((a, b) => a.date.localeCompare(b.date));
|
|
1883
|
+
return { codeChangeTrend, toolUsageDistribution, productivityKPIs, toolCallTrend };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// src/server/routes/analytics.ts
|
|
1887
|
+
var EMPTY_ANALYTICS = {
|
|
1888
|
+
codeChangeTrend: [],
|
|
1889
|
+
toolUsageDistribution: [],
|
|
1890
|
+
productivityKPIs: { avgLinesPerEdit: 0, filesModifiedPerDay: 0, addDeleteRatio: 0, totalEdits: 0, totalFilesModified: 0, activeDaysWithEdits: 0 },
|
|
1891
|
+
toolCallTrend: []
|
|
1892
|
+
};
|
|
1893
|
+
async function getAnalytics(req, res) {
|
|
1894
|
+
const agent = req.query.agent || "claude";
|
|
1895
|
+
const project = req.query.project || void 0;
|
|
1896
|
+
if (agent === "codex" || agent === "opencode") {
|
|
1897
|
+
res.json(EMPTY_ANALYTICS);
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
try {
|
|
1901
|
+
const cacheKey = `analytics:${agent}:${project || "all"}`;
|
|
1902
|
+
const cached = cache.get(cacheKey);
|
|
1903
|
+
if (cached) {
|
|
1904
|
+
res.json(cached);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const toolCalls = agent === "openclaw" ? extractOpenClawToolCalls(project || null) : extractClaudeToolCalls(project || null);
|
|
1908
|
+
const data = computeAnalytics(toolCalls);
|
|
1909
|
+
const validated = validateAnalytics(data);
|
|
1910
|
+
cache.set(cacheKey, validated);
|
|
1911
|
+
res.json(validated);
|
|
1912
|
+
} catch (error) {
|
|
1913
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1914
|
+
console.error("Error fetching analytics:", error);
|
|
1915
|
+
res.status(502).json({
|
|
1916
|
+
error: `Failed to fetch analytics from ${agent}`,
|
|
1917
|
+
hint: message
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// src/server/agentDetection.ts
|
|
1923
|
+
var import_node_fs7 = require("node:fs");
|
|
1924
|
+
var import_node_path7 = require("node:path");
|
|
1925
|
+
var import_node_os7 = require("node:os");
|
|
1926
|
+
var CLAUDE_PROJECTS_DIR3 = (0, import_node_path7.join)((0, import_node_os7.homedir)(), ".claude", "projects");
|
|
1927
|
+
var CODEX_SESSIONS_DIR = (0, import_node_path7.join)((0, import_node_os7.homedir)(), ".codex", "sessions");
|
|
1928
|
+
function isClaudeCodeAvailable() {
|
|
1929
|
+
if (!(0, import_node_fs7.existsSync)(CLAUDE_PROJECTS_DIR3)) return false;
|
|
1930
|
+
try {
|
|
1931
|
+
const dirs = (0, import_node_fs7.readdirSync)(CLAUDE_PROJECTS_DIR3, { withFileTypes: true });
|
|
1932
|
+
return dirs.some((d) => d.isDirectory());
|
|
1933
|
+
} catch {
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
function isCodexAvailable() {
|
|
1938
|
+
return (0, import_node_fs7.existsSync)(CODEX_SESSIONS_DIR);
|
|
1939
|
+
}
|
|
1940
|
+
function isOpencodeAvailable() {
|
|
1941
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)((0, import_node_os7.homedir)(), ".local", "share", "opencode", "opencode.db"));
|
|
1942
|
+
}
|
|
1943
|
+
function detectAvailableAgents() {
|
|
1944
|
+
return {
|
|
1945
|
+
claude: isClaudeCodeAvailable(),
|
|
1946
|
+
codex: isCodexAvailable(),
|
|
1947
|
+
opencode: isOpencodeAvailable()
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/server/routes/api.ts
|
|
1952
|
+
function getAgents(_req, res) {
|
|
1953
|
+
try {
|
|
1954
|
+
const agents = detectAvailableAgents();
|
|
1955
|
+
const available = [];
|
|
1956
|
+
if (agents.claude) available.push("claude");
|
|
1957
|
+
if (agents.codex) available.push("codex");
|
|
1958
|
+
if (isOpenClawAccessible()) available.push("openclaw");
|
|
1959
|
+
if (isOpencodeAccessible()) available.push("opencode");
|
|
1960
|
+
res.json({ available, default: available[0] || null });
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1963
|
+
res.status(500).json({ error: "Failed to detect agents", hint: message });
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function registerApiRoutes(router) {
|
|
1967
|
+
router.get("/agents", getAgents);
|
|
1968
|
+
router.get("/daily", getDaily);
|
|
1969
|
+
router.get("/monthly", getMonthly);
|
|
1970
|
+
router.get("/session", getSession);
|
|
1971
|
+
router.get("/projects", getProjects);
|
|
1972
|
+
router.get("/blocks", getBlocks);
|
|
1973
|
+
router.get("/analytics", getAnalytics);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/server/index.ts
|
|
1977
|
+
var import_open = __toESM(require("open"), 1);
|
|
1978
|
+
var CLI_USAGE = [
|
|
1979
|
+
"Usage:",
|
|
1980
|
+
" tokendash",
|
|
1981
|
+
" tokendash --version",
|
|
1982
|
+
" tokendash --port <number> [--no-open]",
|
|
1983
|
+
" tokendash --tray [--port <number>]"
|
|
1984
|
+
].join("\n");
|
|
1985
|
+
function getPackageVersion() {
|
|
1986
|
+
const __filename = (0, import_node_url.fileURLToPath)(__esbuild_import_meta_url);
|
|
1987
|
+
const __dirname = (0, import_node_path8.dirname)(__filename);
|
|
1988
|
+
const packageJson = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path8.join)(__dirname, "..", "..", "package.json"), "utf8"));
|
|
1989
|
+
return packageJson.version ?? "unknown";
|
|
1990
|
+
}
|
|
1991
|
+
function exitWithCliError(message) {
|
|
1992
|
+
console.error(message);
|
|
1993
|
+
console.error(`
|
|
1994
|
+
${CLI_USAGE}`);
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
}
|
|
1997
|
+
function parseCliArgs() {
|
|
1998
|
+
const args = process.argv.slice(2);
|
|
1999
|
+
const result = {};
|
|
2000
|
+
if (args.length === 1 && (args[0] === "--version" || args[0] === "-v")) {
|
|
2001
|
+
result.showVersion = true;
|
|
2002
|
+
return result;
|
|
2003
|
+
}
|
|
2004
|
+
for (let i = 0; i < args.length; i++) {
|
|
2005
|
+
const arg = args[i];
|
|
2006
|
+
if (arg === "--version" || arg === "-v") {
|
|
2007
|
+
exitWithCliError("The --version flag must be used by itself.");
|
|
2008
|
+
}
|
|
2009
|
+
if (arg === "--port") {
|
|
2010
|
+
if (i + 1 >= args.length) {
|
|
2011
|
+
exitWithCliError("Missing value for --port.");
|
|
2012
|
+
}
|
|
2013
|
+
const value = parseInt(args[i + 1], 10);
|
|
2014
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2015
|
+
exitWithCliError(`Invalid port value: ${args[i + 1]}`);
|
|
2016
|
+
}
|
|
2017
|
+
result.port = value;
|
|
2018
|
+
i++;
|
|
2019
|
+
} else if (arg === "--no-open") {
|
|
2020
|
+
result.noOpen = true;
|
|
2021
|
+
} else if (arg === "--tray") {
|
|
2022
|
+
result.tray = true;
|
|
2023
|
+
} else {
|
|
2024
|
+
exitWithCliError(`Unsupported argument: ${arg}`);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return result;
|
|
2028
|
+
}
|
|
2029
|
+
async function ensureUsageSupportAvailable() {
|
|
2030
|
+
try {
|
|
2031
|
+
const agents = detectAvailableAgents();
|
|
2032
|
+
if (!agents.claude && !agents.codex) {
|
|
2033
|
+
console.error("Error: No AI coding assistant data found.");
|
|
2034
|
+
console.error("\nDetails: Could not find Claude Code (~/.claude/projects/) or Codex (~/.codex/sessions/) data.");
|
|
2035
|
+
console.error("Please install at least one of: Claude Code or Codex CLI.");
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
if (agents.claude) console.log(" \u2713 Claude Code detected");
|
|
2039
|
+
if (agents.codex) console.log(" \u2713 Codex detected");
|
|
2040
|
+
return true;
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
console.error("Error: failed to detect available AI coding assistants");
|
|
2043
|
+
console.error("\nDetails:", error instanceof Error ? error.message : error);
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
function resolvePort(value) {
|
|
2048
|
+
return Number.isInteger(value) && value && value > 0 ? value : 3456;
|
|
2049
|
+
}
|
|
2050
|
+
function listen(app, port) {
|
|
2051
|
+
return new Promise((resolve, reject) => {
|
|
2052
|
+
const server = app.listen(port);
|
|
2053
|
+
const handleListening = () => {
|
|
2054
|
+
cleanup();
|
|
2055
|
+
resolve(server);
|
|
2056
|
+
};
|
|
2057
|
+
const handleError = (error) => {
|
|
2058
|
+
cleanup();
|
|
2059
|
+
reject(error);
|
|
2060
|
+
};
|
|
2061
|
+
const cleanup = () => {
|
|
2062
|
+
server.off("listening", handleListening);
|
|
2063
|
+
server.off("error", handleError);
|
|
2064
|
+
};
|
|
2065
|
+
server.once("listening", handleListening);
|
|
2066
|
+
server.once("error", handleError);
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
async function listenWithPortFallback(app, preferredPort) {
|
|
2070
|
+
let port = preferredPort;
|
|
2071
|
+
for (let attempt = 0; attempt < 20; attempt++, port++) {
|
|
2072
|
+
try {
|
|
2073
|
+
const server = await listen(app, port);
|
|
2074
|
+
return { server, port, usedFallback: port !== preferredPort };
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
const err = error;
|
|
2077
|
+
if (err.code !== "EADDRINUSE") {
|
|
2078
|
+
throw error;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
throw new Error(`Could not find an available port starting from ${preferredPort}`);
|
|
2083
|
+
}
|
|
2084
|
+
function createApp(_port, baseDir) {
|
|
2085
|
+
const app = (0, import_express.default)();
|
|
2086
|
+
const router = import_express.default.Router();
|
|
2087
|
+
registerApiRoutes(router);
|
|
2088
|
+
app.use("/api", router);
|
|
2089
|
+
const _baseDir = baseDir ?? (0, import_node_path8.dirname)((0, import_node_url.fileURLToPath)(__esbuild_import_meta_url));
|
|
2090
|
+
const isProduction = baseDir ? true : __esbuild_import_meta_url.includes("dist/");
|
|
2091
|
+
const popoverPath = isProduction ? (0, import_node_path8.join)(_baseDir, "client", "popover.html") : (0, import_node_path8.join)(_baseDir, "..", "..", "public", "popover.html");
|
|
2092
|
+
app.get("/popover.html", (_req, res) => {
|
|
2093
|
+
res.sendFile(popoverPath);
|
|
2094
|
+
});
|
|
2095
|
+
if (isProduction) {
|
|
2096
|
+
const clientPath = (0, import_node_path8.join)(_baseDir, "client");
|
|
2097
|
+
const clientIndexPath = (0, import_node_path8.join)(clientPath, "index.html");
|
|
2098
|
+
app.use(import_express.default.static(clientPath));
|
|
2099
|
+
app.use("{*path}", (_req, res) => {
|
|
2100
|
+
res.sendFile(clientIndexPath);
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
return app;
|
|
2104
|
+
}
|
|
2105
|
+
async function main() {
|
|
2106
|
+
const args = parseCliArgs();
|
|
2107
|
+
if (args.showVersion) {
|
|
2108
|
+
console.log(getPackageVersion());
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
const version = getPackageVersion();
|
|
2112
|
+
const preferredPort = resolvePort(args.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0));
|
|
2113
|
+
if (args.tray) {
|
|
2114
|
+
if (process.platform !== "darwin") {
|
|
2115
|
+
console.error("Error: --tray is only supported on macOS.");
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
}
|
|
2118
|
+
console.log(`Starting tokendash v${version} in tray mode...`);
|
|
2119
|
+
const { default: electronPath } = await import("electron");
|
|
2120
|
+
const { spawn } = await import("node:child_process");
|
|
2121
|
+
const child = spawn(electronPath, ["."], {
|
|
2122
|
+
env: {
|
|
2123
|
+
...process.env,
|
|
2124
|
+
TOKENDASH_PORT: String(preferredPort),
|
|
2125
|
+
TOKENDASH_TRAY: "1"
|
|
2126
|
+
},
|
|
2127
|
+
stdio: "inherit"
|
|
2128
|
+
});
|
|
2129
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
2130
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const shouldOpenBrowser = !args.noOpen;
|
|
2134
|
+
console.log(`Starting tokendash v${version}...`);
|
|
2135
|
+
console.log(`Checking local usage data sources...`);
|
|
2136
|
+
const isUsageSupportAvailable = await ensureUsageSupportAvailable();
|
|
2137
|
+
if (!isUsageSupportAvailable) {
|
|
2138
|
+
process.exit(1);
|
|
2139
|
+
}
|
|
2140
|
+
const app = createApp(preferredPort);
|
|
2141
|
+
const { server, port, usedFallback } = await listenWithPortFallback(app, preferredPort);
|
|
2142
|
+
if (usedFallback) {
|
|
2143
|
+
console.warn(`tokendash detected that port ${preferredPort} is already in use, switched to http://localhost:${port}`);
|
|
2144
|
+
}
|
|
2145
|
+
console.log(`tokendash running on http://localhost:${port}`);
|
|
2146
|
+
console.log(`API available at http://localhost:${port}/api`);
|
|
2147
|
+
const isProduction = __esbuild_import_meta_url.includes("dist/");
|
|
2148
|
+
if (isProduction) {
|
|
2149
|
+
console.log("Serving production build");
|
|
2150
|
+
} else {
|
|
2151
|
+
console.log('Development mode - use "npm run dev" for full dev experience');
|
|
2152
|
+
}
|
|
2153
|
+
if (shouldOpenBrowser) {
|
|
2154
|
+
setTimeout(() => {
|
|
2155
|
+
console.log("Opening dashboard in your browser...");
|
|
2156
|
+
(0, import_open.default)(`http://localhost:${port}`).catch((err) => {
|
|
2157
|
+
console.warn("Could not open browser:", err.message);
|
|
2158
|
+
});
|
|
2159
|
+
}, 100);
|
|
2160
|
+
} else {
|
|
2161
|
+
console.log("Browser auto-open disabled (--no-open)");
|
|
2162
|
+
}
|
|
2163
|
+
process.on("SIGTERM", () => {
|
|
2164
|
+
server.close(() => {
|
|
2165
|
+
console.log("Server closed");
|
|
2166
|
+
process.exit(0);
|
|
2167
|
+
});
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2171
|
+
0 && (module.exports = {
|
|
2172
|
+
createApp,
|
|
2173
|
+
main
|
|
2174
|
+
});
|
|
2175
|
+
//# sourceMappingURL=electron-server.cjs.map
|