@juliantanx/aiusage 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -27
- package/dist/index.js +1015 -123
- package/dist/web/_app/immutable/assets/10.DpWloLoX.css +1 -0
- package/dist/web/_app/immutable/assets/11.BfaLSE4e.css +1 -0
- package/dist/web/_app/immutable/assets/14.CxRLR3o7.css +1 -0
- package/dist/web/_app/immutable/assets/2.BLunjqo3.css +1 -0
- package/dist/web/_app/immutable/assets/6.8Efnpu8u.css +1 -0
- package/dist/web/_app/immutable/assets/8.BeGOLIpG.css +1 -0
- package/dist/web/_app/immutable/assets/9.DMukHCtv.css +1 -0
- package/dist/web/_app/immutable/assets/_page.8Efnpu8u.css +1 -0
- package/dist/web/_app/immutable/assets/_page.BeGOLIpG.css +1 -0
- package/dist/web/_app/immutable/assets/_page.BfaLSE4e.css +1 -0
- package/dist/web/_app/immutable/assets/_page.CYETnzhl.css +1 -0
- package/dist/web/_app/immutable/assets/_page.CxRLR3o7.css +1 -0
- package/dist/web/_app/immutable/assets/_page.DDYvQrAf.css +1 -0
- package/dist/web/_app/immutable/assets/_page.DpWloLoX.css +1 -0
- package/dist/web/_app/immutable/chunks/{ToolSelector.CnQ-eszE.js → ToolSelector.Dhbay8qR.js} +2 -2
- package/dist/web/_app/immutable/chunks/api.Wak6H2JS.js +1 -0
- package/dist/web/_app/immutable/chunks/entry.CS-NDdiQ.js +3 -0
- package/dist/web/_app/immutable/chunks/i18n.BCaZpNqk.js +1 -0
- package/dist/web/_app/immutable/chunks/{index.wix8pgil.js → index.BmCbw0jM.js} +1 -1
- package/dist/web/_app/immutable/chunks/{stores.C3ZtRBEM.js → stores.BjyZzXAW.js} +1 -1
- package/dist/web/_app/immutable/entry/app.27f4j9Xy.js +2 -0
- package/dist/web/_app/immutable/entry/start.bzCZowcz.js +1 -0
- package/dist/web/_app/immutable/nodes/0.CFOrMeCV.js +1 -0
- package/dist/web/_app/immutable/nodes/{1.CNbcoUIj.js → 1.DZIXtjp3.js} +1 -1
- package/dist/web/_app/immutable/nodes/10.DxJi0s_F.js +1 -0
- package/dist/web/_app/immutable/nodes/11.q1SVEyD9.js +1 -0
- package/dist/web/_app/immutable/nodes/12.CamXVWac.js +1 -0
- package/dist/web/_app/immutable/nodes/{11.TrX-RGQX.js → 13.Bbzem3lL.js} +1 -1
- package/dist/web/_app/immutable/nodes/14.BbcTrrp0.js +1 -0
- package/dist/web/_app/immutable/nodes/2.DcYQmIYj.js +3 -0
- package/dist/web/_app/immutable/nodes/{3.CcnPu3DZ.js → 3.DqJoSOZA.js} +1 -1
- package/dist/web/_app/immutable/nodes/4.CoqRShfZ.js +41 -0
- package/dist/web/_app/immutable/nodes/{5.Bxx7TNGK.js → 5.BcRc8qNP.js} +1 -1
- package/dist/web/_app/immutable/nodes/6.DU7MEeuP.js +1 -0
- package/dist/web/_app/immutable/nodes/{7.bOnMUP4_.js → 7.CE7kQC3R.js} +1 -1
- package/dist/web/_app/immutable/nodes/8.DzIcKP7s.js +1 -0
- package/dist/web/_app/immutable/nodes/9.4mP0VDWh.js +1 -0
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +8 -3
- package/dist/web/_app/immutable/assets/12.DAmfvZ5J.css +0 -1
- package/dist/web/_app/immutable/assets/2.G6nSchsK.css +0 -1
- package/dist/web/_app/immutable/assets/6.CrsKlz6Y.css +0 -1
- package/dist/web/_app/immutable/assets/8.DDodt1Wn.css +0 -1
- package/dist/web/_app/immutable/assets/9.ApFevNUr.css +0 -1
- package/dist/web/_app/immutable/assets/_page.ApFevNUr.css +0 -1
- package/dist/web/_app/immutable/assets/_page.CrsKlz6Y.css +0 -1
- package/dist/web/_app/immutable/assets/_page.DAmfvZ5J.css +0 -1
- package/dist/web/_app/immutable/assets/_page.DDodt1Wn.css +0 -1
- package/dist/web/_app/immutable/assets/_page.DewAUGiJ.css +0 -1
- package/dist/web/_app/immutable/chunks/api.OwJBmtEN.js +0 -1
- package/dist/web/_app/immutable/chunks/entry.DWbp2XzC.js +0 -3
- package/dist/web/_app/immutable/chunks/i18n.BOq44pYI.js +0 -1
- package/dist/web/_app/immutable/entry/app.7Tmcb_qs.js +0 -2
- package/dist/web/_app/immutable/entry/start.CuMG7ijt.js +0 -1
- package/dist/web/_app/immutable/nodes/0.GiWuXD22.js +0 -1
- package/dist/web/_app/immutable/nodes/10.DgPr3L-S.js +0 -1
- package/dist/web/_app/immutable/nodes/12.ZW7n1TLF.js +0 -1
- package/dist/web/_app/immutable/nodes/2.D0gEY9nh.js +0 -3
- package/dist/web/_app/immutable/nodes/4.CCaCZRNi.js +0 -13
- package/dist/web/_app/immutable/nodes/6.8zY9a_YX.js +0 -1
- package/dist/web/_app/immutable/nodes/8.01X9rJRT.js +0 -1
- package/dist/web/_app/immutable/nodes/9.DabFUxt3.js +0 -1
- /package/dist/web/_app/immutable/assets/{10.D4XIqeEN.css → 12.D4XIqeEN.css} +0 -0
- /package/dist/web/_app/immutable/assets/{11.CR85TxeS.css → 13.CR85TxeS.css} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { writeFileSync as
|
|
5
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/commands/serve.ts
|
|
8
8
|
import http2 from "http";
|
|
9
|
-
import { readFileSync as
|
|
10
|
-
import { join as
|
|
9
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6, statSync as statSync2, writeFileSync as writeFileSync4, unlinkSync } from "fs";
|
|
10
|
+
import { join as join7, extname as extname2, dirname } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
13
13
|
// src/api/server.ts
|
|
14
14
|
import http from "http";
|
|
15
|
-
import
|
|
16
|
-
import { hostname as hostname2, platform as platform2 } from "os";
|
|
15
|
+
import { hostname as hostname2, platform as platform3 } from "os";
|
|
17
16
|
|
|
18
17
|
// ../core/dist/index.js
|
|
19
18
|
import { createHash } from "crypto";
|
|
@@ -339,16 +338,24 @@ var ClaudeCodeParser = class {
|
|
|
339
338
|
let callIndex = 0;
|
|
340
339
|
for (const block of parsed.message.content) {
|
|
341
340
|
if (block.type === "tool_use") {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
341
|
+
let storedName;
|
|
342
|
+
if (block.name === "Skill") {
|
|
343
|
+
const raw = block.input?.skill ?? block.input?.skillName ?? block.input?.name ?? "";
|
|
344
|
+
const skillArg = typeof raw === "string" ? raw.trim() : "";
|
|
345
|
+
storedName = skillArg ? `skill__${skillArg}` : "skill__unknown";
|
|
346
|
+
} else {
|
|
347
|
+
const rawName = block.name ?? "";
|
|
348
|
+
const cleanName = rawName.replace(/[=:"'{\[\s].*$/s, "").replace(/[^a-zA-Z0-9_-]/g, "");
|
|
349
|
+
if (!cleanName) {
|
|
350
|
+
callIndex++;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
storedName = cleanName;
|
|
347
354
|
}
|
|
348
355
|
toolCalls.push({
|
|
349
|
-
id: generateToolCallId(record.id,
|
|
356
|
+
id: generateToolCallId(record.id, storedName, ts, callIndex),
|
|
350
357
|
recordId: record.id,
|
|
351
|
-
name:
|
|
358
|
+
name: storedName,
|
|
352
359
|
ts,
|
|
353
360
|
callIndex
|
|
354
361
|
});
|
|
@@ -370,7 +377,7 @@ var CodexParser = class {
|
|
|
370
377
|
} catch {
|
|
371
378
|
return null;
|
|
372
379
|
}
|
|
373
|
-
const payload = parsed.event_msg?.payload ?? (parsed.type === "event_msg" ? parsed.payload : void 0);
|
|
380
|
+
const payload = parsed.event_msg?.payload ?? (parsed.type === "event_msg" || parsed.type === "response_item" ? parsed.payload : void 0);
|
|
374
381
|
const turnCtx = parsed.type === "turn_context" ? parsed.payload : void 0;
|
|
375
382
|
if (turnCtx?.model) {
|
|
376
383
|
this.currentModel = turnCtx.model;
|
|
@@ -383,7 +390,8 @@ var CodexParser = class {
|
|
|
383
390
|
if (payload.type === "function_call") {
|
|
384
391
|
const rawFcTs = parsed.event_msg?.timestamp ?? parsed.timestamp ?? context.now;
|
|
385
392
|
this.pendingToolCalls.push({
|
|
386
|
-
name: payload.function
|
|
393
|
+
// newer format: payload.name; older format: payload.function.name
|
|
394
|
+
name: payload.name ?? payload.function?.name ?? "unknown",
|
|
387
395
|
ts: typeof rawFcTs === "number" ? rawFcTs : new Date(rawFcTs).getTime()
|
|
388
396
|
});
|
|
389
397
|
return null;
|
|
@@ -464,6 +472,27 @@ var OpenClawParser = class {
|
|
|
464
472
|
} catch {
|
|
465
473
|
return null;
|
|
466
474
|
}
|
|
475
|
+
if (parsed.message?.role === "assistant" && Array.isArray(parsed.message.content) && !parsed.message?.usage) {
|
|
476
|
+
const rawTs2 = parsed.message.timestamp ?? parsed.timestamp ?? context.now;
|
|
477
|
+
const ts2 = typeof rawTs2 === "number" ? rawTs2 : new Date(rawTs2).getTime();
|
|
478
|
+
const toolCalls2 = [];
|
|
479
|
+
let callIndex = 0;
|
|
480
|
+
for (const block of parsed.message.content) {
|
|
481
|
+
if (block.type === "tool_use" || block.type === "toolCall") {
|
|
482
|
+
toolCalls2.push({
|
|
483
|
+
id: generateOrphanToolCallId(this.tool, block.name, ts2, callIndex),
|
|
484
|
+
recordId: null,
|
|
485
|
+
name: block.name,
|
|
486
|
+
ts: ts2,
|
|
487
|
+
callIndex
|
|
488
|
+
});
|
|
489
|
+
callIndex++;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (toolCalls2.length > 0) {
|
|
493
|
+
return { record: null, toolCalls: toolCalls2 };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
467
496
|
if (!parsed.message?.usage) return null;
|
|
468
497
|
const usage = parsed.message.usage;
|
|
469
498
|
const model = parsed.message.model ?? "unknown";
|
|
@@ -522,7 +551,7 @@ var OpenClawParser = class {
|
|
|
522
551
|
if (Array.isArray(parsed.message.content)) {
|
|
523
552
|
let callIndex = 0;
|
|
524
553
|
for (const block of parsed.message.content) {
|
|
525
|
-
if (block.type === "tool_use") {
|
|
554
|
+
if (block.type === "tool_use" || block.type === "toolCall") {
|
|
526
555
|
toolCalls.push({
|
|
527
556
|
id: generateToolCallId(record.id, block.name, ts, callIndex),
|
|
528
557
|
recordId: record.id,
|
|
@@ -633,8 +662,20 @@ var QoderParser = class {
|
|
|
633
662
|
return { record, toolCalls };
|
|
634
663
|
}
|
|
635
664
|
finalize() {
|
|
665
|
+
const toolCalls = [];
|
|
666
|
+
for (const pending of this.pendingToolCallsByLoopId.values()) {
|
|
667
|
+
for (const [callIndex, tc] of pending.entries()) {
|
|
668
|
+
toolCalls.push({
|
|
669
|
+
id: generateOrphanToolCallId(this.tool, tc.name, tc.ts, callIndex),
|
|
670
|
+
recordId: null,
|
|
671
|
+
name: tc.name,
|
|
672
|
+
ts: tc.ts,
|
|
673
|
+
callIndex
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
636
677
|
this.pendingToolCallsByLoopId.clear();
|
|
637
|
-
return [];
|
|
678
|
+
return toolCalls.length > 0 ? [{ record: null, toolCalls }] : [];
|
|
638
679
|
}
|
|
639
680
|
};
|
|
640
681
|
var Aggregator = class {
|
|
@@ -733,6 +774,28 @@ function loadCredential(key) {
|
|
|
733
774
|
|
|
734
775
|
// src/api/project-extraction.ts
|
|
735
776
|
import path from "path";
|
|
777
|
+
var CWD_WORKSPACE_ROOTS = /* @__PURE__ */ new Set([
|
|
778
|
+
"WebstormProjects",
|
|
779
|
+
"Documents",
|
|
780
|
+
"Projects",
|
|
781
|
+
"workspace",
|
|
782
|
+
"Workspace",
|
|
783
|
+
"dev",
|
|
784
|
+
"code",
|
|
785
|
+
"repos",
|
|
786
|
+
"Developer",
|
|
787
|
+
"src"
|
|
788
|
+
]);
|
|
789
|
+
function extractProjectFromCwd(cwd) {
|
|
790
|
+
if (!cwd) return "unknown";
|
|
791
|
+
const normalized = cwd.replace(/\\/g, "/");
|
|
792
|
+
const withoutHome = normalized.replace(/^[A-Za-z]:\/(?:Users|home)\/[^/]+\//, "").replace(/^\/(Users|home)\/[^/]+\//, "").replace(/^\/root\//, "");
|
|
793
|
+
const parts = withoutHome.split("/").filter(Boolean);
|
|
794
|
+
for (const part of parts) {
|
|
795
|
+
if (!CWD_WORKSPACE_ROOTS.has(part)) return part;
|
|
796
|
+
}
|
|
797
|
+
return parts[parts.length - 1] ?? "unknown";
|
|
798
|
+
}
|
|
736
799
|
var GENERIC_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
737
800
|
"sessions",
|
|
738
801
|
"session",
|
|
@@ -960,14 +1023,15 @@ function defaultFileData() {
|
|
|
960
1023
|
"openclaw": {},
|
|
961
1024
|
"opencode": {},
|
|
962
1025
|
"hermes": {},
|
|
963
|
-
"qoder": {}
|
|
1026
|
+
"qoder": {},
|
|
1027
|
+
"cursor": {}
|
|
964
1028
|
};
|
|
965
1029
|
}
|
|
966
1030
|
var WatermarkManager = class {
|
|
967
1031
|
data;
|
|
968
1032
|
path;
|
|
969
|
-
constructor(
|
|
970
|
-
this.path =
|
|
1033
|
+
constructor(path2) {
|
|
1034
|
+
this.path = path2;
|
|
971
1035
|
this.data = this.load();
|
|
972
1036
|
}
|
|
973
1037
|
load() {
|
|
@@ -980,7 +1044,7 @@ var WatermarkManager = class {
|
|
|
980
1044
|
if (parsed && typeof parsed === "object" && !("files" in parsed)) {
|
|
981
1045
|
return { files: { ...defaultFileData(), ...parsed } };
|
|
982
1046
|
}
|
|
983
|
-
return { files: { ...defaultFileData(), ...parsed.files ?? {} }, opencode: parsed.opencode ?? null, hermes: parsed.hermes ?? null, qoder: parsed.qoder ?? null };
|
|
1047
|
+
return { files: { ...defaultFileData(), ...parsed.files ?? {} }, opencode: parsed.opencode ?? null, hermes: parsed.hermes ?? null, qoder: parsed.qoder ?? null, cursor: parsed.cursor ?? null };
|
|
984
1048
|
} catch {
|
|
985
1049
|
return { files: defaultFileData() };
|
|
986
1050
|
}
|
|
@@ -1025,11 +1089,17 @@ var WatermarkManager = class {
|
|
|
1025
1089
|
setQoderCursor(cursor) {
|
|
1026
1090
|
this.data.qoder = cursor;
|
|
1027
1091
|
}
|
|
1092
|
+
getCursorCursor() {
|
|
1093
|
+
return this.data.cursor ?? null;
|
|
1094
|
+
}
|
|
1095
|
+
setCursorCursor(cursor) {
|
|
1096
|
+
this.data.cursor = cursor;
|
|
1097
|
+
}
|
|
1028
1098
|
};
|
|
1029
1099
|
|
|
1030
1100
|
// src/commands/parse-opencode.ts
|
|
1031
1101
|
function runParseOpenCode(db, options) {
|
|
1032
|
-
const { dbPath, device, deviceInstanceId, platform:
|
|
1102
|
+
const { dbPath, device, deviceInstanceId, platform: platform5, now, cursor, exchangeRate } = options;
|
|
1033
1103
|
const records = [];
|
|
1034
1104
|
const toolCalls = [];
|
|
1035
1105
|
const errors = [];
|
|
@@ -1099,7 +1169,7 @@ function runParseOpenCode(db, options) {
|
|
|
1099
1169
|
sourceFile: dbPath,
|
|
1100
1170
|
device,
|
|
1101
1171
|
deviceInstanceId,
|
|
1102
|
-
platform:
|
|
1172
|
+
platform: platform5
|
|
1103
1173
|
};
|
|
1104
1174
|
records.push(record);
|
|
1105
1175
|
const parts = partsStmt.all(message.id);
|
|
@@ -1129,7 +1199,7 @@ function runParseOpenCode(db, options) {
|
|
|
1129
1199
|
|
|
1130
1200
|
// src/commands/parse-hermes.ts
|
|
1131
1201
|
function runParseHermes(db, options) {
|
|
1132
|
-
const { dbPath, device, deviceInstanceId, platform:
|
|
1202
|
+
const { dbPath, device, deviceInstanceId, platform: platform5, now, cursor, exchangeRate } = options;
|
|
1133
1203
|
const records = [];
|
|
1134
1204
|
const toolCalls = [];
|
|
1135
1205
|
const errors = [];
|
|
@@ -1202,7 +1272,7 @@ function runParseHermes(db, options) {
|
|
|
1202
1272
|
sourceFile: dbPath,
|
|
1203
1273
|
device,
|
|
1204
1274
|
deviceInstanceId,
|
|
1205
|
-
platform:
|
|
1275
|
+
platform: platform5
|
|
1206
1276
|
};
|
|
1207
1277
|
records.push(record);
|
|
1208
1278
|
const messages = toolCallsStmt.all(session.id);
|
|
@@ -1250,7 +1320,7 @@ function parseModelKey(recordExtra) {
|
|
|
1250
1320
|
}
|
|
1251
1321
|
}
|
|
1252
1322
|
function runParseQoder(db, options) {
|
|
1253
|
-
const { dbPath, device, deviceInstanceId, platform:
|
|
1323
|
+
const { dbPath, device, deviceInstanceId, platform: platform5, now, cursor, exchangeRate } = options;
|
|
1254
1324
|
const records = [];
|
|
1255
1325
|
const toolCalls = [];
|
|
1256
1326
|
const errors = [];
|
|
@@ -1310,12 +1380,104 @@ function runParseQoder(db, options) {
|
|
|
1310
1380
|
sourceFile: dbPath,
|
|
1311
1381
|
device,
|
|
1312
1382
|
deviceInstanceId,
|
|
1313
|
-
platform:
|
|
1383
|
+
platform: platform5
|
|
1314
1384
|
});
|
|
1315
1385
|
}
|
|
1316
1386
|
return { records, toolCalls, nextCursor: lastCursor, errors };
|
|
1317
1387
|
}
|
|
1318
1388
|
|
|
1389
|
+
// src/commands/parse-cursor.ts
|
|
1390
|
+
function runParseCursor(db, options) {
|
|
1391
|
+
const { dbPath, device, deviceInstanceId, platform: platform5, now, cursor } = options;
|
|
1392
|
+
const records = [];
|
|
1393
|
+
const toolCalls = [];
|
|
1394
|
+
const errors = [];
|
|
1395
|
+
let lastCursor = null;
|
|
1396
|
+
const composerRows = db.prepare(`SELECT key, CAST(value AS TEXT) as value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND length(value) > 100`).all();
|
|
1397
|
+
const composers = [];
|
|
1398
|
+
for (const row of composerRows) {
|
|
1399
|
+
try {
|
|
1400
|
+
const j = JSON.parse(row.value);
|
|
1401
|
+
const composerId = row.key.slice("composerData:".length);
|
|
1402
|
+
const createdAt = j.createdAt ?? 0;
|
|
1403
|
+
if (!createdAt) continue;
|
|
1404
|
+
composers.push({
|
|
1405
|
+
composerId,
|
|
1406
|
+
createdAt,
|
|
1407
|
+
usageData: j.usageData
|
|
1408
|
+
});
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
composers.sort(
|
|
1413
|
+
(a, b) => a.createdAt !== b.createdAt ? a.createdAt - b.createdAt : a.composerId.localeCompare(b.composerId)
|
|
1414
|
+
);
|
|
1415
|
+
const filtered = cursor ? composers.filter(
|
|
1416
|
+
(c) => c.createdAt > cursor.lastCreatedAt || c.createdAt === cursor.lastCreatedAt && c.composerId > cursor.lastId
|
|
1417
|
+
) : composers;
|
|
1418
|
+
if (filtered.length === 0) {
|
|
1419
|
+
return { records, toolCalls, nextCursor: lastCursor, errors };
|
|
1420
|
+
}
|
|
1421
|
+
const bubbleStmt = db.prepare(
|
|
1422
|
+
`SELECT CAST(value AS TEXT) as value FROM cursorDiskKV WHERE key LIKE ?`
|
|
1423
|
+
);
|
|
1424
|
+
for (const composer of filtered) {
|
|
1425
|
+
lastCursor = { lastCreatedAt: composer.createdAt, lastId: composer.composerId };
|
|
1426
|
+
try {
|
|
1427
|
+
const bubbleRows = bubbleStmt.all(`bubbleId:${composer.composerId}:%`);
|
|
1428
|
+
let totalInputTokens = 0;
|
|
1429
|
+
let totalOutputTokens = 0;
|
|
1430
|
+
for (const row of bubbleRows) {
|
|
1431
|
+
try {
|
|
1432
|
+
const bubble = JSON.parse(row.value);
|
|
1433
|
+
if (bubble.type !== 2) continue;
|
|
1434
|
+
const tc = bubble.tokenCount;
|
|
1435
|
+
if (!tc) continue;
|
|
1436
|
+
totalInputTokens += tc.inputTokens ?? 0;
|
|
1437
|
+
totalOutputTokens += tc.outputTokens ?? 0;
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (totalInputTokens === 0 && totalOutputTokens === 0) continue;
|
|
1442
|
+
const ts = composer.createdAt;
|
|
1443
|
+
let cost = 0;
|
|
1444
|
+
let costSource = "unknown";
|
|
1445
|
+
const defaultUsage = composer.usageData?.default;
|
|
1446
|
+
if (defaultUsage?.costInCents != null && defaultUsage.costInCents > 0) {
|
|
1447
|
+
cost = defaultUsage.costInCents / 100;
|
|
1448
|
+
costSource = "log";
|
|
1449
|
+
}
|
|
1450
|
+
const recordId = generateRecordId(deviceInstanceId, `${dbPath}:${composer.composerId}`, ts);
|
|
1451
|
+
const record = {
|
|
1452
|
+
id: recordId,
|
|
1453
|
+
ts,
|
|
1454
|
+
ingestedAt: now,
|
|
1455
|
+
updatedAt: now,
|
|
1456
|
+
lineOffset: 0,
|
|
1457
|
+
tool: "cursor",
|
|
1458
|
+
model: "cursor-composer",
|
|
1459
|
+
provider: "cursor",
|
|
1460
|
+
inputTokens: totalInputTokens,
|
|
1461
|
+
outputTokens: totalOutputTokens,
|
|
1462
|
+
cacheReadTokens: 0,
|
|
1463
|
+
cacheWriteTokens: 0,
|
|
1464
|
+
thinkingTokens: 0,
|
|
1465
|
+
cost,
|
|
1466
|
+
costSource,
|
|
1467
|
+
sessionId: composer.composerId,
|
|
1468
|
+
sourceFile: dbPath,
|
|
1469
|
+
device,
|
|
1470
|
+
deviceInstanceId,
|
|
1471
|
+
platform: platform5
|
|
1472
|
+
};
|
|
1473
|
+
records.push(record);
|
|
1474
|
+
} catch (e) {
|
|
1475
|
+
errors.push(`composerId ${composer.composerId}: ${e instanceof Error ? e.message : e}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return { records, toolCalls, nextCursor: lastCursor, errors };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1319
1481
|
// src/commands/parse.ts
|
|
1320
1482
|
function unique(paths) {
|
|
1321
1483
|
return [...new Set(paths)];
|
|
@@ -1426,6 +1588,19 @@ function defaultQoderDbPath() {
|
|
|
1426
1588
|
const xdgDataHome = process.env.XDG_DATA_HOME ?? join3(home, ".local", "share");
|
|
1427
1589
|
return join3(xdgDataHome, "Qoder", "SharedClientCache", "cache", "db", "local.db");
|
|
1428
1590
|
}
|
|
1591
|
+
function defaultCursorDbPath() {
|
|
1592
|
+
const home = homedir2();
|
|
1593
|
+
const plat = platform();
|
|
1594
|
+
if (plat === "win32") {
|
|
1595
|
+
const appData = process.env.APPDATA ?? join3(home, "AppData", "Roaming");
|
|
1596
|
+
return join3(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
1597
|
+
}
|
|
1598
|
+
if (plat === "darwin") {
|
|
1599
|
+
return join3(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
1600
|
+
}
|
|
1601
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME ?? join3(home, ".config");
|
|
1602
|
+
return join3(xdgConfigHome, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
1603
|
+
}
|
|
1429
1604
|
function discoverLogFiles(sources) {
|
|
1430
1605
|
const home = homedir2();
|
|
1431
1606
|
const results = [];
|
|
@@ -1546,6 +1721,16 @@ async function runParse(db, filterTool, options) {
|
|
|
1546
1721
|
continue;
|
|
1547
1722
|
}
|
|
1548
1723
|
if (byteOffset < offset) {
|
|
1724
|
+
aggregator.parseLine(line, aggregator.createContext({
|
|
1725
|
+
tool,
|
|
1726
|
+
sourceFile: filePath,
|
|
1727
|
+
lineOffset: byteOffset,
|
|
1728
|
+
sessionId,
|
|
1729
|
+
device,
|
|
1730
|
+
deviceInstanceId,
|
|
1731
|
+
platform: devicePlatform,
|
|
1732
|
+
exchangeRate
|
|
1733
|
+
}));
|
|
1549
1734
|
byteOffset += Buffer.byteLength(line, "utf-8") + 1;
|
|
1550
1735
|
continue;
|
|
1551
1736
|
}
|
|
@@ -1687,6 +1872,36 @@ async function runParse(db, filterTool, options) {
|
|
|
1687
1872
|
errors.push(`${qoderDbPath}: ${e instanceof Error ? e.message : e}`);
|
|
1688
1873
|
}
|
|
1689
1874
|
}
|
|
1875
|
+
const cursorDbPath = options?.cursorDbPath ?? config?.sources?.["cursor"] ?? defaultCursorDbPath();
|
|
1876
|
+
if ((!filterTool || filterTool === "cursor") && existsSync4(cursorDbPath)) {
|
|
1877
|
+
try {
|
|
1878
|
+
const cursorDb = new Database(cursorDbPath, { readonly: true });
|
|
1879
|
+
try {
|
|
1880
|
+
const result = runParseCursor(cursorDb, {
|
|
1881
|
+
dbPath: cursorDbPath,
|
|
1882
|
+
device,
|
|
1883
|
+
deviceInstanceId,
|
|
1884
|
+
platform: devicePlatform,
|
|
1885
|
+
now: Date.now(),
|
|
1886
|
+
cursor: wm.getCursorCursor()
|
|
1887
|
+
});
|
|
1888
|
+
for (const record of result.records) insertRecord(db, record);
|
|
1889
|
+
for (const tc of result.toolCalls) insertToolCall(db, tc);
|
|
1890
|
+
if (result.nextCursor) {
|
|
1891
|
+
wm.setCursorCursor(result.nextCursor);
|
|
1892
|
+
wm.save();
|
|
1893
|
+
}
|
|
1894
|
+
parsedCount += result.records.length;
|
|
1895
|
+
toolCallCount += result.toolCalls.length;
|
|
1896
|
+
errors.push(...result.errors);
|
|
1897
|
+
onProgress({ phase: "Parsing SQLite", tool: "cursor", current: 1, total: 1, records: parsedCount, toolCalls: toolCallCount });
|
|
1898
|
+
} finally {
|
|
1899
|
+
cursorDb.close();
|
|
1900
|
+
}
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
errors.push(`${cursorDbPath}: ${e instanceof Error ? e.message : e}`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1690
1905
|
if (deviceInstanceId !== "unknown") {
|
|
1691
1906
|
db.prepare(
|
|
1692
1907
|
`UPDATE records SET device_instance_id = ?, device = ? WHERE device_instance_id = 'unknown'`
|
|
@@ -1714,8 +1929,180 @@ async function runParse(db, filterTool, options) {
|
|
|
1714
1929
|
} catch {
|
|
1715
1930
|
}
|
|
1716
1931
|
}
|
|
1932
|
+
backfillSkillNames(db);
|
|
1933
|
+
backfillCodexModels(db);
|
|
1934
|
+
backfillMissingToolCalls(db, exchangeRate);
|
|
1717
1935
|
return { parsedCount, toolCallCount, errors };
|
|
1718
1936
|
}
|
|
1937
|
+
function backfillSkillNames(db) {
|
|
1938
|
+
const rows = db.prepare(`
|
|
1939
|
+
SELECT tc.id, tc.record_id, tc.ts, tc.call_index,
|
|
1940
|
+
r.source_file, r.line_offset
|
|
1941
|
+
FROM tool_calls tc
|
|
1942
|
+
JOIN records r ON r.id = tc.record_id
|
|
1943
|
+
WHERE (tc.name = 'Skill' OR tc.name = 'skill__unknown')
|
|
1944
|
+
AND r.source_file NOT LIKE 'synced/%'
|
|
1945
|
+
`).all();
|
|
1946
|
+
if (rows.length === 0) return;
|
|
1947
|
+
const updateStmt = db.prepare("UPDATE tool_calls SET id = ?, name = ? WHERE id = ?");
|
|
1948
|
+
for (const row of rows) {
|
|
1949
|
+
try {
|
|
1950
|
+
const fd = openSync(row.source_file, "r");
|
|
1951
|
+
const buf = Buffer.alloc(65536);
|
|
1952
|
+
const n = readSync(fd, buf, 0, buf.length, row.line_offset);
|
|
1953
|
+
closeSync(fd);
|
|
1954
|
+
let lineEnd = 0;
|
|
1955
|
+
while (lineEnd < n && buf[lineEnd] !== 10) lineEnd++;
|
|
1956
|
+
const line = buf.subarray(0, lineEnd).toString("utf8");
|
|
1957
|
+
const parsed = JSON.parse(line);
|
|
1958
|
+
if (!Array.isArray(parsed.message?.content)) continue;
|
|
1959
|
+
let callIndex = 0;
|
|
1960
|
+
let skillArg = "";
|
|
1961
|
+
for (const block of parsed.message.content) {
|
|
1962
|
+
if (block.type !== "tool_use") continue;
|
|
1963
|
+
if (callIndex === row.call_index) {
|
|
1964
|
+
if (block.name === "Skill") {
|
|
1965
|
+
const raw = block.input?.skill ?? block.input?.skillName ?? block.input?.name ?? "";
|
|
1966
|
+
skillArg = typeof raw === "string" ? raw.trim() : "";
|
|
1967
|
+
}
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1970
|
+
callIndex++;
|
|
1971
|
+
}
|
|
1972
|
+
const storedName = skillArg ? `skill__${skillArg}` : "skill__unknown";
|
|
1973
|
+
const newId = generateToolCallId(row.record_id, storedName, row.ts, row.call_index);
|
|
1974
|
+
updateStmt.run(newId, storedName, row.id);
|
|
1975
|
+
} catch {
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
function backfillCodexModels(db) {
|
|
1980
|
+
const rows = db.prepare(`
|
|
1981
|
+
SELECT id, source_file, line_offset
|
|
1982
|
+
FROM records
|
|
1983
|
+
WHERE tool = 'codex' AND model = 'unknown'
|
|
1984
|
+
AND source_file NOT LIKE 'synced/%'
|
|
1985
|
+
`).all();
|
|
1986
|
+
if (rows.length === 0) return;
|
|
1987
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1988
|
+
for (const row of rows) {
|
|
1989
|
+
const list = byFile.get(row.source_file) ?? [];
|
|
1990
|
+
list.push(row);
|
|
1991
|
+
byFile.set(row.source_file, list);
|
|
1992
|
+
}
|
|
1993
|
+
const updateStmt = db.prepare(
|
|
1994
|
+
`UPDATE records SET model = ?, provider = ?, cost = ?, cost_source = ?, updated_at = ? WHERE id = ?`
|
|
1995
|
+
);
|
|
1996
|
+
for (const [sourceFile, fileRows] of byFile) {
|
|
1997
|
+
try {
|
|
1998
|
+
const content = readFileSync4(sourceFile, "utf-8");
|
|
1999
|
+
const lines = content.split("\n");
|
|
2000
|
+
const offsetToModel = /* @__PURE__ */ new Map();
|
|
2001
|
+
let currentModel = "";
|
|
2002
|
+
let byteOffset = 0;
|
|
2003
|
+
for (const line of lines) {
|
|
2004
|
+
if (line.trim()) {
|
|
2005
|
+
try {
|
|
2006
|
+
const parsed = JSON.parse(line);
|
|
2007
|
+
const turnModel = parsed.type === "turn_context" ? parsed.payload?.model : void 0;
|
|
2008
|
+
if (turnModel) currentModel = turnModel;
|
|
2009
|
+
const payload = parsed.event_msg?.payload ?? (parsed.type === "event_msg" ? parsed.payload : void 0);
|
|
2010
|
+
if (payload?.type === "token_count" && currentModel) {
|
|
2011
|
+
offsetToModel.set(byteOffset, currentModel);
|
|
2012
|
+
}
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
byteOffset += Buffer.byteLength(line, "utf-8") + 1;
|
|
2017
|
+
}
|
|
2018
|
+
for (const row of fileRows) {
|
|
2019
|
+
const model = offsetToModel.get(row.line_offset);
|
|
2020
|
+
if (!model) continue;
|
|
2021
|
+
const provider = inferProvider(model);
|
|
2022
|
+
updateStmt.run(model, provider, 0, "unknown", Date.now(), row.id);
|
|
2023
|
+
}
|
|
2024
|
+
} catch {
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function backfillMissingToolCalls(db, exchangeRate) {
|
|
2029
|
+
const rows = db.prepare(`
|
|
2030
|
+
SELECT r.id, r.source_file, r.tool, r.line_offset, r.session_id
|
|
2031
|
+
FROM records r
|
|
2032
|
+
LEFT JOIN tool_calls tc ON tc.record_id = r.id
|
|
2033
|
+
WHERE r.tool IN ('codex', 'openclaw', 'qoder')
|
|
2034
|
+
AND r.source_file NOT LIKE 'synced/%'
|
|
2035
|
+
AND tc.id IS NULL
|
|
2036
|
+
`).all();
|
|
2037
|
+
if (rows.length === 0) return;
|
|
2038
|
+
const rowsByFile = /* @__PURE__ */ new Map();
|
|
2039
|
+
for (const row of rows) {
|
|
2040
|
+
const list = rowsByFile.get(row.source_file) ?? [];
|
|
2041
|
+
list.push(row);
|
|
2042
|
+
rowsByFile.set(row.source_file, list);
|
|
2043
|
+
}
|
|
2044
|
+
for (const [sourceFile, fileRows] of rowsByFile) {
|
|
2045
|
+
try {
|
|
2046
|
+
const content = readFileSync4(sourceFile, "utf-8");
|
|
2047
|
+
const lines = content.split("\n");
|
|
2048
|
+
const aggregator = new Aggregator();
|
|
2049
|
+
const recordIdByOffset = /* @__PURE__ */ new Map();
|
|
2050
|
+
const sessionIdByOffset = /* @__PURE__ */ new Map();
|
|
2051
|
+
const tool = fileRows[0]?.tool;
|
|
2052
|
+
if (!tool) continue;
|
|
2053
|
+
for (const row of fileRows) {
|
|
2054
|
+
recordIdByOffset.set(row.line_offset, row.id);
|
|
2055
|
+
sessionIdByOffset.set(row.line_offset, row.session_id);
|
|
2056
|
+
}
|
|
2057
|
+
let byteOffset = 0;
|
|
2058
|
+
for (const line of lines) {
|
|
2059
|
+
if (!line.trim()) {
|
|
2060
|
+
byteOffset += Buffer.byteLength(line, "utf-8") + 1;
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
const context = aggregator.createContext({
|
|
2064
|
+
tool,
|
|
2065
|
+
sourceFile,
|
|
2066
|
+
lineOffset: byteOffset,
|
|
2067
|
+
sessionId: sessionIdByOffset.get(byteOffset) ?? deriveSessionId(tool, sourceFile),
|
|
2068
|
+
device: "",
|
|
2069
|
+
deviceInstanceId: "",
|
|
2070
|
+
exchangeRate
|
|
2071
|
+
});
|
|
2072
|
+
const result = aggregator.parseLine(line, context);
|
|
2073
|
+
if (result?.toolCalls?.length) {
|
|
2074
|
+
insertBackfilledToolCalls(db, result.toolCalls, tool, recordIdByOffset.get(byteOffset) ?? null);
|
|
2075
|
+
}
|
|
2076
|
+
byteOffset += Buffer.byteLength(line, "utf-8") + 1;
|
|
2077
|
+
}
|
|
2078
|
+
const orphanResults = aggregator.finalize();
|
|
2079
|
+
for (const result of orphanResults) {
|
|
2080
|
+
if (result.toolCalls.length) insertBackfilledToolCalls(db, result.toolCalls, tool, null);
|
|
2081
|
+
}
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function insertBackfilledToolCalls(db, toolCalls, tool, actualRecordId) {
|
|
2087
|
+
for (const tc of toolCalls) {
|
|
2088
|
+
if (actualRecordId) {
|
|
2089
|
+
const name = tc.name;
|
|
2090
|
+
const id = generateToolCallId(actualRecordId, name, tc.ts, tc.callIndex);
|
|
2091
|
+
const dup2 = db.prepare("SELECT 1 FROM tool_calls WHERE id = ?").get(id);
|
|
2092
|
+
if (!dup2) insertToolCall(db, { ...tc, id, recordId: actualRecordId });
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
const dup = db.prepare("SELECT 1 FROM tool_calls WHERE id = ?").get(tc.id);
|
|
2096
|
+
if (!dup) insertToolCall(db, { ...tc, tool });
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
function deriveSessionId(tool, sourceFile) {
|
|
2100
|
+
const normalized = sourceFile.replace(/\\/g, "/");
|
|
2101
|
+
const fileName = normalized.split("/").pop() ?? sourceFile;
|
|
2102
|
+
if (tool === "openclaw") return fileName.replace(/\.(trajectory\.)?jsonl$/, "");
|
|
2103
|
+
if (tool === "qoder") return normalized.split("/").slice(-2, -1)[0] ?? fileName.replace(/\.jsonl$/, "");
|
|
2104
|
+
return fileName.replace(/\.jsonl$/, "");
|
|
2105
|
+
}
|
|
1719
2106
|
function getDefaultSourcePaths() {
|
|
1720
2107
|
const home = homedir2();
|
|
1721
2108
|
return {
|
|
@@ -1725,10 +2112,293 @@ function getDefaultSourcePaths() {
|
|
|
1725
2112
|
"opencode": defaultOpenCodeDbPath(),
|
|
1726
2113
|
"hermes": defaultHermesDbPath(),
|
|
1727
2114
|
"qoder": join3(home, ".qoder", "logs", "sessions"),
|
|
1728
|
-
"qoder-db": defaultQoderDbPath()
|
|
2115
|
+
"qoder-db": defaultQoderDbPath(),
|
|
2116
|
+
"cursor": defaultCursorDbPath()
|
|
1729
2117
|
};
|
|
1730
2118
|
}
|
|
1731
2119
|
|
|
2120
|
+
// src/quota.ts
|
|
2121
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
2122
|
+
import { join as join4 } from "path";
|
|
2123
|
+
import { homedir as homedir3, platform as platform2 } from "os";
|
|
2124
|
+
import { execSync } from "child_process";
|
|
2125
|
+
function nowMs() {
|
|
2126
|
+
return Date.now();
|
|
2127
|
+
}
|
|
2128
|
+
function notFound(tool) {
|
|
2129
|
+
return { tool, credentialStatus: "not_found", credentialMessage: null, success: false, tiers: [], error: null, queriedAt: null };
|
|
2130
|
+
}
|
|
2131
|
+
function parseError(tool, message) {
|
|
2132
|
+
return { tool, credentialStatus: "parse_error", credentialMessage: message, success: false, tiers: [], error: message, queriedAt: nowMs() };
|
|
2133
|
+
}
|
|
2134
|
+
function expiredError(tool, message) {
|
|
2135
|
+
return { tool, credentialStatus: "expired", credentialMessage: message, success: false, tiers: [], error: message, queriedAt: nowMs() };
|
|
2136
|
+
}
|
|
2137
|
+
function apiError(tool, message) {
|
|
2138
|
+
return { tool, credentialStatus: "valid", credentialMessage: null, success: false, tiers: [], error: message, queriedAt: nowMs() };
|
|
2139
|
+
}
|
|
2140
|
+
function isExpired(expiresAt) {
|
|
2141
|
+
if (expiresAt == null) return false;
|
|
2142
|
+
const nowSecs = Date.now() / 1e3;
|
|
2143
|
+
if (typeof expiresAt === "number") {
|
|
2144
|
+
const secs = expiresAt > 1e12 ? expiresAt / 1e3 : expiresAt;
|
|
2145
|
+
return secs < nowSecs;
|
|
2146
|
+
}
|
|
2147
|
+
if (typeof expiresAt === "string") {
|
|
2148
|
+
const ts = Date.parse(expiresAt);
|
|
2149
|
+
if (!isNaN(ts)) return ts / 1e3 < nowSecs;
|
|
2150
|
+
}
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
function readFromKeychain(service) {
|
|
2154
|
+
if (platform2() !== "darwin") return null;
|
|
2155
|
+
try {
|
|
2156
|
+
const result = execSync(`security find-generic-password -s "${service}" -w 2>/dev/null`, {
|
|
2157
|
+
timeout: 3e3,
|
|
2158
|
+
encoding: "utf8",
|
|
2159
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2160
|
+
}).trim();
|
|
2161
|
+
return result || null;
|
|
2162
|
+
} catch {
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function parseClaudeCredJson(content) {
|
|
2167
|
+
let parsed;
|
|
2168
|
+
try {
|
|
2169
|
+
parsed = JSON.parse(content);
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
return { token: null, status: "parse_error", message: `Failed to parse credentials JSON: ${e}` };
|
|
2172
|
+
}
|
|
2173
|
+
const entry = parsed["claudeAiOauth"] ?? parsed["claude.ai_oauth"];
|
|
2174
|
+
if (!entry) {
|
|
2175
|
+
return { token: null, status: "parse_error", message: "No OAuth entry found in credentials" };
|
|
2176
|
+
}
|
|
2177
|
+
const accessToken = entry["accessToken"];
|
|
2178
|
+
if (!accessToken) {
|
|
2179
|
+
return { token: null, status: "parse_error", message: "accessToken is empty or missing" };
|
|
2180
|
+
}
|
|
2181
|
+
if (isExpired(entry["expiresAt"])) {
|
|
2182
|
+
return { token: accessToken, status: "expired", message: "OAuth token has expired" };
|
|
2183
|
+
}
|
|
2184
|
+
return { token: accessToken, status: "valid", message: null };
|
|
2185
|
+
}
|
|
2186
|
+
function readClaudeCredentials() {
|
|
2187
|
+
const keychainJson = readFromKeychain("Claude Code-credentials");
|
|
2188
|
+
if (keychainJson) {
|
|
2189
|
+
return parseClaudeCredJson(keychainJson);
|
|
2190
|
+
}
|
|
2191
|
+
const credPath = join4(homedir3(), ".claude", ".credentials.json");
|
|
2192
|
+
if (!existsSync5(credPath)) {
|
|
2193
|
+
return { token: null, status: "not_found", message: null };
|
|
2194
|
+
}
|
|
2195
|
+
let content;
|
|
2196
|
+
try {
|
|
2197
|
+
content = readFileSync5(credPath, "utf-8");
|
|
2198
|
+
} catch (e) {
|
|
2199
|
+
return { token: null, status: "parse_error", message: `Failed to read credentials file: ${e}` };
|
|
2200
|
+
}
|
|
2201
|
+
return parseClaudeCredJson(content);
|
|
2202
|
+
}
|
|
2203
|
+
function parseCodexCredJson(content) {
|
|
2204
|
+
let parsed;
|
|
2205
|
+
try {
|
|
2206
|
+
parsed = JSON.parse(content);
|
|
2207
|
+
} catch (e) {
|
|
2208
|
+
return { token: null, accountId: null, status: "parse_error", message: `Failed to parse Codex auth JSON: ${e}` };
|
|
2209
|
+
}
|
|
2210
|
+
if (parsed["auth_mode"] !== "chatgpt") {
|
|
2211
|
+
return { token: null, accountId: null, status: "not_found", message: "Codex not using OAuth mode" };
|
|
2212
|
+
}
|
|
2213
|
+
const tokens = parsed["tokens"];
|
|
2214
|
+
if (!tokens) {
|
|
2215
|
+
return { token: null, accountId: null, status: "parse_error", message: "No tokens in Codex auth" };
|
|
2216
|
+
}
|
|
2217
|
+
const accessToken = tokens["access_token"];
|
|
2218
|
+
if (!accessToken) {
|
|
2219
|
+
return { token: null, accountId: null, status: "parse_error", message: "access_token is empty or missing" };
|
|
2220
|
+
}
|
|
2221
|
+
const accountId = tokens["account_id"] ?? parsed["account_id"] ?? null;
|
|
2222
|
+
const expiresAt = tokens["expires_at"] ?? tokens["expiresAt"];
|
|
2223
|
+
if (isExpired(expiresAt)) {
|
|
2224
|
+
return { token: accessToken, accountId, status: "expired", message: "Codex OAuth token may be stale" };
|
|
2225
|
+
}
|
|
2226
|
+
return { token: accessToken, accountId, status: "valid", message: null };
|
|
2227
|
+
}
|
|
2228
|
+
function readCodexCredentials() {
|
|
2229
|
+
const keychainJson = readFromKeychain("Codex Auth");
|
|
2230
|
+
if (keychainJson) {
|
|
2231
|
+
return parseCodexCredJson(keychainJson);
|
|
2232
|
+
}
|
|
2233
|
+
const authPath = join4(homedir3(), ".codex", "auth.json");
|
|
2234
|
+
if (!existsSync5(authPath)) {
|
|
2235
|
+
return { token: null, accountId: null, status: "not_found", message: null };
|
|
2236
|
+
}
|
|
2237
|
+
let content;
|
|
2238
|
+
try {
|
|
2239
|
+
content = readFileSync5(authPath, "utf-8");
|
|
2240
|
+
} catch (e) {
|
|
2241
|
+
return { token: null, accountId: null, status: "parse_error", message: `Failed to read Codex auth file: ${e}` };
|
|
2242
|
+
}
|
|
2243
|
+
return parseCodexCredJson(content);
|
|
2244
|
+
}
|
|
2245
|
+
var CLAUDE_KNOWN_TIERS = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
|
|
2246
|
+
var CLAUDE_QUOTA_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
2247
|
+
async function queryClaudeQuota(accessToken) {
|
|
2248
|
+
let resp;
|
|
2249
|
+
try {
|
|
2250
|
+
resp = await fetch(CLAUDE_QUOTA_URL, {
|
|
2251
|
+
headers: {
|
|
2252
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
2253
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
2254
|
+
"Accept": "application/json"
|
|
2255
|
+
},
|
|
2256
|
+
signal: AbortSignal.timeout(1e4)
|
|
2257
|
+
});
|
|
2258
|
+
} catch (e) {
|
|
2259
|
+
return apiError("claude-code", `Network error: ${e}`);
|
|
2260
|
+
}
|
|
2261
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
2262
|
+
return expiredError("claude-code", `Authentication failed (HTTP ${resp.status}). Please re-login with Claude CLI.`);
|
|
2263
|
+
}
|
|
2264
|
+
if (!resp.ok) {
|
|
2265
|
+
const body2 = await resp.text().catch(() => "");
|
|
2266
|
+
return apiError("claude-code", `API error (HTTP ${resp.status}): ${body2}`);
|
|
2267
|
+
}
|
|
2268
|
+
let body;
|
|
2269
|
+
try {
|
|
2270
|
+
body = await resp.json();
|
|
2271
|
+
} catch (e) {
|
|
2272
|
+
return apiError("claude-code", `Failed to parse API response: ${e}`);
|
|
2273
|
+
}
|
|
2274
|
+
const tiers = [];
|
|
2275
|
+
for (const name of CLAUDE_KNOWN_TIERS) {
|
|
2276
|
+
const window = body[name];
|
|
2277
|
+
if (!window) continue;
|
|
2278
|
+
const utilization = window["utilization"];
|
|
2279
|
+
if (utilization == null) continue;
|
|
2280
|
+
tiers.push({
|
|
2281
|
+
name,
|
|
2282
|
+
utilization,
|
|
2283
|
+
resetsAt: window["resets_at"] ?? null
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
for (const [key, value] of Object.entries(body)) {
|
|
2287
|
+
if (key === "extra_usage" || CLAUDE_KNOWN_TIERS.includes(key)) continue;
|
|
2288
|
+
const window = value;
|
|
2289
|
+
if (!window || typeof window !== "object") continue;
|
|
2290
|
+
const utilization = window["utilization"];
|
|
2291
|
+
if (utilization == null) continue;
|
|
2292
|
+
tiers.push({
|
|
2293
|
+
name: key,
|
|
2294
|
+
utilization,
|
|
2295
|
+
resetsAt: window["resets_at"] ?? null
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
tool: "claude-code",
|
|
2300
|
+
credentialStatus: "valid",
|
|
2301
|
+
credentialMessage: null,
|
|
2302
|
+
success: true,
|
|
2303
|
+
tiers,
|
|
2304
|
+
error: null,
|
|
2305
|
+
queriedAt: nowMs()
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
var CODEX_QUOTA_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
2309
|
+
function windowSecondsToTierName(seconds) {
|
|
2310
|
+
if (seconds <= 3600 * 6) return "five_hour";
|
|
2311
|
+
if (seconds <= 86400 * 7) return "weekly_limit";
|
|
2312
|
+
return `${seconds}s`;
|
|
2313
|
+
}
|
|
2314
|
+
async function callCodexQuotaApi(accessToken, accountId) {
|
|
2315
|
+
const headers = {
|
|
2316
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
2317
|
+
"User-Agent": "codex-cli",
|
|
2318
|
+
"Accept": "application/json"
|
|
2319
|
+
};
|
|
2320
|
+
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
|
|
2321
|
+
let resp;
|
|
2322
|
+
try {
|
|
2323
|
+
resp = await fetch(CODEX_QUOTA_URL, {
|
|
2324
|
+
headers,
|
|
2325
|
+
signal: AbortSignal.timeout(1e4)
|
|
2326
|
+
});
|
|
2327
|
+
} catch (e) {
|
|
2328
|
+
return apiError("codex", `Network error: ${e}`);
|
|
2329
|
+
}
|
|
2330
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
2331
|
+
return expiredError("codex", `Authentication failed (HTTP ${resp.status}). Please re-login with Codex CLI.`);
|
|
2332
|
+
}
|
|
2333
|
+
if (!resp.ok) {
|
|
2334
|
+
const body2 = await resp.text().catch(() => "");
|
|
2335
|
+
return apiError("codex", `API error (HTTP ${resp.status}): ${body2}`);
|
|
2336
|
+
}
|
|
2337
|
+
let body;
|
|
2338
|
+
try {
|
|
2339
|
+
body = await resp.json();
|
|
2340
|
+
} catch (e) {
|
|
2341
|
+
return apiError("codex", `Failed to parse API response: ${e}`);
|
|
2342
|
+
}
|
|
2343
|
+
const tiers = [];
|
|
2344
|
+
const rateLimit = body["rate_limit"];
|
|
2345
|
+
if (rateLimit) {
|
|
2346
|
+
for (const windowKey of ["primary_window", "secondary_window"]) {
|
|
2347
|
+
const window = rateLimit[windowKey];
|
|
2348
|
+
if (!window) continue;
|
|
2349
|
+
const usedPercent = window["used_percent"];
|
|
2350
|
+
if (usedPercent == null) continue;
|
|
2351
|
+
const windowSecs = window["limit_window_seconds"];
|
|
2352
|
+
const name = windowSecs != null ? windowSecondsToTierName(windowSecs) : "unknown";
|
|
2353
|
+
const resetAt = window["reset_at"];
|
|
2354
|
+
const resetsAt = resetAt ? new Date(resetAt * 1e3).toISOString() : null;
|
|
2355
|
+
tiers.push({ name, utilization: usedPercent, resetsAt });
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
tool: "codex",
|
|
2360
|
+
credentialStatus: "valid",
|
|
2361
|
+
credentialMessage: null,
|
|
2362
|
+
success: true,
|
|
2363
|
+
tiers,
|
|
2364
|
+
error: null,
|
|
2365
|
+
queriedAt: nowMs()
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
async function queryClaudeCodeQuota() {
|
|
2369
|
+
const cred = readClaudeCredentials();
|
|
2370
|
+
if (cred.status === "not_found") return notFound("claude-code");
|
|
2371
|
+
if (cred.status === "parse_error") return parseError("claude-code", cred.message ?? "Failed to parse credentials");
|
|
2372
|
+
if (cred.status === "expired") {
|
|
2373
|
+
if (cred.token) {
|
|
2374
|
+
const result = await queryClaudeQuota(cred.token);
|
|
2375
|
+
if (result.success) return result;
|
|
2376
|
+
}
|
|
2377
|
+
return expiredError("claude-code", cred.message ?? "OAuth token has expired");
|
|
2378
|
+
}
|
|
2379
|
+
return queryClaudeQuota(cred.token);
|
|
2380
|
+
}
|
|
2381
|
+
async function queryCodexQuota() {
|
|
2382
|
+
const cred = readCodexCredentials();
|
|
2383
|
+
if (cred.status === "not_found") return notFound("codex");
|
|
2384
|
+
if (cred.status === "parse_error") return parseError("codex", cred.message ?? "Failed to parse credentials");
|
|
2385
|
+
if (cred.status === "expired") {
|
|
2386
|
+
if (cred.token) {
|
|
2387
|
+
const result = await callCodexQuotaApi(cred.token, cred.accountId);
|
|
2388
|
+
if (result.success) return result;
|
|
2389
|
+
}
|
|
2390
|
+
return expiredError("codex", cred.message ?? "Codex OAuth token may be stale");
|
|
2391
|
+
}
|
|
2392
|
+
return callCodexQuotaApi(cred.token, cred.accountId);
|
|
2393
|
+
}
|
|
2394
|
+
async function queryAllQuotas() {
|
|
2395
|
+
const [claude, codex] = await Promise.all([
|
|
2396
|
+
queryClaudeCodeQuota(),
|
|
2397
|
+
queryCodexQuota()
|
|
2398
|
+
]);
|
|
2399
|
+
return [claude, codex];
|
|
2400
|
+
}
|
|
2401
|
+
|
|
1732
2402
|
// src/api/server.ts
|
|
1733
2403
|
function getDateRangeFilter(range, from, to, prefix = "", weekStart = 1) {
|
|
1734
2404
|
const ts = prefix ? `${prefix}.ts` : "ts";
|
|
@@ -1769,6 +2439,27 @@ function getToolFilter(tool, prefix = "") {
|
|
|
1769
2439
|
const col = prefix ? `${prefix}.tool` : "tool";
|
|
1770
2440
|
return { where: `AND ${col} = @tool`, params: { tool } };
|
|
1771
2441
|
}
|
|
2442
|
+
function classifyToolCall(name) {
|
|
2443
|
+
if (name.startsWith("mcp__")) return "mcp";
|
|
2444
|
+
if (name.startsWith("skill__") || name === "Skill") return "skill";
|
|
2445
|
+
return "builtin";
|
|
2446
|
+
}
|
|
2447
|
+
function parseMcpName(name) {
|
|
2448
|
+
const withoutPrefix = name.slice(5);
|
|
2449
|
+
const idx = withoutPrefix.indexOf("__");
|
|
2450
|
+
if (idx === -1) return { server: withoutPrefix, action: "", display: withoutPrefix };
|
|
2451
|
+
return {
|
|
2452
|
+
server: withoutPrefix.slice(0, idx),
|
|
2453
|
+
action: withoutPrefix.slice(idx + 2),
|
|
2454
|
+
display: `${withoutPrefix.slice(0, idx)} / ${withoutPrefix.slice(idx + 2)}`
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
function getToolTypeFilter(toolType) {
|
|
2458
|
+
if (toolType === "mcp") return "AND tc.name LIKE 'mcp\\_\\_%' ESCAPE '\\'";
|
|
2459
|
+
if (toolType === "skill") return "AND (tc.name LIKE 'skill\\_\\_%' ESCAPE '\\' OR tc.name = 'Skill')";
|
|
2460
|
+
if (toolType === "builtin") return "AND tc.name NOT LIKE 'mcp\\_\\_%' ESCAPE '\\' AND tc.name NOT LIKE 'skill\\_\\_%' ESCAPE '\\' AND tc.name != 'Skill'";
|
|
2461
|
+
return "";
|
|
2462
|
+
}
|
|
1772
2463
|
var LOCAL_ONLY_FILTER = "AND source_file NOT LIKE 'synced/%'";
|
|
1773
2464
|
function getDeviceFilter(device, currentDeviceInstanceId) {
|
|
1774
2465
|
if (!currentDeviceInstanceId) {
|
|
@@ -1809,7 +2500,7 @@ function createApiServer(db, options) {
|
|
|
1809
2500
|
const from = url.searchParams.get("from");
|
|
1810
2501
|
const to = url.searchParams.get("to");
|
|
1811
2502
|
const toolParam = url.searchParams.get("tool");
|
|
1812
|
-
const VALID_TOOLS = ["claude-code", "codex", "openclaw", "opencode", "hermes", "qoder"];
|
|
2503
|
+
const VALID_TOOLS = ["claude-code", "codex", "openclaw", "opencode", "hermes", "qoder", "cursor"];
|
|
1813
2504
|
if (toolParam && !VALID_TOOLS.includes(toolParam)) {
|
|
1814
2505
|
json(res, { error: { code: "INVALID_PARAM", message: "Invalid tool" } }, 400);
|
|
1815
2506
|
return;
|
|
@@ -1920,6 +2611,21 @@ function createApiServer(db, options) {
|
|
|
1920
2611
|
WHERE 1=1 ${dfJoin} ${drJoin.where} ${tfJoin.where}
|
|
1921
2612
|
GROUP BY tc.name ORDER BY count DESC LIMIT 10
|
|
1922
2613
|
`).all({ ...drJoin.params, ...df.params, ...tfJoin.params });
|
|
2614
|
+
const topMcpServersRaw = db.prepare(`
|
|
2615
|
+
SELECT tc.name, COUNT(*) AS count
|
|
2616
|
+
FROM tool_calls tc
|
|
2617
|
+
JOIN records r ON r.id = tc.record_id
|
|
2618
|
+
WHERE tc.name LIKE 'mcp\\_\\_%' ESCAPE '\\'
|
|
2619
|
+
AND INSTR(SUBSTR(tc.name, 6), '__') > 0
|
|
2620
|
+
${dfJoin} ${drJoin.where} ${tfJoin.where}
|
|
2621
|
+
GROUP BY tc.name ORDER BY count DESC
|
|
2622
|
+
`).all({ ...drJoin.params, ...df.params, ...tfJoin.params });
|
|
2623
|
+
const mcpServerMap = /* @__PURE__ */ new Map();
|
|
2624
|
+
for (const row of topMcpServersRaw) {
|
|
2625
|
+
const server2 = parseMcpName(row.name).server;
|
|
2626
|
+
mcpServerMap.set(server2, (mcpServerMap.get(server2) ?? 0) + row.count);
|
|
2627
|
+
}
|
|
2628
|
+
const topMcpServers = Array.from(mcpServerMap.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([server2, count]) => ({ server: server2, count }));
|
|
1923
2629
|
json(res, {
|
|
1924
2630
|
inputTokens: totals.inputTokens,
|
|
1925
2631
|
outputTokens: totals.outputTokens,
|
|
@@ -1931,7 +2637,8 @@ function createApiServer(db, options) {
|
|
|
1931
2637
|
activeDays: totals.activeDays,
|
|
1932
2638
|
totalSessions: totals.totalSessions,
|
|
1933
2639
|
byTool,
|
|
1934
|
-
topToolCalls
|
|
2640
|
+
topToolCalls,
|
|
2641
|
+
topMcpServers
|
|
1935
2642
|
});
|
|
1936
2643
|
return;
|
|
1937
2644
|
}
|
|
@@ -2127,31 +2834,126 @@ function createApiServer(db, options) {
|
|
|
2127
2834
|
const dr = getDateRangeFilter(range, from, to, "r", weekStart);
|
|
2128
2835
|
const tool = url.searchParams.get("tool");
|
|
2129
2836
|
const tf = getToolFilter(tool, "r");
|
|
2837
|
+
const toolType = url.searchParams.get("toolType");
|
|
2838
|
+
if (toolType && !["mcp", "skill", "builtin"].includes(toolType)) {
|
|
2839
|
+
json(res, { error: { code: "INVALID_PARAM", message: "Invalid toolType" } }, 400);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const ttf = getToolTypeFilter(toolType);
|
|
2130
2843
|
const totalRow = db.prepare(`
|
|
2131
2844
|
SELECT COUNT(*) AS total FROM tool_calls tc
|
|
2132
2845
|
JOIN records r ON r.id = tc.record_id
|
|
2133
|
-
WHERE 1=1 ${dr.where} ${tf.where}
|
|
2846
|
+
WHERE 1=1 ${dr.where} ${tf.where} ${ttf}
|
|
2134
2847
|
`).get({ ...dr.params, ...tf.params });
|
|
2135
2848
|
const total = totalRow.total || 1;
|
|
2136
2849
|
const rows = db.prepare(`
|
|
2137
2850
|
SELECT tc.name, COUNT(*) AS count
|
|
2138
2851
|
FROM tool_calls tc
|
|
2139
2852
|
JOIN records r ON r.id = tc.record_id
|
|
2140
|
-
WHERE 1=1 ${dr.where} ${tf.where}
|
|
2853
|
+
WHERE 1=1 ${dr.where} ${tf.where} ${ttf}
|
|
2141
2854
|
GROUP BY tc.name ORDER BY count DESC
|
|
2142
2855
|
`).all({ ...dr.params, ...tf.params });
|
|
2143
|
-
const toolCalls = rows.map((r) =>
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2856
|
+
const toolCalls = rows.map((r) => {
|
|
2857
|
+
const type = classifyToolCall(r.name);
|
|
2858
|
+
const mcpParsed = type === "mcp" ? parseMcpName(r.name) : null;
|
|
2859
|
+
const displayName = mcpParsed ? mcpParsed.display : type === "skill" && r.name.startsWith("skill__") ? r.name.slice("skill__".length) : r.name;
|
|
2860
|
+
return {
|
|
2861
|
+
name: r.name,
|
|
2862
|
+
displayName,
|
|
2863
|
+
mcpServer: mcpParsed ? mcpParsed.server : null,
|
|
2864
|
+
type,
|
|
2865
|
+
count: r.count,
|
|
2866
|
+
percentage: Math.round(r.count / total * 1e3) / 10
|
|
2867
|
+
};
|
|
2868
|
+
});
|
|
2148
2869
|
json(res, { toolCalls });
|
|
2149
2870
|
return;
|
|
2150
2871
|
}
|
|
2872
|
+
if (url.pathname.startsWith("/api/sessions/") && req.method === "GET") {
|
|
2873
|
+
const sessionId = decodeURIComponent(url.pathname.slice("/api/sessions/".length));
|
|
2874
|
+
if (!sessionId) {
|
|
2875
|
+
json(res, { error: { code: "INVALID_PARAM", message: "Missing sessionId" } }, 400);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const toolParam2 = url.searchParams.get("tool");
|
|
2879
|
+
const deviceParam = url.searchParams.get("device");
|
|
2880
|
+
const toolClause = toolParam2 ? "AND tool = @tool" : "";
|
|
2881
|
+
const deviceClause = deviceParam ? "AND device_instance_id = @device" : "";
|
|
2882
|
+
const filterParams = { sessionId };
|
|
2883
|
+
if (toolParam2) filterParams.tool = toolParam2;
|
|
2884
|
+
if (deviceParam) filterParams.device = deviceParam;
|
|
2885
|
+
const meta = db.prepare(`
|
|
2886
|
+
SELECT session_id AS sessionId,
|
|
2887
|
+
tool, model,
|
|
2888
|
+
MIN(ts) AS firstTs,
|
|
2889
|
+
MAX(ts) AS lastTs,
|
|
2890
|
+
MAX(ts) - MIN(ts) AS duration,
|
|
2891
|
+
SUM(input_tokens) AS inputTokens,
|
|
2892
|
+
SUM(output_tokens) AS outputTokens,
|
|
2893
|
+
SUM(cache_read_tokens) AS cacheReadTokens,
|
|
2894
|
+
SUM(cache_write_tokens) AS cacheWriteTokens,
|
|
2895
|
+
SUM(thinking_tokens) AS thinkingTokens,
|
|
2896
|
+
SUM(cost) AS cost,
|
|
2897
|
+
COUNT(*) AS recordCount
|
|
2898
|
+
FROM records
|
|
2899
|
+
WHERE session_id = @sessionId ${toolClause} ${deviceClause}
|
|
2900
|
+
GROUP BY session_id, tool, model
|
|
2901
|
+
ORDER BY MIN(ts) ASC
|
|
2902
|
+
LIMIT 1
|
|
2903
|
+
`).get(filterParams);
|
|
2904
|
+
if (!meta) {
|
|
2905
|
+
json(res, { error: { code: "NOT_FOUND", message: "Session not found" } }, 404);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
const records = db.prepare(`
|
|
2909
|
+
SELECT id, ts, model,
|
|
2910
|
+
input_tokens AS inputTokens,
|
|
2911
|
+
output_tokens AS outputTokens,
|
|
2912
|
+
cache_read_tokens AS cacheReadTokens,
|
|
2913
|
+
cache_write_tokens AS cacheWriteTokens,
|
|
2914
|
+
thinking_tokens AS thinkingTokens,
|
|
2915
|
+
cost
|
|
2916
|
+
FROM records
|
|
2917
|
+
WHERE session_id = @sessionId ${toolClause} ${deviceClause}
|
|
2918
|
+
ORDER BY ts ASC
|
|
2919
|
+
`).all(filterParams);
|
|
2920
|
+
const rToolClause = toolParam2 ? "AND r.tool = @tool" : "";
|
|
2921
|
+
const rDeviceClause = deviceParam ? "AND r.device_instance_id = @device" : "";
|
|
2922
|
+
const toolCallRows = db.prepare(`
|
|
2923
|
+
SELECT tc.record_id AS recordId, tc.name, tc.ts, tc.call_index AS callIndex
|
|
2924
|
+
FROM tool_calls tc
|
|
2925
|
+
JOIN records r ON r.id = tc.record_id
|
|
2926
|
+
WHERE r.session_id = @sessionId ${rToolClause} ${rDeviceClause}
|
|
2927
|
+
ORDER BY tc.record_id, tc.call_index ASC
|
|
2928
|
+
`).all(filterParams);
|
|
2929
|
+
const toolCallsByRecord = {};
|
|
2930
|
+
for (const tc of toolCallRows) {
|
|
2931
|
+
if (!toolCallsByRecord[tc.recordId]) toolCallsByRecord[tc.recordId] = [];
|
|
2932
|
+
const type = classifyToolCall(tc.name);
|
|
2933
|
+
const mcpParsed = type === "mcp" ? parseMcpName(tc.name) : null;
|
|
2934
|
+
const displayName = mcpParsed ? mcpParsed.display : type === "skill" && tc.name.startsWith("skill__") ? tc.name.slice("skill__".length) : tc.name;
|
|
2935
|
+
toolCallsByRecord[tc.recordId].push({
|
|
2936
|
+
name: tc.name,
|
|
2937
|
+
displayName,
|
|
2938
|
+
type,
|
|
2939
|
+
ts: tc.ts,
|
|
2940
|
+
callIndex: tc.callIndex
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
const toolCallCount = toolCallRows.length;
|
|
2944
|
+
json(res, {
|
|
2945
|
+
session: { ...meta, toolCallCount },
|
|
2946
|
+
records: records.map((r) => ({
|
|
2947
|
+
...r,
|
|
2948
|
+
toolCalls: toolCallsByRecord[r.id] ?? []
|
|
2949
|
+
}))
|
|
2950
|
+
});
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2151
2953
|
if (url.pathname === "/api/sessions") {
|
|
2152
2954
|
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
|
|
2153
2955
|
const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get("pageSize") || "50", 10)));
|
|
2154
|
-
const dr = getDateRangeFilter(range, from, to, "", weekStart);
|
|
2956
|
+
const dr = getDateRangeFilter(range, from, to, "r", weekStart);
|
|
2155
2957
|
const device = url.searchParams.get("device");
|
|
2156
2958
|
const df = getDeviceFilter(device, options?.currentDeviceInstanceId);
|
|
2157
2959
|
const tool = url.searchParams.get("tool");
|
|
@@ -2164,26 +2966,30 @@ function createApiServer(db, options) {
|
|
|
2164
2966
|
});
|
|
2165
2967
|
return;
|
|
2166
2968
|
}
|
|
2167
|
-
const tf = getToolFilter(tool);
|
|
2969
|
+
const tf = getToolFilter(tool, "r");
|
|
2168
2970
|
const params = { ...dr.params, ...tf.params };
|
|
2169
2971
|
const totalRow = db.prepare(`
|
|
2170
|
-
SELECT COUNT(DISTINCT session_id) AS total
|
|
2171
|
-
FROM records
|
|
2972
|
+
SELECT COUNT(DISTINCT r.session_id) AS total
|
|
2973
|
+
FROM records r
|
|
2974
|
+
WHERE 1=1 ${dr.where} ${df.localOnly ? LOCAL_ONLY_FILTER : ""} ${tf.where}
|
|
2172
2975
|
`).get(params);
|
|
2173
2976
|
const sessions = db.prepare(`
|
|
2174
|
-
SELECT session_id AS sessionId,
|
|
2175
|
-
tool,
|
|
2176
|
-
model,
|
|
2177
|
-
MIN(ts) AS ts,
|
|
2178
|
-
|
|
2179
|
-
SUM(
|
|
2180
|
-
SUM(
|
|
2181
|
-
SUM(
|
|
2182
|
-
SUM(
|
|
2183
|
-
|
|
2977
|
+
SELECT r.session_id AS sessionId,
|
|
2978
|
+
r.tool,
|
|
2979
|
+
r.model,
|
|
2980
|
+
MIN(r.ts) AS ts,
|
|
2981
|
+
MAX(r.ts) - MIN(r.ts) AS duration,
|
|
2982
|
+
SUM(r.input_tokens) AS inputTokens,
|
|
2983
|
+
SUM(r.output_tokens) AS outputTokens,
|
|
2984
|
+
SUM(r.cache_read_tokens) AS cacheReadTokens,
|
|
2985
|
+
SUM(r.cache_write_tokens) AS cacheWriteTokens,
|
|
2986
|
+
SUM(r.cost) AS cost,
|
|
2987
|
+
COUNT(DISTINCT tc.id) AS toolCallCount
|
|
2988
|
+
FROM records r
|
|
2989
|
+
LEFT JOIN tool_calls tc ON tc.record_id = r.id
|
|
2184
2990
|
WHERE 1=1 ${dr.where} ${df.localOnly ? LOCAL_ONLY_FILTER : ""} ${tf.where}
|
|
2185
|
-
GROUP BY session_id
|
|
2186
|
-
ORDER BY ts DESC
|
|
2991
|
+
GROUP BY r.session_id
|
|
2992
|
+
ORDER BY MIN(r.ts) DESC
|
|
2187
2993
|
LIMIT @limit OFFSET @offset
|
|
2188
2994
|
`).all({ ...params, limit: pageSize, offset: (page - 1) * pageSize });
|
|
2189
2995
|
json(res, {
|
|
@@ -2212,10 +3018,23 @@ function createApiServer(db, options) {
|
|
|
2212
3018
|
FROM records WHERE 1=1 ${dr.where} ${df.localOnly ? LOCAL_ONLY_FILTER : ""} ${tf.where}
|
|
2213
3019
|
GROUP BY source_file ORDER BY totalTokens DESC
|
|
2214
3020
|
`).all({ ...dr.params, ...tf.params });
|
|
3021
|
+
const cwdByEncodedDir = {};
|
|
3022
|
+
for (const row of rows) {
|
|
3023
|
+
if (row.cwd) {
|
|
3024
|
+
const m = row.source_file.replace(/\\/g, "/").match(/\.claude\/projects\/([^/]+)/);
|
|
3025
|
+
if (m && !cwdByEncodedDir[m[1]]) cwdByEncodedDir[m[1]] = row.cwd;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
2215
3028
|
const projectMap = {};
|
|
2216
3029
|
for (const row of rows) {
|
|
2217
|
-
|
|
2218
|
-
if (!
|
|
3030
|
+
let effectiveCwd = row.cwd || "";
|
|
3031
|
+
if (!effectiveCwd) {
|
|
3032
|
+
const m = row.source_file.replace(/\\/g, "/").match(/\.claude\/projects\/([^/]+)/);
|
|
3033
|
+
if (m) effectiveCwd = cwdByEncodedDir[m[1]] || "";
|
|
3034
|
+
}
|
|
3035
|
+
const fromCwd = effectiveCwd ? extractProjectFromCwd(effectiveCwd) : null;
|
|
3036
|
+
const project = fromCwd && fromCwd !== "unknown" ? fromCwd : extractProject(row.source_file);
|
|
3037
|
+
if (!projectMap[project]) projectMap[project] = { sessions: 0, tokens: 0, cost: 0, fullPath: effectiveCwd || row.source_file };
|
|
2219
3038
|
projectMap[project].sessions += row.sessionCount;
|
|
2220
3039
|
projectMap[project].tokens += row.totalTokens;
|
|
2221
3040
|
projectMap[project].cost += row.cost;
|
|
@@ -2226,7 +3045,8 @@ function createApiServer(db, options) {
|
|
|
2226
3045
|
sessions: data.sessions,
|
|
2227
3046
|
tokens: data.tokens,
|
|
2228
3047
|
cost: data.cost,
|
|
2229
|
-
percentage: Math.round(data.tokens / totalTokens * 1e3) / 10
|
|
3048
|
+
percentage: Math.round(data.tokens / totalTokens * 1e3) / 10,
|
|
3049
|
+
fullPath: data.fullPath
|
|
2230
3050
|
})).sort((a, b) => b.tokens - a.tokens);
|
|
2231
3051
|
json(res, { projects });
|
|
2232
3052
|
return;
|
|
@@ -2376,6 +3196,11 @@ function createApiServer(db, options) {
|
|
|
2376
3196
|
json(res, result);
|
|
2377
3197
|
return;
|
|
2378
3198
|
}
|
|
3199
|
+
if (url.pathname === "/api/quotas" && req.method === "GET") {
|
|
3200
|
+
const results = await queryAllQuotas();
|
|
3201
|
+
json(res, { quotas: results });
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
2379
3204
|
if (url.pathname === "/api/devices") {
|
|
2380
3205
|
let getDisplayName2 = function(device, deviceInstanceId) {
|
|
2381
3206
|
if (deviceInstanceId === currentId) return currentDeviceAlias;
|
|
@@ -2416,7 +3241,7 @@ function createApiServer(db, options) {
|
|
|
2416
3241
|
WHERE device_instance_id != @currentId
|
|
2417
3242
|
GROUP BY device_instance_id
|
|
2418
3243
|
`).all({ currentId });
|
|
2419
|
-
const currentPlatform = config?.platform ||
|
|
3244
|
+
const currentPlatform = config?.platform || platform3();
|
|
2420
3245
|
const deviceMap = /* @__PURE__ */ new Map();
|
|
2421
3246
|
for (const row of localRows) {
|
|
2422
3247
|
const displayName = getDisplayName2(row.device, row.deviceInstanceId);
|
|
@@ -2453,7 +3278,7 @@ function createApiServer(db, options) {
|
|
|
2453
3278
|
if (url.pathname === "/api/config") {
|
|
2454
3279
|
if (req.method === "GET") {
|
|
2455
3280
|
const currentCfg = loadConfig() ?? {};
|
|
2456
|
-
const osPlatform =
|
|
3281
|
+
const osPlatform = platform3();
|
|
2457
3282
|
const { credentials, priceOverrides, platform: _cfgPlatform, ...rest } = currentCfg;
|
|
2458
3283
|
json(res, {
|
|
2459
3284
|
device: rest.device ?? null,
|
|
@@ -2468,7 +3293,8 @@ function createApiServer(db, options) {
|
|
|
2468
3293
|
opencode: rest.sources?.opencode ?? null,
|
|
2469
3294
|
hermes: rest.sources?.hermes ?? null,
|
|
2470
3295
|
qoder: rest.sources?.qoder ?? null,
|
|
2471
|
-
"qoder-db": rest.sources?.["qoder-db"] ?? null
|
|
3296
|
+
"qoder-db": rest.sources?.["qoder-db"] ?? null,
|
|
3297
|
+
cursor: rest.sources?.cursor ?? null
|
|
2472
3298
|
},
|
|
2473
3299
|
sync: rest.sync ?? null,
|
|
2474
3300
|
displayCurrency: rest.displayCurrency ?? "USD",
|
|
@@ -2531,7 +3357,7 @@ function createApiServer(db, options) {
|
|
|
2531
3357
|
if (update.sources && typeof update.sources === "object") {
|
|
2532
3358
|
const src = update.sources;
|
|
2533
3359
|
const s = existing.sources ?? {};
|
|
2534
|
-
for (const key of ["claude-code", "codex", "openclaw", "opencode", "hermes", "qoder", "qoder-db"]) {
|
|
3360
|
+
for (const key of ["claude-code", "codex", "openclaw", "opencode", "hermes", "qoder", "qoder-db", "cursor"]) {
|
|
2535
3361
|
if (key in src) {
|
|
2536
3362
|
if (!src[key]) delete s[key];
|
|
2537
3363
|
else s[key] = String(src[key]);
|
|
@@ -2599,7 +3425,7 @@ function createApiServer(db, options) {
|
|
|
2599
3425
|
}
|
|
2600
3426
|
|
|
2601
3427
|
// src/commands/sync.ts
|
|
2602
|
-
import { join as
|
|
3428
|
+
import { join as join6 } from "path";
|
|
2603
3429
|
|
|
2604
3430
|
// src/db/synced-records.ts
|
|
2605
3431
|
function insertSyncedRecord(db, record) {
|
|
@@ -2803,15 +3629,15 @@ var SyncOrchestrator = class {
|
|
|
2803
3629
|
});
|
|
2804
3630
|
if (paths.length === 0) return 0;
|
|
2805
3631
|
let totalPulled = 0;
|
|
2806
|
-
for (const [index,
|
|
3632
|
+
for (const [index, path2] of paths.entries()) {
|
|
2807
3633
|
this.options.onProgress?.({
|
|
2808
3634
|
phase: "pulling",
|
|
2809
|
-
currentPath:
|
|
3635
|
+
currentPath: path2,
|
|
2810
3636
|
completedFiles: index,
|
|
2811
3637
|
totalFiles: paths.length,
|
|
2812
3638
|
pulledCount: totalPulled
|
|
2813
3639
|
});
|
|
2814
|
-
const content = await this.backend.readFile(
|
|
3640
|
+
const content = await this.backend.readFile(path2);
|
|
2815
3641
|
if (!content) continue;
|
|
2816
3642
|
for (const line of content.split("\n").filter(Boolean)) {
|
|
2817
3643
|
try {
|
|
@@ -2829,7 +3655,7 @@ var SyncOrchestrator = class {
|
|
|
2829
3655
|
}
|
|
2830
3656
|
this.options.onProgress?.({
|
|
2831
3657
|
phase: "pulling",
|
|
2832
|
-
currentPath:
|
|
3658
|
+
currentPath: path2,
|
|
2833
3659
|
completedFiles: index + 1,
|
|
2834
3660
|
totalFiles: paths.length,
|
|
2835
3661
|
pulledCount: totalPulled
|
|
@@ -2837,13 +3663,13 @@ var SyncOrchestrator = class {
|
|
|
2837
3663
|
}
|
|
2838
3664
|
return totalPulled;
|
|
2839
3665
|
}
|
|
2840
|
-
async uploadFile(
|
|
2841
|
-
const existingContent = await this.backend.readFile(
|
|
3666
|
+
async uploadFile(path2, localSyncRecords) {
|
|
3667
|
+
const existingContent = await this.backend.readFile(path2);
|
|
2842
3668
|
const existing = existingContent ? parseNdjson(existingContent) : /* @__PURE__ */ new Map();
|
|
2843
3669
|
const changedCount = mergeRecords(existing, localSyncRecords);
|
|
2844
3670
|
if (changedCount === 0) return 0;
|
|
2845
3671
|
const content = Array.from(existing.values()).map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
2846
|
-
await this.backend.writeFile(
|
|
3672
|
+
await this.backend.writeFile(path2, content);
|
|
2847
3673
|
return changedCount;
|
|
2848
3674
|
}
|
|
2849
3675
|
async upload() {
|
|
@@ -2851,9 +3677,9 @@ var SyncOrchestrator = class {
|
|
|
2851
3677
|
if (unsynced.length === 0) return 0;
|
|
2852
3678
|
const byPath = /* @__PURE__ */ new Map();
|
|
2853
3679
|
for (const record of unsynced) {
|
|
2854
|
-
const
|
|
2855
|
-
if (!byPath.has(
|
|
2856
|
-
byPath.get(
|
|
3680
|
+
const path2 = getSyncPath(record.ts, this.options.deviceInstanceId);
|
|
3681
|
+
if (!byPath.has(path2)) byPath.set(path2, []);
|
|
3682
|
+
byPath.get(path2).push(record);
|
|
2857
3683
|
}
|
|
2858
3684
|
let totalUploaded = 0;
|
|
2859
3685
|
const uploads = Array.from(byPath.entries());
|
|
@@ -2863,20 +3689,20 @@ var SyncOrchestrator = class {
|
|
|
2863
3689
|
totalFiles: uploads.length,
|
|
2864
3690
|
uploadedCount: 0
|
|
2865
3691
|
});
|
|
2866
|
-
for (const [index, [
|
|
3692
|
+
for (const [index, [path2, localRecords]] of uploads.entries()) {
|
|
2867
3693
|
this.options.onProgress?.({
|
|
2868
3694
|
phase: "uploading",
|
|
2869
|
-
currentPath:
|
|
3695
|
+
currentPath: path2,
|
|
2870
3696
|
completedFiles: index,
|
|
2871
3697
|
totalFiles: uploads.length,
|
|
2872
3698
|
uploadedCount: totalUploaded
|
|
2873
3699
|
});
|
|
2874
3700
|
const localSyncRecords = localRecords.map(mapStatsRecordToSyncRecord);
|
|
2875
|
-
const changedCount = await this.uploadFile(
|
|
3701
|
+
const changedCount = await this.uploadFile(path2, localSyncRecords);
|
|
2876
3702
|
totalUploaded += changedCount;
|
|
2877
3703
|
this.options.onProgress?.({
|
|
2878
3704
|
phase: "uploading",
|
|
2879
|
-
currentPath:
|
|
3705
|
+
currentPath: path2,
|
|
2880
3706
|
completedFiles: index + 1,
|
|
2881
3707
|
totalFiles: uploads.length,
|
|
2882
3708
|
uploadedCount: totalUploaded
|
|
@@ -2915,7 +3741,7 @@ function verifyConsent(storedFingerprint, currentConfig) {
|
|
|
2915
3741
|
import { execFile } from "child_process";
|
|
2916
3742
|
import { promisify } from "util";
|
|
2917
3743
|
import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
|
|
2918
|
-
import { join as
|
|
3744
|
+
import { join as join5 } from "path";
|
|
2919
3745
|
var exec = promisify(execFile);
|
|
2920
3746
|
var GitSyncBackend = class _GitSyncBackend {
|
|
2921
3747
|
repo;
|
|
@@ -2927,7 +3753,7 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
2927
3753
|
this.repo = config.repo;
|
|
2928
3754
|
this.token = config.token;
|
|
2929
3755
|
this.cacheDir = config.cacheDir;
|
|
2930
|
-
this.dataDir =
|
|
3756
|
+
this.dataDir = join5(config.cacheDir, "data");
|
|
2931
3757
|
this.branch = config.branch ?? "main";
|
|
2932
3758
|
}
|
|
2933
3759
|
get remoteUrl() {
|
|
@@ -2959,7 +3785,7 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
2959
3785
|
/** Clone or pull the repo to get latest remote state */
|
|
2960
3786
|
async prepare() {
|
|
2961
3787
|
try {
|
|
2962
|
-
await stat(
|
|
3788
|
+
await stat(join5(this.cacheDir, ".git"));
|
|
2963
3789
|
await this.git(["fetch", "origin", this.branch, "--depth=1"]);
|
|
2964
3790
|
await this.git(["reset", "--hard", `origin/${this.branch}`]);
|
|
2965
3791
|
} catch {
|
|
@@ -2974,17 +3800,17 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
2974
3800
|
}
|
|
2975
3801
|
}
|
|
2976
3802
|
}
|
|
2977
|
-
async readFile(
|
|
3803
|
+
async readFile(path2) {
|
|
2978
3804
|
try {
|
|
2979
|
-
const fullPath =
|
|
3805
|
+
const fullPath = join5(this.dataDir, path2);
|
|
2980
3806
|
return await readFile(fullPath, "utf-8");
|
|
2981
3807
|
} catch {
|
|
2982
3808
|
return null;
|
|
2983
3809
|
}
|
|
2984
3810
|
}
|
|
2985
|
-
async writeFile(
|
|
2986
|
-
const fullPath =
|
|
2987
|
-
await mkdir(
|
|
3811
|
+
async writeFile(path2, content) {
|
|
3812
|
+
const fullPath = join5(this.dataDir, path2);
|
|
3813
|
+
await mkdir(join5(fullPath, ".."), { recursive: true });
|
|
2988
3814
|
await writeFile(fullPath, content, "utf-8");
|
|
2989
3815
|
}
|
|
2990
3816
|
async listFiles() {
|
|
@@ -3018,7 +3844,7 @@ var GitSyncBackend = class _GitSyncBackend {
|
|
|
3018
3844
|
for (const entry of entries) {
|
|
3019
3845
|
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
3020
3846
|
if (entry.isDirectory()) {
|
|
3021
|
-
files.push(...await this.walkDir(
|
|
3847
|
+
files.push(...await this.walkDir(join5(dir, entry.name), relPath));
|
|
3022
3848
|
} else if (entry.name.endsWith(".ndjson")) {
|
|
3023
3849
|
files.push(relPath);
|
|
3024
3850
|
}
|
|
@@ -3045,11 +3871,11 @@ var S3SyncBackend = class {
|
|
|
3045
3871
|
}
|
|
3046
3872
|
});
|
|
3047
3873
|
}
|
|
3048
|
-
getObjectKey(
|
|
3049
|
-
return `${this.prefix}${
|
|
3874
|
+
getObjectKey(path2) {
|
|
3875
|
+
return `${this.prefix}${path2}`;
|
|
3050
3876
|
}
|
|
3051
|
-
async readFile(
|
|
3052
|
-
const key = this.getObjectKey(
|
|
3877
|
+
async readFile(path2) {
|
|
3878
|
+
const key = this.getObjectKey(path2);
|
|
3053
3879
|
try {
|
|
3054
3880
|
const command = new GetObjectCommand({
|
|
3055
3881
|
Bucket: this.bucket,
|
|
@@ -3065,8 +3891,8 @@ var S3SyncBackend = class {
|
|
|
3065
3891
|
throw error;
|
|
3066
3892
|
}
|
|
3067
3893
|
}
|
|
3068
|
-
async writeFile(
|
|
3069
|
-
const key = this.getObjectKey(
|
|
3894
|
+
async writeFile(path2, content) {
|
|
3895
|
+
const key = this.getObjectKey(path2);
|
|
3070
3896
|
const command = new PutObjectCommand({
|
|
3071
3897
|
Bucket: this.bucket,
|
|
3072
3898
|
Key: key,
|
|
@@ -3098,8 +3924,8 @@ var S3SyncBackend = class {
|
|
|
3098
3924
|
} while (continuationToken);
|
|
3099
3925
|
return files.sort();
|
|
3100
3926
|
}
|
|
3101
|
-
async fileExists(
|
|
3102
|
-
const key = this.getObjectKey(
|
|
3927
|
+
async fileExists(path2) {
|
|
3928
|
+
const key = this.getObjectKey(path2);
|
|
3103
3929
|
try {
|
|
3104
3930
|
const command = new HeadObjectCommand({
|
|
3105
3931
|
Bucket: this.bucket,
|
|
@@ -3127,7 +3953,7 @@ function createBackend(config) {
|
|
|
3127
3953
|
return new GitSyncBackend({
|
|
3128
3954
|
repo: sync.repo,
|
|
3129
3955
|
token,
|
|
3130
|
-
cacheDir:
|
|
3956
|
+
cacheDir: join6(AIUSAGE_DIR, "sync-repo")
|
|
3131
3957
|
});
|
|
3132
3958
|
}
|
|
3133
3959
|
if (sync.backend === "s3") {
|
|
@@ -3348,6 +4174,7 @@ var RuntimeSettingsController = class {
|
|
|
3348
4174
|
|
|
3349
4175
|
// src/commands/serve.ts
|
|
3350
4176
|
var MAX_PORT_ATTEMPTS = 10;
|
|
4177
|
+
var PORT_FILE = join7(AIUSAGE_DIR, ".serve-port");
|
|
3351
4178
|
var MIME_TYPES = {
|
|
3352
4179
|
".html": "text/html",
|
|
3353
4180
|
".js": "application/javascript",
|
|
@@ -3399,9 +4226,9 @@ function serve(options) {
|
|
|
3399
4226
|
onConfigUpdated: () => runtimeSettings.reload()
|
|
3400
4227
|
});
|
|
3401
4228
|
const webBuildDir = (() => {
|
|
3402
|
-
const prodDir =
|
|
3403
|
-
if (
|
|
3404
|
-
return
|
|
4229
|
+
const prodDir = join7(dirname(fileURLToPath(import.meta.url)), "web");
|
|
4230
|
+
if (existsSync6(prodDir)) return prodDir;
|
|
4231
|
+
return join7(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
|
|
3405
4232
|
})();
|
|
3406
4233
|
const server = http2.createServer(async (req, res) => {
|
|
3407
4234
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -3409,19 +4236,19 @@ function serve(options) {
|
|
|
3409
4236
|
apiServer.emit("request", req, res);
|
|
3410
4237
|
return;
|
|
3411
4238
|
}
|
|
3412
|
-
if (
|
|
3413
|
-
let filePath =
|
|
4239
|
+
if (existsSync6(webBuildDir)) {
|
|
4240
|
+
let filePath = join7(webBuildDir, url.pathname);
|
|
3414
4241
|
try {
|
|
3415
4242
|
if (statSync2(filePath).isDirectory()) {
|
|
3416
|
-
filePath =
|
|
4243
|
+
filePath = join7(filePath, "index.html");
|
|
3417
4244
|
}
|
|
3418
4245
|
} catch {
|
|
3419
4246
|
}
|
|
3420
|
-
if (!
|
|
3421
|
-
filePath =
|
|
4247
|
+
if (!existsSync6(filePath)) {
|
|
4248
|
+
filePath = join7(webBuildDir, "index.html");
|
|
3422
4249
|
}
|
|
3423
4250
|
try {
|
|
3424
|
-
const content =
|
|
4251
|
+
const content = readFileSync6(filePath);
|
|
3425
4252
|
const ext = extname2(filePath);
|
|
3426
4253
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
3427
4254
|
res.writeHead(200, { "Content-Type": contentType });
|
|
@@ -3441,6 +4268,7 @@ function serve(options) {
|
|
|
3441
4268
|
};
|
|
3442
4269
|
server.on("listening", () => {
|
|
3443
4270
|
started = true;
|
|
4271
|
+
writeFileSync4(PORT_FILE, String(currentPort), "utf-8");
|
|
3444
4272
|
console.log(`aiusage serve listening on http://localhost:${currentPort}`);
|
|
3445
4273
|
});
|
|
3446
4274
|
server.on("error", (error) => {
|
|
@@ -3456,14 +4284,22 @@ function serve(options) {
|
|
|
3456
4284
|
throw error;
|
|
3457
4285
|
});
|
|
3458
4286
|
listenOnPort(options.port);
|
|
4287
|
+
const cleanup = () => {
|
|
4288
|
+
try {
|
|
4289
|
+
unlinkSync(PORT_FILE);
|
|
4290
|
+
} catch {
|
|
4291
|
+
}
|
|
4292
|
+
};
|
|
3459
4293
|
process.on("SIGINT", () => {
|
|
3460
4294
|
console.log("\nShutting down...");
|
|
4295
|
+
cleanup();
|
|
3461
4296
|
runtimeSettings.stop();
|
|
3462
4297
|
server.close(() => {
|
|
3463
4298
|
process.exit(0);
|
|
3464
4299
|
});
|
|
3465
4300
|
});
|
|
3466
4301
|
process.on("SIGTERM", () => {
|
|
4302
|
+
cleanup();
|
|
3467
4303
|
runtimeSettings.stop();
|
|
3468
4304
|
server.close(() => {
|
|
3469
4305
|
process.exit(0);
|
|
@@ -3472,13 +4308,13 @@ function serve(options) {
|
|
|
3472
4308
|
}
|
|
3473
4309
|
|
|
3474
4310
|
// src/commands/init.ts
|
|
3475
|
-
import { platform as
|
|
4311
|
+
import { platform as platform4 } from "os";
|
|
3476
4312
|
function runInit(options) {
|
|
3477
4313
|
const existingConfig = loadConfig();
|
|
3478
4314
|
if (options.backend === "skip" || !options.backend) {
|
|
3479
4315
|
const config = {
|
|
3480
4316
|
device: options.device ?? existingConfig?.device,
|
|
3481
|
-
platform: existingConfig?.platform ??
|
|
4317
|
+
platform: existingConfig?.platform ?? platform4(),
|
|
3482
4318
|
retentionDays: existingConfig?.retentionDays,
|
|
3483
4319
|
parseInterval: existingConfig?.parseInterval,
|
|
3484
4320
|
dashboardPollInterval: existingConfig?.dashboardPollInterval,
|
|
@@ -3501,7 +4337,7 @@ function runInit(options) {
|
|
|
3501
4337
|
credentialRef: `github/${options.repo}/token`
|
|
3502
4338
|
},
|
|
3503
4339
|
device: options.device ?? existingConfig?.device,
|
|
3504
|
-
platform: existingConfig?.platform ??
|
|
4340
|
+
platform: existingConfig?.platform ?? platform4(),
|
|
3505
4341
|
retentionDays: existingConfig?.retentionDays,
|
|
3506
4342
|
parseInterval: existingConfig?.parseInterval,
|
|
3507
4343
|
dashboardPollInterval: existingConfig?.dashboardPollInterval,
|
|
@@ -3547,7 +4383,7 @@ Consent recorded for schema v1.`
|
|
|
3547
4383
|
credentialRef: `s3/${options.bucket}/accessKeyId`
|
|
3548
4384
|
},
|
|
3549
4385
|
device: options.device ?? existingConfig?.device,
|
|
3550
|
-
platform: existingConfig?.platform ??
|
|
4386
|
+
platform: existingConfig?.platform ?? platform4(),
|
|
3551
4387
|
retentionDays: existingConfig?.retentionDays,
|
|
3552
4388
|
parseInterval: existingConfig?.parseInterval,
|
|
3553
4389
|
dashboardPollInterval: existingConfig?.dashboardPollInterval,
|
|
@@ -3672,9 +4508,9 @@ function generateSummary(db, options) {
|
|
|
3672
4508
|
}
|
|
3673
4509
|
|
|
3674
4510
|
// src/commands/status.ts
|
|
3675
|
-
import { statSync as statSync3, existsSync as
|
|
4511
|
+
import { statSync as statSync3, existsSync as existsSync7 } from "fs";
|
|
3676
4512
|
import { hostname as hostname3 } from "os";
|
|
3677
|
-
import { join as
|
|
4513
|
+
import { join as join8 } from "path";
|
|
3678
4514
|
function formatFileSize(bytes) {
|
|
3679
4515
|
if (bytes < 1024) return `${bytes} B`;
|
|
3680
4516
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -3692,9 +4528,9 @@ function generateStatus(db) {
|
|
|
3692
4528
|
const schemaVersion = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get()?.version ?? 0;
|
|
3693
4529
|
const tableCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get()?.count ?? 0;
|
|
3694
4530
|
const viewCount = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'view'").get()?.count ?? 0;
|
|
3695
|
-
const sizePath =
|
|
4531
|
+
const sizePath = join8(AIUSAGE_DIR, "cache.db");
|
|
3696
4532
|
let databaseSize = "0 B";
|
|
3697
|
-
if (
|
|
4533
|
+
if (existsSync7(sizePath)) {
|
|
3698
4534
|
try {
|
|
3699
4535
|
const stat2 = statSync3(sizePath);
|
|
3700
4536
|
databaseSize = formatFileSize(stat2.size);
|
|
@@ -3769,16 +4605,16 @@ function exportData(db, format) {
|
|
|
3769
4605
|
}
|
|
3770
4606
|
|
|
3771
4607
|
// src/commands/reset.ts
|
|
3772
|
-
import { unlinkSync, existsSync as
|
|
3773
|
-
import { join as
|
|
4608
|
+
import { unlinkSync as unlinkSync2, existsSync as existsSync8 } from "fs";
|
|
4609
|
+
import { join as join9 } from "path";
|
|
3774
4610
|
function runReset(db) {
|
|
3775
4611
|
const recordsResult = db.prepare("DELETE FROM records").run();
|
|
3776
4612
|
const toolCallsResult = db.prepare("DELETE FROM tool_calls").run();
|
|
3777
4613
|
const syncedResult = db.prepare("DELETE FROM synced_records").run();
|
|
3778
|
-
const watermarkPath =
|
|
4614
|
+
const watermarkPath = join9(AIUSAGE_DIR, "watermark.json");
|
|
3779
4615
|
let watermarkRemoved = false;
|
|
3780
|
-
if (
|
|
3781
|
-
|
|
4616
|
+
if (existsSync8(watermarkPath)) {
|
|
4617
|
+
unlinkSync2(watermarkPath);
|
|
3782
4618
|
watermarkRemoved = true;
|
|
3783
4619
|
}
|
|
3784
4620
|
return {
|
|
@@ -3858,6 +4694,59 @@ var ProgressReporter = class {
|
|
|
3858
4694
|
}
|
|
3859
4695
|
};
|
|
3860
4696
|
|
|
4697
|
+
// src/commands/widget.ts
|
|
4698
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
4699
|
+
import { spawn, execSync as execSync2 } from "child_process";
|
|
4700
|
+
import { join as join10 } from "path";
|
|
4701
|
+
var WIDGET_PID_PATH = join10(AIUSAGE_DIR, "widget.pid");
|
|
4702
|
+
async function launchWidget() {
|
|
4703
|
+
if (isWidgetRunning()) {
|
|
4704
|
+
console.log("aiusage widget is already running in the system tray.");
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
const electronBin = resolveElectronBin();
|
|
4708
|
+
if (!electronBin) {
|
|
4709
|
+
console.error(
|
|
4710
|
+
"Widget not installed. Run: npm install -g @juliantanx/aiusage-widget"
|
|
4711
|
+
);
|
|
4712
|
+
process.exit(1);
|
|
4713
|
+
}
|
|
4714
|
+
const child = spawn(electronBin, [], {
|
|
4715
|
+
detached: true,
|
|
4716
|
+
stdio: "ignore",
|
|
4717
|
+
shell: false
|
|
4718
|
+
});
|
|
4719
|
+
child.unref();
|
|
4720
|
+
console.log("aiusage widget started.");
|
|
4721
|
+
}
|
|
4722
|
+
function isWidgetRunning() {
|
|
4723
|
+
if (!existsSync9(WIDGET_PID_PATH)) return false;
|
|
4724
|
+
let pid;
|
|
4725
|
+
try {
|
|
4726
|
+
pid = parseInt(readFileSync7(WIDGET_PID_PATH, "utf-8").trim(), 10);
|
|
4727
|
+
} catch {
|
|
4728
|
+
return false;
|
|
4729
|
+
}
|
|
4730
|
+
if (isNaN(pid)) return false;
|
|
4731
|
+
try {
|
|
4732
|
+
process.kill(pid, 0);
|
|
4733
|
+
return true;
|
|
4734
|
+
} catch (err) {
|
|
4735
|
+
return err.code !== "ESRCH";
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
function resolveElectronBin() {
|
|
4739
|
+
try {
|
|
4740
|
+
const result = execSync2(
|
|
4741
|
+
process.platform === "win32" ? "where aiusage-widget" : "which aiusage-widget",
|
|
4742
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4743
|
+
).trim();
|
|
4744
|
+
return result || null;
|
|
4745
|
+
} catch {
|
|
4746
|
+
return null;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
|
|
3861
4750
|
// src/db/index.ts
|
|
3862
4751
|
import Database2 from "better-sqlite3";
|
|
3863
4752
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -4098,18 +4987,18 @@ function initializeDatabase(db) {
|
|
|
4098
4987
|
applyPragmas(db);
|
|
4099
4988
|
runMigrations(db);
|
|
4100
4989
|
}
|
|
4101
|
-
function createDatabase(
|
|
4102
|
-
mkdirSync2(dirname2(
|
|
4103
|
-
const db = new Database2(
|
|
4990
|
+
function createDatabase(path2) {
|
|
4991
|
+
mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
|
|
4992
|
+
const db = new Database2(path2);
|
|
4104
4993
|
initializeDatabase(db);
|
|
4105
4994
|
return db;
|
|
4106
4995
|
}
|
|
4107
4996
|
|
|
4108
4997
|
// src/cli.ts
|
|
4109
|
-
import { join as
|
|
4110
|
-
var DB_PATH =
|
|
4998
|
+
import { join as join11 } from "path";
|
|
4999
|
+
var DB_PATH = join11(AIUSAGE_DIR, "cache.db");
|
|
4111
5000
|
var program = new Command();
|
|
4112
|
-
program.name("aiusage").version(true ? "1.3.
|
|
5001
|
+
program.name("aiusage").version(true ? "1.3.1" : "dev").description("CLI tool for AI usage statistics");
|
|
4113
5002
|
program.action(() => {
|
|
4114
5003
|
const db = createDatabase(DB_PATH);
|
|
4115
5004
|
const state = getState(AIUSAGE_DIR);
|
|
@@ -4136,7 +5025,7 @@ program.action(() => {
|
|
|
4136
5025
|
}
|
|
4137
5026
|
db.close();
|
|
4138
5027
|
});
|
|
4139
|
-
program.command("summary").description("Show usage summary").option("--week", "Show this week").option("--month", "Show this month").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--device <id>", "Filter by device instance ID").option("--tool <tool>", "Filter by tool type (claude-code|codex|openclaw|opencode|hermes|qoder)").action((options) => {
|
|
5028
|
+
program.command("summary").description("Show usage summary").option("--week", "Show this week").option("--month", "Show this month").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("--device <id>", "Filter by device instance ID").option("--tool <tool>", "Filter by tool type (claude-code|codex|openclaw|opencode|hermes|qoder|cursor)").action((options) => {
|
|
4140
5029
|
const db = createDatabase(DB_PATH);
|
|
4141
5030
|
const state = getState(AIUSAGE_DIR);
|
|
4142
5031
|
const summary = generateSummary(db, {
|
|
@@ -4185,14 +5074,14 @@ program.command("export").description("Export data").requiredOption("--format <f
|
|
|
4185
5074
|
const db = createDatabase(DB_PATH);
|
|
4186
5075
|
const data = exportData(db, format);
|
|
4187
5076
|
if (options.output) {
|
|
4188
|
-
|
|
5077
|
+
writeFileSync5(options.output, data, "utf-8");
|
|
4189
5078
|
console.log(`Exported to ${options.output}`);
|
|
4190
5079
|
} else {
|
|
4191
5080
|
console.log(data);
|
|
4192
5081
|
}
|
|
4193
5082
|
db.close();
|
|
4194
5083
|
});
|
|
4195
|
-
program.command("parse").description("Parse AI tool session logs").option("--tool <tool>", "Specific tool to parse (claude-code|codex|openclaw|opencode|hermes|qoder)").option("--progress", "Show real-time progress").action(async (options) => {
|
|
5084
|
+
program.command("parse").description("Parse AI tool session logs").option("--tool <tool>", "Specific tool to parse (claude-code|codex|openclaw|opencode|hermes|qoder|cursor)").option("--progress", "Show real-time progress").action(async (options) => {
|
|
4196
5085
|
const db = createDatabase(DB_PATH);
|
|
4197
5086
|
const reporter = options.progress ? new ProgressReporter() : void 0;
|
|
4198
5087
|
try {
|
|
@@ -4272,6 +5161,9 @@ program.command("sync").description("Sync data with cloud storage").action(async
|
|
|
4272
5161
|
}
|
|
4273
5162
|
db.close();
|
|
4274
5163
|
});
|
|
5164
|
+
program.command("widget").description("Start the system tray widget").action(async () => {
|
|
5165
|
+
await launchWidget();
|
|
5166
|
+
});
|
|
4275
5167
|
|
|
4276
5168
|
// src/index.ts
|
|
4277
5169
|
program.parse(process.argv);
|