@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -27
  3. package/dist/index.js +1051 -123
  4. package/dist/web/_app/immutable/assets/10.BfaLSE4e.css +1 -0
  5. package/dist/web/_app/immutable/assets/13.CxRLR3o7.css +1 -0
  6. package/dist/web/_app/immutable/assets/2.BLunjqo3.css +1 -0
  7. package/dist/web/_app/immutable/assets/5.8Efnpu8u.css +1 -0
  8. package/dist/web/_app/immutable/assets/7.BeGOLIpG.css +1 -0
  9. package/dist/web/_app/immutable/assets/8.DMukHCtv.css +1 -0
  10. package/dist/web/_app/immutable/assets/9.DpWloLoX.css +1 -0
  11. package/dist/web/_app/immutable/assets/_page.8Efnpu8u.css +1 -0
  12. package/dist/web/_app/immutable/assets/_page.BeGOLIpG.css +1 -0
  13. package/dist/web/_app/immutable/assets/_page.BfaLSE4e.css +1 -0
  14. package/dist/web/_app/immutable/assets/_page.CYETnzhl.css +1 -0
  15. package/dist/web/_app/immutable/assets/_page.CxRLR3o7.css +1 -0
  16. package/dist/web/_app/immutable/assets/_page.DDYvQrAf.css +1 -0
  17. package/dist/web/_app/immutable/assets/_page.DpWloLoX.css +1 -0
  18. package/dist/web/_app/immutable/chunks/ToolSelector.4cKBP-DC.js +2 -0
  19. package/dist/web/_app/immutable/chunks/api.BuM0qEja.js +1 -0
  20. package/dist/web/_app/immutable/chunks/entry.rSFvFffH.js +3 -0
  21. package/dist/web/_app/immutable/chunks/{index.wix8pgil.js → index.BmCbw0jM.js} +1 -1
  22. package/dist/web/_app/immutable/chunks/{stores.C3ZtRBEM.js → stores.Ce9c-jZZ.js} +1 -1
  23. package/dist/web/_app/immutable/entry/app.UR40BQ-u.js +2 -0
  24. package/dist/web/_app/immutable/entry/start.C0n6bEuf.js +1 -0
  25. package/dist/web/_app/immutable/nodes/0.btDtSa8k.js +1 -0
  26. package/dist/web/_app/immutable/nodes/{1.CNbcoUIj.js → 1.BAehr4TO.js} +1 -1
  27. package/dist/web/_app/immutable/nodes/10.CXxg0LZ5.js +1 -0
  28. package/dist/web/_app/immutable/nodes/11.D3gLypzx.js +1 -0
  29. package/dist/web/_app/immutable/nodes/12.BJYMQ3v1.js +1 -0
  30. package/dist/web/_app/immutable/nodes/13.uRTNTtbn.js +1 -0
  31. package/dist/web/_app/immutable/nodes/2.CdevLrEl.js +3 -0
  32. package/dist/web/_app/immutable/nodes/3.SOGeLIrs.js +1 -0
  33. package/dist/web/_app/immutable/nodes/4.B5ZJKhP_.js +1 -0
  34. package/dist/web/_app/immutable/nodes/5.D3sV9sQw.js +1 -0
  35. package/dist/web/_app/immutable/nodes/{7.bOnMUP4_.js → 6.DWlBNHmu.js} +2 -2
  36. package/dist/web/_app/immutable/nodes/7.BbLG55tv.js +1 -0
  37. package/dist/web/_app/immutable/nodes/8.BWHIAsQs.js +1 -0
  38. package/dist/web/_app/immutable/nodes/9.BXVXXU3Z.js +1 -0
  39. package/dist/web/_app/version.json +1 -1
  40. package/dist/web/index.html +7 -7
  41. package/package.json +8 -3
  42. package/dist/web/_app/immutable/assets/12.DAmfvZ5J.css +0 -1
  43. package/dist/web/_app/immutable/assets/2.G6nSchsK.css +0 -1
  44. package/dist/web/_app/immutable/assets/4.CnA4GaE5.css +0 -1
  45. package/dist/web/_app/immutable/assets/6.CrsKlz6Y.css +0 -1
  46. package/dist/web/_app/immutable/assets/8.DDodt1Wn.css +0 -1
  47. package/dist/web/_app/immutable/assets/9.ApFevNUr.css +0 -1
  48. package/dist/web/_app/immutable/assets/_page.ApFevNUr.css +0 -1
  49. package/dist/web/_app/immutable/assets/_page.CnA4GaE5.css +0 -1
  50. package/dist/web/_app/immutable/assets/_page.CrsKlz6Y.css +0 -1
  51. package/dist/web/_app/immutable/assets/_page.DAmfvZ5J.css +0 -1
  52. package/dist/web/_app/immutable/assets/_page.DDodt1Wn.css +0 -1
  53. package/dist/web/_app/immutable/assets/_page.DewAUGiJ.css +0 -1
  54. package/dist/web/_app/immutable/chunks/ToolSelector.CnQ-eszE.js +0 -2
  55. package/dist/web/_app/immutable/chunks/api.OwJBmtEN.js +0 -1
  56. package/dist/web/_app/immutable/chunks/entry.DWbp2XzC.js +0 -3
  57. package/dist/web/_app/immutable/chunks/i18n.BOq44pYI.js +0 -1
  58. package/dist/web/_app/immutable/entry/app.7Tmcb_qs.js +0 -2
  59. package/dist/web/_app/immutable/entry/start.CuMG7ijt.js +0 -1
  60. package/dist/web/_app/immutable/nodes/0.GiWuXD22.js +0 -1
  61. package/dist/web/_app/immutable/nodes/10.DgPr3L-S.js +0 -1
  62. package/dist/web/_app/immutable/nodes/11.TrX-RGQX.js +0 -1
  63. package/dist/web/_app/immutable/nodes/12.ZW7n1TLF.js +0 -1
  64. package/dist/web/_app/immutable/nodes/2.D0gEY9nh.js +0 -3
  65. package/dist/web/_app/immutable/nodes/3.CcnPu3DZ.js +0 -1
  66. package/dist/web/_app/immutable/nodes/4.CCaCZRNi.js +0 -13
  67. package/dist/web/_app/immutable/nodes/5.Bxx7TNGK.js +0 -1
  68. package/dist/web/_app/immutable/nodes/6.8zY9a_YX.js +0 -1
  69. package/dist/web/_app/immutable/nodes/8.01X9rJRT.js +0 -1
  70. package/dist/web/_app/immutable/nodes/9.DabFUxt3.js +0 -1
  71. /package/dist/web/_app/immutable/assets/{10.D4XIqeEN.css → 11.D4XIqeEN.css} +0 -0
  72. /package/dist/web/_app/immutable/assets/{11.CR85TxeS.css → 12.CR85TxeS.css} +0 -0
  73. /package/dist/web/_app/immutable/assets/{5.D4N7n6x9.css → 4.D4N7n6x9.css} +0 -0
  74. /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 writeFileSync4 } from "fs";
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 readFileSync5, existsSync as existsSync5, statSync as statSync2 } from "fs";
10
- import { join as join6, extname as extname2, dirname } from "path";
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 path2 from "path";
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
- const rawName = block.name ?? "";
343
- const cleanName = rawName.replace(/[=:"'{\[\s].*$/s, "").replace(/[^a-zA-Z0-9_-]/g, "");
344
- if (!cleanName) {
345
- callIndex++;
346
- continue;
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, cleanName, ts, callIndex),
357
+ id: generateToolCallId(record.id, storedName, ts, callIndex),
350
358
  recordId: record.id,
351
- name: cleanName,
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?.name ?? "unknown",
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(path3) {
970
- this.path = path3;
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: platform4, now, cursor, exchangeRate } = options;
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: platform4
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: platform4, now, cursor, exchangeRate } = options;
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: platform4
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: platform4, now, cursor, exchangeRate } = options;
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: platform4
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
- name: r.name,
2145
- count: r.count,
2146
- percentage: Math.round(r.count / total * 1e3) / 10
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 WHERE 1=1 ${dr.where} ${df.localOnly ? LOCAL_ONLY_FILTER : ""} ${tf.where}
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
- SUM(input_tokens) AS inputTokens,
2179
- SUM(output_tokens) AS outputTokens,
2180
- SUM(cache_read_tokens) AS cacheReadTokens,
2181
- SUM(cache_write_tokens) AS cacheWriteTokens,
2182
- SUM(cost) AS cost
2183
- FROM records
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
- const project = row.cwd ? path2.basename(row.cwd) : extractProject(row.source_file);
2218
- if (!projectMap[project]) projectMap[project] = { sessions: 0, tokens: 0, cost: 0 };
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 || platform2();
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 = platform2();
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 join5 } from "path";
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, path3] of paths.entries()) {
3633
+ for (const [index, path2] of paths.entries()) {
2807
3634
  this.options.onProgress?.({
2808
3635
  phase: "pulling",
2809
- currentPath: path3,
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(path3);
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: path3,
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(path3, localSyncRecords) {
2841
- const existingContent = await this.backend.readFile(path3);
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(path3, content);
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 path3 = getSyncPath(record.ts, this.options.deviceInstanceId);
2855
- if (!byPath.has(path3)) byPath.set(path3, []);
2856
- byPath.get(path3).push(record);
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, [path3, localRecords]] of uploads.entries()) {
3693
+ for (const [index, [path2, localRecords]] of uploads.entries()) {
2867
3694
  this.options.onProgress?.({
2868
3695
  phase: "uploading",
2869
- currentPath: path3,
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(path3, localSyncRecords);
3702
+ const changedCount = await this.uploadFile(path2, localSyncRecords);
2876
3703
  totalUploaded += changedCount;
2877
3704
  this.options.onProgress?.({
2878
3705
  phase: "uploading",
2879
- currentPath: path3,
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 join4 } from "path";
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 = join4(config.cacheDir, "data");
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(join4(this.cacheDir, ".git"));
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(path3) {
3804
+ async readFile(path2) {
2978
3805
  try {
2979
- const fullPath = join4(this.dataDir, path3);
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(path3, content) {
2986
- const fullPath = join4(this.dataDir, path3);
2987
- await mkdir(join4(fullPath, ".."), { recursive: true });
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(join4(dir, entry.name), relPath));
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(path3) {
3049
- return `${this.prefix}${path3}`;
3875
+ getObjectKey(path2) {
3876
+ return `${this.prefix}${path2}`;
3050
3877
  }
3051
- async readFile(path3) {
3052
- const key = this.getObjectKey(path3);
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(path3, content) {
3069
- const key = this.getObjectKey(path3);
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(path3) {
3102
- const key = this.getObjectKey(path3);
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: join5(AIUSAGE_DIR, "sync-repo")
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 = join6(dirname(fileURLToPath(import.meta.url)), "web");
3403
- if (existsSync5(prodDir)) return prodDir;
3404
- return join6(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "web", "build");
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 (existsSync5(webBuildDir)) {
3413
- let filePath = join6(webBuildDir, url.pathname);
4240
+ if (existsSync6(webBuildDir)) {
4241
+ let filePath = join7(webBuildDir, url.pathname);
3414
4242
  try {
3415
4243
  if (statSync2(filePath).isDirectory()) {
3416
- filePath = join6(filePath, "index.html");
4244
+ filePath = join7(filePath, "index.html");
3417
4245
  }
3418
4246
  } catch {
3419
4247
  }
3420
- if (!existsSync5(filePath)) {
3421
- filePath = join6(webBuildDir, "index.html");
4248
+ if (!existsSync6(filePath)) {
4249
+ filePath = join7(webBuildDir, "index.html");
3422
4250
  }
3423
4251
  try {
3424
- const content = readFileSync5(filePath);
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 platform3 } from "os";
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 ?? platform3(),
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 ?? platform3(),
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 ?? platform3(),
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 existsSync6 } from "fs";
4512
+ import { statSync as statSync3, existsSync as existsSync7 } from "fs";
3676
4513
  import { hostname as hostname3 } from "os";
3677
- import { join as join7 } from "path";
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 = join7(AIUSAGE_DIR, "cache.db");
4532
+ const sizePath = join8(AIUSAGE_DIR, "cache.db");
3696
4533
  let databaseSize = "0 B";
3697
- if (existsSync6(sizePath)) {
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 existsSync7 } from "fs";
3773
- import { join as join8 } from "path";
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 = join8(AIUSAGE_DIR, "watermark.json");
4615
+ const watermarkPath = join9(AIUSAGE_DIR, "watermark.json");
3779
4616
  let watermarkRemoved = false;
3780
- if (existsSync7(watermarkPath)) {
3781
- unlinkSync(watermarkPath);
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(path3) {
4102
- mkdirSync2(dirname2(path3), { recursive: true, mode: 448 });
4103
- const db = new Database2(path3);
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 join9 } from "path";
4110
- var DB_PATH = join9(AIUSAGE_DIR, "cache.db");
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.0" : "dev").description("CLI tool for AI usage statistics");
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
- writeFileSync4(options.output, data, "utf-8");
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);