@tokz/cli 0.2.9 → 0.2.11

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.
@@ -206,6 +206,11 @@ async function loadProjects(home = homedir(), onProgress) {
206
206
 
207
207
  // src/agents/usage.ts
208
208
  import { readFile as readFile2 } from "fs/promises";
209
+ function recordDedupKey(r) {
210
+ const scope = r.id ?? r.ts;
211
+ if (!scope) return void 0;
212
+ return `${scope}|${r.model}|${r.input}|${r.output}|${r.cacheRead}|${r.cacheWrite}`;
213
+ }
209
214
  async function readJson(file) {
210
215
  try {
211
216
  return JSON.parse(await readFile2(file, "utf8"));
@@ -230,10 +235,17 @@ async function readJsonl(file) {
230
235
  }
231
236
  return out;
232
237
  }
233
- function sessionFromRecords(file, cwd, records) {
238
+ function sessionFromRecords(file, cwd, records, seen) {
234
239
  const stats = { file, cwd, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
235
240
  for (const r of records) {
236
241
  if (r.input + r.output + r.cacheRead + r.cacheWrite === 0) continue;
242
+ if (seen) {
243
+ const key = recordDedupKey(r);
244
+ if (key !== void 0) {
245
+ if (seen.has(key)) continue;
246
+ seen.add(key);
247
+ }
248
+ }
237
249
  if (r.ts) {
238
250
  if (!stats.firstTs || r.ts < stats.firstTs) stats.firstTs = r.ts;
239
251
  if (!stats.lastTs || r.ts > stats.lastTs) stats.lastTs = r.ts;
@@ -558,7 +570,7 @@ function isAssistant(msg) {
558
570
  const role = str(msg, "variant") ?? str(msg, "role");
559
571
  return role === "ai" || role === "agent" || role === "assistant";
560
572
  }
561
- async function parseFile(file) {
573
+ async function parseFile(file, seen) {
562
574
  const arr = await readJson(file);
563
575
  if (!Array.isArray(arr)) return sessionFromRecords(file, void 0, []);
564
576
  let fileTs;
@@ -575,6 +587,7 @@ async function parseFile(file) {
575
587
  records.push({
576
588
  model: str(meta, "model") ?? "codebuff-unknown",
577
589
  ts: str(msg, "timestamp") ?? fileTs,
590
+ id: str(msg, "id") ?? str(msg, "messageId") ?? str(msg, "message_id"),
578
591
  input: pickNum(usage, ["inputTokens", "input_tokens"]),
579
592
  output: pickNum(usage, ["outputTokens", "output_tokens"]),
580
593
  cacheRead: pickNum(usage, ["cacheReadInputTokens", "cache_read_input_tokens", "cachedTokens"]),
@@ -591,10 +604,11 @@ async function loadCodebuffProjects(home, onProgress) {
591
604
  );
592
605
  }
593
606
  const sessions = [];
607
+ const seen = /* @__PURE__ */ new Set();
594
608
  let parsed = 0;
595
609
  for (const f of files) {
596
610
  onProgress?.({ parsed, total: files.length, currentProject: "Codebuff chats" });
597
- sessions.push(await parseFile(f));
611
+ sessions.push(await parseFile(f, seen));
598
612
  parsed += 1;
599
613
  onProgress?.({ parsed, total: files.length, currentProject: "Codebuff chats" });
600
614
  }
@@ -642,7 +656,12 @@ var Line = z.object({
642
656
  cwd: z.string().optional(),
643
657
  model: z.string().optional(),
644
658
  name: z.string().optional(),
645
- info: z.object({ total_token_usage: TokenTotals.optional() }).nullish()
659
+ info: z.object({
660
+ total_token_usage: TokenTotals.optional(),
661
+ // Codex writes the per-turn amount here directly; prefer it over
662
+ // differencing cumulative totals.
663
+ last_token_usage: TokenTotals.optional()
664
+ }).nullish()
646
665
  }).passthrough().optional()
647
666
  });
648
667
  function codexHome(home) {
@@ -650,7 +669,7 @@ function codexHome(home) {
650
669
  return process.env.CODEX_HOME ?? join6(homedir6(), ".codex");
651
670
  }
652
671
  var DEFAULT_MODEL = "gpt-5";
653
- async function parseCodexRollout(file) {
672
+ async function parseCodexRollout(file, seen = /* @__PURE__ */ new Set()) {
654
673
  const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
655
674
  const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
656
675
  let model = DEFAULT_MODEL;
@@ -678,20 +697,46 @@ async function parseCodexRollout(file) {
678
697
  stats.toolCalls[toolName] = (stats.toolCalls[toolName] ?? 0) + 1;
679
698
  turnTools.push(toolName);
680
699
  }
681
- const totals = payload2?.type === "token_count" ? payload2.info?.total_token_usage : void 0;
682
- if (!totals) continue;
683
- const cur = {
684
- input: totals.input_tokens,
685
- cached: totals.cached_input_tokens,
686
- output: totals.output_tokens,
687
- total: totals.total_tokens
688
- };
689
- if (cur.total < prev.total) prev = { input: 0, cached: 0, output: 0, total: 0 };
690
- const dInput = Math.max(0, cur.input - prev.input);
691
- const dCached = Math.max(0, cur.cached - prev.cached);
692
- const dOutput = Math.max(0, cur.output - prev.output);
693
- prev = cur;
700
+ const info = payload2?.type === "token_count" ? payload2.info : void 0;
701
+ const last = info?.last_token_usage;
702
+ const totals = info?.total_token_usage;
703
+ if (!last && !totals) continue;
704
+ const idTotals = last ?? totals;
705
+ const dedupKey = `${timestamp ?? ""}|${model}|${idTotals.input_tokens}|${idTotals.cached_input_tokens}|${idTotals.output_tokens}|${idTotals.reasoning_output_tokens}|${idTotals.total_tokens}`;
706
+ let dInput;
707
+ let dCached;
708
+ let dOutput;
709
+ if (last) {
710
+ dInput = last.input_tokens;
711
+ dCached = last.cached_input_tokens;
712
+ dOutput = last.output_tokens;
713
+ if (totals) {
714
+ prev = {
715
+ input: totals.input_tokens,
716
+ cached: totals.cached_input_tokens,
717
+ output: totals.output_tokens,
718
+ total: totals.total_tokens
719
+ };
720
+ }
721
+ } else {
722
+ const cur = {
723
+ input: totals.input_tokens,
724
+ cached: totals.cached_input_tokens,
725
+ output: totals.output_tokens,
726
+ total: totals.total_tokens
727
+ };
728
+ if (cur.total < prev.total) prev = { input: 0, cached: 0, output: 0, total: 0 };
729
+ dInput = Math.max(0, cur.input - prev.input);
730
+ dCached = Math.max(0, cur.cached - prev.cached);
731
+ dOutput = Math.max(0, cur.output - prev.output);
732
+ prev = cur;
733
+ }
694
734
  if (dInput + dOutput === 0) continue;
735
+ if (seen.has(dedupKey)) {
736
+ turnTools = [];
737
+ continue;
738
+ }
739
+ seen.add(dedupKey);
695
740
  const delta = {
696
741
  // Codex's input_tokens INCLUDES cached tokens; split them out.
697
742
  inputTokens: Math.max(0, dInput - dCached),
@@ -730,10 +775,11 @@ async function loadCodexProjects(home, onProgress) {
730
775
  }).catch(() => []);
731
776
  if (files.length === 0) return [];
732
777
  const sessions = [];
778
+ const seen = /* @__PURE__ */ new Set();
733
779
  let parsed = 0;
734
780
  for (const f of files) {
735
781
  onProgress?.({ parsed, total: files.length, currentProject: "Codex sessions" });
736
- sessions.push(await parseCodexRollout(f));
782
+ sessions.push(await parseCodexRollout(f, seen));
737
783
  parsed += 1;
738
784
  onProgress?.({ parsed, total: files.length, currentProject: "Codex sessions" });
739
785
  }
@@ -791,9 +837,8 @@ function attrStr(attrs, keys) {
791
837
  }
792
838
  return void 0;
793
839
  }
794
- function parseOtelFile(records) {
840
+ function parseOtelFile(records, seen = /* @__PURE__ */ new Set()) {
795
841
  const bySession = /* @__PURE__ */ new Map();
796
- const seen = /* @__PURE__ */ new Set();
797
842
  for (const rec of records) {
798
843
  const attrs = rec.attributes;
799
844
  if (!attrs || typeof attrs !== "object") continue;
@@ -824,10 +869,11 @@ function parseOtelFile(records) {
824
869
  async function loadCopilotProjects(home, onProgress) {
825
870
  const files = await glob5(["**/*.jsonl"], { cwd: copilotDir(home), absolute: true }).catch(() => []);
826
871
  const bySession = /* @__PURE__ */ new Map();
872
+ const seen = /* @__PURE__ */ new Set();
827
873
  let parsed = 0;
828
874
  for (const f of files) {
829
875
  onProgress?.({ parsed, total: files.length, currentProject: "Copilot otel" });
830
- for (const [session, records] of parseOtelFile(await readJsonl(f))) {
876
+ for (const [session, records] of parseOtelFile(await readJsonl(f), seen)) {
831
877
  const list = bySession.get(session) ?? [];
832
878
  list.push(...records);
833
879
  bySession.set(session, list);
@@ -928,13 +974,14 @@ function recordFrom(node, model, ts) {
928
974
  return {
929
975
  model: str(node, "model") ?? model ?? "gemini-unknown",
930
976
  ts: str(node, "timestamp") ?? ts,
977
+ id: str(node, "id") ?? str(node, "messageId") ?? str(node, "message_id") ?? str(node, "responseId"),
931
978
  input: pickNum(tokens, IN),
932
979
  output: pickNum(tokens, OUT) + reasoning,
933
980
  cacheRead: pickNum(tokens, ["cached", "cached_tokens"]),
934
981
  cacheWrite: 0
935
982
  };
936
983
  }
937
- async function parseFile3(file) {
984
+ async function parseFile3(file, seen) {
938
985
  const records = [];
939
986
  const push = (r) => r && records.push(r);
940
987
  if (file.endsWith(".jsonl")) {
@@ -946,17 +993,18 @@ async function parseFile3(file) {
946
993
  if (Array.isArray(messages)) for (const m of messages) push(recordFrom(m, model, void 0));
947
994
  push(recordFrom(doc, model, void 0));
948
995
  }
949
- return sessionFromRecords(file, void 0, records);
996
+ return sessionFromRecords(file, void 0, records, seen);
950
997
  }
951
998
  async function loadGeminiProjects(home, onProgress) {
952
999
  const files = await glob7(["**/*.json", "**/*.jsonl"], { cwd: geminiRoot(home), absolute: true }).catch(
953
1000
  () => []
954
1001
  );
955
1002
  const sessions = [];
1003
+ const seen = /* @__PURE__ */ new Set();
956
1004
  let parsed = 0;
957
1005
  for (const f of files) {
958
1006
  onProgress?.({ parsed, total: files.length, currentProject: "Gemini logs" });
959
- sessions.push(await parseFile3(f));
1007
+ sessions.push(await parseFile3(f, seen));
960
1008
  parsed += 1;
961
1009
  onProgress?.({ parsed, total: files.length, currentProject: "Gemini logs" });
962
1010
  }
@@ -1325,21 +1373,23 @@ function kimiRoots(home) {
1325
1373
  const h = home ?? homedir13();
1326
1374
  return KIMI_DIRS.map((d) => join13(h, d));
1327
1375
  }
1328
- async function parseFile4(file) {
1376
+ async function parseFile4(file, seen) {
1329
1377
  const records = [];
1330
1378
  for (const line of await readJsonl(file)) {
1331
1379
  const usage = deepFind(line, USAGE_KEYS);
1332
1380
  if (!usage) continue;
1381
+ const idHost = deepFind(line, ["messageId", "message_id", "id"]);
1333
1382
  records.push({
1334
1383
  model: str(line, "model") ?? "kimi-unknown",
1335
1384
  ts: str(line, "timestamp"),
1385
+ id: idHost && (str(idHost, "messageId") ?? str(idHost, "message_id") ?? str(idHost, "id")),
1336
1386
  input: pickNum(usage, ["inputOther", "input_other"]),
1337
1387
  output: pickNum(usage, ["output"]),
1338
1388
  cacheRead: pickNum(usage, ["inputCacheRead", "input_cache_read"]),
1339
1389
  cacheWrite: pickNum(usage, ["inputCacheCreation", "input_cache_creation"])
1340
1390
  });
1341
1391
  }
1342
- return sessionFromRecords(file, void 0, records);
1392
+ return sessionFromRecords(file, void 0, records, seen);
1343
1393
  }
1344
1394
  async function loadKimiProjects(home, onProgress) {
1345
1395
  const files = [];
@@ -1349,10 +1399,11 @@ async function loadKimiProjects(home, onProgress) {
1349
1399
  );
1350
1400
  }
1351
1401
  const sessions = [];
1402
+ const seen = /* @__PURE__ */ new Set();
1352
1403
  let parsed = 0;
1353
1404
  for (const f of files) {
1354
1405
  onProgress?.({ parsed, total: files.length, currentProject: "Kimi sessions" });
1355
- sessions.push(await parseFile4(f));
1406
+ sessions.push(await parseFile4(f, seen));
1356
1407
  parsed += 1;
1357
1408
  onProgress?.({ parsed, total: files.length, currentProject: "Kimi sessions" });
1358
1409
  }
@@ -1390,7 +1441,7 @@ function openclawRoots(home) {
1390
1441
  function modelFrom(obj) {
1391
1442
  return str(obj, "modelId") ?? str(obj, "model");
1392
1443
  }
1393
- async function parseFile5(file) {
1444
+ async function parseFile5(file, seen) {
1394
1445
  const records = [];
1395
1446
  let currentModel;
1396
1447
  for (const line of await readJsonl(file)) {
@@ -1405,13 +1456,14 @@ async function parseFile5(file) {
1405
1456
  records.push({
1406
1457
  model,
1407
1458
  ts: str(l.message, "timestamp") ?? str(l, "timestamp"),
1459
+ id: str(l.message, "id") ?? str(l, "id") ?? str(l.message, "messageId"),
1408
1460
  input: pickNum(usage, ["input"]),
1409
1461
  output: pickNum(usage, ["output"]),
1410
1462
  cacheRead: pickNum(usage, ["cacheRead"]),
1411
1463
  cacheWrite: pickNum(usage, ["cacheWrite"])
1412
1464
  });
1413
1465
  }
1414
- return sessionFromRecords(file, void 0, records);
1466
+ return sessionFromRecords(file, void 0, records, seen);
1415
1467
  }
1416
1468
  async function loadOpenclawProjects(home, onProgress) {
1417
1469
  const files = [];
@@ -1419,10 +1471,11 @@ async function loadOpenclawProjects(home, onProgress) {
1419
1471
  files.push(...await glob9(["**/*.jsonl", "**/*.jsonl.*"], { cwd: root, absolute: true }).catch(() => []));
1420
1472
  }
1421
1473
  const sessions = [];
1474
+ const seen = /* @__PURE__ */ new Set();
1422
1475
  let parsed = 0;
1423
1476
  for (const f of files) {
1424
1477
  onProgress?.({ parsed, total: files.length, currentProject: "OpenClaw sessions" });
1425
- sessions.push(await parseFile5(f));
1478
+ sessions.push(await parseFile5(f, seen));
1426
1479
  parsed += 1;
1427
1480
  onProgress?.({ parsed, total: files.length, currentProject: "OpenClaw sessions" });
1428
1481
  }
@@ -1566,7 +1619,7 @@ import { glob as glob11 } from "tinyglobby";
1566
1619
  function piRoot(home) {
1567
1620
  return process.env.PI_AGENT_DIR ?? join16(home ?? homedir16(), ".pi", "agent", "sessions");
1568
1621
  }
1569
- async function parseFile6(file) {
1622
+ async function parseFile6(file, seen) {
1570
1623
  const records = [];
1571
1624
  for (const line of await readJsonl(file)) {
1572
1625
  const msg = line.message;
@@ -1575,21 +1628,23 @@ async function parseFile6(file) {
1575
1628
  records.push({
1576
1629
  model: str(msg, "model") ?? "pi-unknown",
1577
1630
  ts: str(line, "timestamp"),
1631
+ id: str(msg, "id") ?? str(line, "id") ?? str(msg, "messageId"),
1578
1632
  input: pickNum(usage, ["input"]),
1579
1633
  output: pickNum(usage, ["output"]),
1580
1634
  cacheRead: pickNum(usage, ["cacheRead"]),
1581
1635
  cacheWrite: pickNum(usage, ["cacheWrite"])
1582
1636
  });
1583
1637
  }
1584
- return sessionFromRecords(file, void 0, records);
1638
+ return sessionFromRecords(file, void 0, records, seen);
1585
1639
  }
1586
1640
  async function loadPiProjects(home, onProgress) {
1587
1641
  const files = await glob11(["**/*.jsonl"], { cwd: piRoot(home), absolute: true }).catch(() => []);
1588
1642
  const sessions = [];
1643
+ const seen = /* @__PURE__ */ new Set();
1589
1644
  let parsed = 0;
1590
1645
  for (const f of files) {
1591
1646
  onProgress?.({ parsed, total: files.length, currentProject: "pi sessions" });
1592
- sessions.push(await parseFile6(f));
1647
+ sessions.push(await parseFile6(f, seen));
1593
1648
  parsed += 1;
1594
1649
  onProgress?.({ parsed, total: files.length, currentProject: "pi sessions" });
1595
1650
  }
@@ -1618,7 +1673,7 @@ import { glob as glob12 } from "tinyglobby";
1618
1673
  function qwenRoot(home) {
1619
1674
  return process.env.QWEN_DATA_DIR ?? join17(home ?? homedir17(), ".qwen");
1620
1675
  }
1621
- async function parseFile7(file) {
1676
+ async function parseFile7(file, seen) {
1622
1677
  const records = [];
1623
1678
  for (const line of await readJsonl(file)) {
1624
1679
  const meta = line.usageMetadata;
@@ -1629,23 +1684,25 @@ async function parseFile7(file) {
1629
1684
  records.push({
1630
1685
  model: str(line, "model") ?? "qwen-unknown",
1631
1686
  ts: str(line, "timestamp"),
1687
+ id: str(line, "id") ?? str(line, "messageId") ?? str(line, "message_id"),
1632
1688
  input: Math.max(0, prompt - cached),
1633
1689
  output: pickNum(meta, ["candidatesTokenCount"]) + reasoning,
1634
1690
  cacheRead: cached,
1635
1691
  cacheWrite: 0
1636
1692
  });
1637
1693
  }
1638
- return sessionFromRecords(file, void 0, records);
1694
+ return sessionFromRecords(file, void 0, records, seen);
1639
1695
  }
1640
1696
  async function loadQwenProjects(home, onProgress) {
1641
1697
  const files = await glob12(["projects/**/*.jsonl"], { cwd: qwenRoot(home), absolute: true }).catch(
1642
1698
  () => []
1643
1699
  );
1644
1700
  const sessions = [];
1701
+ const seen = /* @__PURE__ */ new Set();
1645
1702
  let parsed = 0;
1646
1703
  for (const f of files) {
1647
1704
  onProgress?.({ parsed, total: files.length, currentProject: "Qwen sessions" });
1648
- sessions.push(await parseFile7(f));
1705
+ sessions.push(await parseFile7(f, seen));
1649
1706
  parsed += 1;
1650
1707
  onProgress?.({ parsed, total: files.length, currentProject: "Qwen sessions" });
1651
1708
  }
package/dist/cli.js CHANGED
@@ -276,7 +276,7 @@ program.action(async () => {
276
276
  const [{ render }, React, { Root }, { Fullscreen }] = await Promise.all([
277
277
  import("ink"),
278
278
  import("react"),
279
- import("./Root-ZJO6F6LM.js"),
279
+ import("./Root-T2V2YHJ2.js"),
280
280
  import("./Fullscreen-GK2ZYXHV.js")
281
281
  ]);
282
282
  render(React.createElement(Fullscreen, null, React.createElement(Root)));
package/package.json CHANGED
@@ -1,9 +1,43 @@
1
1
  {
2
2
  "name": "@tokz/cli",
3
- "version": "0.2.9",
4
- "description": "Audit where your coding agent's context window and API dollars go.",
5
- "type": "module",
3
+ "version": "0.2.11",
4
+ "description": "See where your coding agents' tokens and dollars actually go.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "anthropic",
9
+ "ai",
10
+ "agent",
11
+ "coding-agent",
12
+ "cli",
13
+ "token",
14
+ "tokens",
15
+ "token-usage",
16
+ "context-window",
17
+ "prompt",
18
+ "prompt-engineering",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "transcript",
22
+ "audit",
23
+ "cost",
24
+ "api-cost",
25
+ "analytics",
26
+ "developer-tools",
27
+ "productivity",
28
+ "typescript",
29
+ "terminal"
30
+ ],
31
+ "homepage": "https://github.com/tokz-dev/cli#readme",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/tokz-dev/cli.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/tokz-dev/cli/issues"
38
+ },
6
39
  "license": "MIT",
40
+ "type": "module",
7
41
  "engines": {
8
42
  "node": ">=20.9.0"
9
43
  },
@@ -40,4 +74,4 @@
40
74
  "typescript": "^5.7.0",
41
75
  "vitest": "^3.0.0"
42
76
  }
43
- }
77
+ }