tokentracker-cli 0.12.0 → 0.13.0

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.
@@ -0,0 +1,4 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M512 0H0V512H512V0Z" fill="black"/>
3
+ <path d="M512 512H0V0H512V512ZM322.783 322.784H278.261V392.747L308.472 422.958H378.435V378.437H322.782L322.783 322.784ZM422.957 308.474L392.746 278.263H322.783V322.784H378.435L378.435 378.437H422.957L422.957 308.474ZM233.739 278.263H189.217V322.784H233.739V278.263ZM89.0435 392.747L119.254 422.958H233.739V378.437H133.565V278.263H89.043L89.0435 392.747ZM372.538 189.217V119.254L342.327 89.0435H278.261V133.565H328.017V189.217H278.261V233.739H422.957V189.217H372.538ZM133.565 89.0435H89.0435V233.739H133.565V183.652H189.218V233.739H233.74V183.652L189.218 139.13H133.565V89.0435ZM233.739 89.0435H189.217L189.218 139.13H233.739V89.0435Z" fill="#FAF74F"/>
4
+ </svg>
@@ -210,7 +210,7 @@
210
210
  ]
211
211
  }
212
212
  </script>
213
- <script type="module" crossorigin src="/assets/main-CjKIAPGE.js"></script>
213
+ <script type="module" crossorigin src="/assets/main-Dc116CZl.js"></script>
214
214
  <link rel="stylesheet" crossorigin href="/assets/main-B-qohcBn.css">
215
215
  </head>
216
216
  <body>
@@ -51,7 +51,7 @@
51
51
  "description": "Shareable Token Tracker dashboard snapshot."
52
52
  }
53
53
  </script>
54
- <script type="module" crossorigin src="/assets/main-CjKIAPGE.js"></script>
54
+ <script type="module" crossorigin src="/assets/main-Dc116CZl.js"></script>
55
55
  <link rel="stylesheet" crossorigin href="/assets/main-B-qohcBn.css">
56
56
  </head>
57
57
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokentracker-cli",
3
- "version": "0.12.0",
4
- "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, oh-my-pi, pi, Craft Agents)",
3
+ "version": "0.13.0",
4
+ "description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code)",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
7
7
  "tokentracker-cli": "bin/tracker.js",
@@ -459,6 +459,34 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
459
459
  }
460
460
  }
461
461
 
462
+ // Kilo CLI (kilo.ai @kilocode/plugin): passive reader — no hook installation
463
+ // needed. Reuses OpenCode-fork SQLite schema at ~/.local/share/kilo/kilo.db
464
+ // (override via KILO_HOME).
465
+ {
466
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
467
+ const kiloHome = process.env.KILO_HOME || path.join(xdgDataHome, "kilo");
468
+ const kiloDbPath = path.join(kiloHome, "kilo.db");
469
+ if (fssync.existsSync(kiloDbPath)) {
470
+ summary.push({ label: "Kilo CLI", status: "detected", detail: "Passive reader (no hook needed)" });
471
+ }
472
+ }
473
+
474
+ // Kilo Code VS Code extension (kilocode.kilo-code): passive reader — no hook
475
+ // installation needed. Scans ui_messages.json under every detected VS Code-
476
+ // family install (Code, Cursor, CodeBuddy, Windsurf, …).
477
+ {
478
+ const { resolveKilocodeTaskFiles } = require("../lib/rollout");
479
+ const taskFiles = resolveKilocodeTaskFiles(process.env);
480
+ if (taskFiles.length > 0) {
481
+ const ides = Array.from(new Set(taskFiles.map((t) => t.ide))).join(", ");
482
+ summary.push({
483
+ label: "Kilo Code (VS Code extension)",
484
+ status: "detected",
485
+ detail: `Passive reader · ${taskFiles.length} task${taskFiles.length !== 1 ? "s" : ""} in ${ides}`,
486
+ });
487
+ }
488
+ }
489
+
462
490
  // CodeBuddy: Claude-Code fork. Install the SessionEnd hook so finished
463
491
  // sessions trigger notify.cjs → tracker sync; passive scan still runs as a
464
492
  // safety net for sessions that don't fire SessionEnd cleanly.
