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