@juliantanx/aiusage 1.3.0 → 1.3.1

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