@@ -46,6 +46,7 @@ const {
46
46
  piAgentDirCollidesWithOmp,
47
47
  resolveCraftSessionFiles,
48
48
  resolveCraftConfigDir,
49
+ resolveKilocodeTaskFiles,
49
50
  } = require("../lib/rollout");
50
51
 
51
52
  async function cmdStatus(argv = []) {
@@ -200,6 +201,17 @@ async function cmdStatus(argv = []) {
200
201
  const craftInstalled = fssync.existsSync(craftConfigDir);
201
202
  const craftFiles = craftInstalled ? resolveCraftSessionFiles(process.env) : [];
202
203
 
204
+ // Kilo CLI (kilo.ai @kilocode/plugin) — passive scan of kilo.db.
205
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
206
+ const kiloHome = process.env.KILO_HOME || path.join(xdgDataHome, "kilo");
207
+ const kiloDbPath = path.join(kiloHome, "kilo.db");
208
+ const kiloInstalled = fssync.existsSync(kiloDbPath);
209
+
210
+ // Kilo Code VS Code extension — passive scan of all VS Code-family
211
+ // globalStorage/kilocode.kilo-code/tasks/ ui_messages.json files.
212
+ const kilocodeTaskFiles = resolveKilocodeTaskFiles(process.env);
213
+ const kilocodeInstalled = kilocodeTaskFiles.length > 0;
214
+
203
215
  const copilotToken = readCopilotOauthToken({ home });
204
216
  const copilotOtel = describeCopilotOtelStatus({ home, env: process.env });
205
217
  const copilotLines = formatCopilotLines({
@@ -247,6 +259,12 @@ async function cmdStatus(argv = []) {
247
259
  craftInstalled
248
260
  ? `- Craft Agents: passive reader (${craftFiles.length} session jsonl file${craftFiles.length !== 1 ? "s" : ""} found)`
249
261
  : null,
262
+ kiloInstalled
263
+ ? `- Kilo CLI: passive reader (${kiloDbPath})`
264
+ : null,
265
+ kilocodeInstalled
266
+ ? `- Kilo Code (VS Code extension): passive reader (${kilocodeTaskFiles.length} task${kilocodeTaskFiles.length !== 1 ? "s" : ""} across ${new Set(kilocodeTaskFiles.map((t) => t.ide)).size} IDE${new Set(kilocodeTaskFiles.map((t) => t.ide)).size !== 1 ? "s" : ""})`
267
+ : null,
250
268
  ...copilotLines,
251
269
  ...subscriptionLines,
252
270
  "",
@@ -40,9 +40,12 @@ const {
40
40
  resolveKiroCliSessionFiles,
41
41
  resolveKiroCliDbPath,
42
42
  parseKiroCliIncremental,
43
+ resolveKilocodeTaskFiles,
44
+ parseKilocodeIncremental,
43
45
  bucketKey,
44
46
  totalsKey,
45
47
  groupBucketKey,
48
+ claudeMessageDedupKey,
46
49
  } = require("../lib/rollout");
47
50
  const { computeClaudeGroundTruthBuckets } = require("../lib/claude-categorizer");
48
51
  const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress");
@@ -83,7 +86,16 @@ const CLAUDE_MEM_OBSERVER_PATH_SEGMENT = "--claude-mem-observer-sessions";
83
86
  // Project Usage panel stayed inflated. v3 drops every claude /
84
87
  // claude-mem row from project.queue.jsonl too, and resets the
85
88
  // matching cursors.projectHourly + project.queue.state offset.
86
- const CLAUDE_GROUND_TRUTH_REPAIR_KEY = "claudeGroundTruthRepair_2026_05_v3";
89
+ // v4 fixes the dedup short-circuit (issue #64): v3's ground-truth scan
90
+ // itself used `if (msgId && reqId)` to build the dedup key, which silently
91
+ // disabled dedup for any provider whose jsonl entries lack `requestId`
92
+ // (DeepSeek/Kimi/Mimo/MiniMax anthropic-compatible endpoints, plus Claude
93
+ // Code's sub-agent / thinking transport paths). The repaired ground truth
94
+ // was therefore inflated by 1.6–3.7x on those providers — v3 left it that
95
+ // way. v4 re-runs the same five-step atomic repair against the corrected
96
+ // `claudeMessageDedupKey()` (msgId is globally unique on its own per the
97
+ // Anthropic protocol, so the reqId requirement was always unnecessary).
98
+ const CLAUDE_GROUND_TRUTH_REPAIR_KEY = "claudeGroundTruthRepair_2026_05_v4";
87
99
 
88
100
  async function cmdSync(argv) {
89
101
  const opts = parseArgs(argv);
@@ -123,6 +135,7 @@ async function cmdSync(argv) {
123
135
  const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
124
136
  const opencodeHome = process.env.OPENCODE_HOME || path.join(xdgDataHome, "opencode");
125
137
  const opencodeStorageDir = path.join(opencodeHome, "storage");
138
+ const kiloHome = process.env.KILO_HOME || path.join(xdgDataHome, "kilo");
126
139
 
127
140
  // OpenClaw hook integration: allow a hook to request incremental parsing for a single session jsonl.
128
141
  // We still parse all regular sources so model/source attribution stays complete (e.g. Kimi sessions).
@@ -314,6 +327,63 @@ async function cmdSync(argv) {
314
327
  opencodeResult.bucketsQueued += opencodeDbResult.bucketsQueued;
315
328
  }
316
329
 
330
+ // ── Kilo CLI (kilo.ai @kilocode/plugin — OpenCode-fork SQLite) ──
331
+ // Uses the exact same `message` table schema as OpenCode v1.2+. We reuse
332
+ // the OpenCode DB reader/parser, just with a separate cursor namespace so
333
+ // the message indexes don't collide.
334
+ const kiloDbPath = path.join(kiloHome, "kilo.db");
335
+ let kiloResult = { messagesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
336
+ const kiloDbMessages = readOpencodeDbMessages(kiloDbPath);
337
+ if (kiloDbMessages.length > 0) {
338
+ if (progress?.enabled) {
339
+ progress.start(
340
+ `Parsing Kilo CLI ${renderBar(0)} 0/${formatNumber(kiloDbMessages.length)} msgs | buckets 0`,
341
+ );
342
+ }
343
+ kiloResult = await parseOpencodeDbIncremental({
344
+ dbMessages: kiloDbMessages,
345
+ cursors,
346
+ queuePath,
347
+ projectQueuePath,
348
+ onProgress: (p) => {
349
+ if (!progress?.enabled) return;
350
+ const pct = p.total > 0 ? p.index / p.total : 1;
351
+ progress.update(
352
+ `Parsing Kilo CLI ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
353
+ p.total,
354
+ )} msgs | buckets ${formatNumber(p.bucketsQueued)}`,
355
+ );
356
+ },
357
+ source: "kilo-cli",
358
+ cursorKey: "kiloCli",
359
+ });
360
+ }
361
+
362
+ // ── Kilo Code VS Code extension (Cline-style ui_messages.json) ──
363
+ const kilocodeTaskFiles = resolveKilocodeTaskFiles(process.env);
364
+ let kilocodeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
365
+ if (kilocodeTaskFiles.length > 0) {
366
+ if (progress?.enabled) {
367
+ progress.start(
368
+ `Parsing Kilo Code ${renderBar(0)} 0/${formatNumber(kilocodeTaskFiles.length)} tasks | buckets 0`,
369
+ );
370
+ }
371
+ kilocodeResult = await parseKilocodeIncremental({
372
+ taskFiles: kilocodeTaskFiles,
373
+ cursors,
374
+ queuePath,
375
+ onProgress: (p) => {
376
+ if (!progress?.enabled) return;
377
+ const pct = p.total > 0 ? p.index / p.total : 1;
378
+ progress.update(
379
+ `Parsing Kilo Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(
380
+ p.total,
381
+ )} tasks | buckets ${formatNumber(p.bucketsQueued)}`,
382
+ );
383
+ },
384
+ });
385
+ }
386
+
317
387
  // ── Cursor (API-based) ──
318
388
  // One-time migration: earlier CLI versions mis-parsed the Cursor CSV after
319
389
  // Cursor inserted new "Cloud Agent ID"/"Automation ID" columns, writing
@@ -679,7 +749,9 @@ async function cmdSync(argv) {
679
749
  ompResult.recordsProcessed +
680
750
  piResult.recordsProcessed +
681
751
  craftResult.recordsProcessed +
682
- copilotResult.recordsProcessed;
752
+ copilotResult.recordsProcessed +
753
+ kiloResult.messagesProcessed +
754
+ kilocodeResult.recordsProcessed;
683
755
  const totalBuckets =
684
756
  parseResult.bucketsQueued +
685
757
  openclawResult.bucketsQueued +
@@ -695,7 +767,9 @@ async function cmdSync(argv) {
695
767
  ompResult.bucketsQueued +
696
768
  piResult.bucketsQueued +
697
769
  craftResult.bucketsQueued +
698
- copilotResult.bucketsQueued;
770
+ copilotResult.bucketsQueued +
771
+ kiloResult.bucketsQueued +
772
+ kilocodeResult.bucketsQueued;
699
773
  process.stdout.write(
700
774
  [
701
775
  "Sync finished:",
@@ -1394,7 +1468,7 @@ async function repairClaudeQueueFromGroundTruth({
1394
1468
  }
1395
1469
  uploadState.offset = 0;
1396
1470
  uploadState.updatedAt = new Date().toISOString();
1397
- uploadState.note = "reset_after_claude_repair_2026_05_v3";
1471
+ uploadState.note = "reset_after_claude_repair_2026_05_v4";
1398
1472
  await fs.writeFile(queueStatePath, JSON.stringify(uploadState));
1399
1473
  }
1400
1474
 
@@ -1461,7 +1535,7 @@ async function repairClaudeQueueFromGroundTruth({
1461
1535
  }
1462
1536
  st.offset = 0;
1463
1537
  st.updatedAt = new Date().toISOString();
1464
- st.note = "reset_after_claude_repair_2026_05_v3";
1538
+ st.note = "reset_after_claude_repair_2026_05_v4";
1465
1539
  await fs.writeFile(projectQueueStatePath, JSON.stringify(st));
1466
1540
  }
1467
1541
  }
@@ -1586,9 +1660,8 @@ async function collectClaudeMessageHashes(filePaths) {
1586
1660
  } catch (_e) {
1587
1661
  continue;
1588
1662
  }
1589
- const msgId = obj?.message?.id;
1590
- const reqId = obj?.requestId;
1591
- if (msgId && reqId) hashes.add(`${msgId}:${reqId}`);
1663
+ const hash = claudeMessageDedupKey(obj);
1664
+ if (hash) hashes.add(hash);
1592
1665
  }
1593
1666
  rl.close();
1594
1667
  stream.close?.();
@@ -26,6 +26,7 @@ const {
26
26
  buildExecStatsEntry,
27
27
  allocateByLargestRemainder,
28
28
  } = require("./categorizer-utils");
29
+ const { claudeMessageDedupKey } = require("./rollout");
29
30
 
30
31
  const CATEGORY_KEYS = [
31
32
  "system_prefix",
@@ -478,10 +479,8 @@ async function categorizeSessionFile(filePath, { fromIso, toIso, seenHashes }, b
478
479
  if (fromIso && ts < fromIso) continue;
479
480
  if (toIso && ts > toIso) continue;
480
481
 
481
- const msgId = obj?.message?.id;
482
- const reqId = obj?.requestId;
483
- if (msgId && reqId) {
484
- const hash = `${msgId}:${reqId}`;
482
+ const hash = claudeMessageDedupKey(obj);
483
+ if (hash) {
485
484
  if (seenHashes.has(hash)) continue;
486
485
  seenHashes.add(hash);
487
486
  }
@@ -1133,10 +1132,8 @@ async function computeClaudeGroundTruthBuckets({ rootDir = null } = {}) {
1133
1132
  const usage = obj?.message?.usage;
1134
1133
  if (!usage || typeof usage !== "object") continue;
1135
1134
 
1136
- const msgId = obj?.message?.id;
1137
- const reqId = obj?.requestId;
1138
- if (msgId && reqId) {
1139
- const hash = `${msgId}:${reqId}`;
1135
+ const hash = claudeMessageDedupKey(obj);
1136
+ if (hash) {
1140
1137
  if (seenHashes.has(hash)) continue;
1141
1138
  seenHashes.add(hash);
1142
1139
  }