tokentracker-cli 1.1.2 → 1.1.3

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 (107) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +6 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-CX7g238N.js → AchievementBadge-dt_UDU_h.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-BbkEymra.js → AchievementsPage-B5DJ3S9l.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CmhoerE7.js → AchievementsSection-CDxcyig2.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-996i13Vf.js → BadgeDetailModal-Cp98zNn2.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-B22CfAlF.js → BotAnimated-CR7MiUpo.js} +1 -1
  11. package/dashboard/dist/assets/{Card-DFd9hCFB.js → Card-C8IdDvlg.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-DXKN28M2.js → ClawdAnimated-COeTlW8z.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-DVRs34TV.js → CommandPalette-Co9k2CCc.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-BExjH9E8.js → CommunityStatsModal-C6XQeRgw.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-KJhOrJ98.js → ConfirmModal-Ai4182O-.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-xnIEnOqd.js → Controls-BbKzN-Pe.js} +1 -1
  17. package/dashboard/dist/assets/{DashboardPage-BzsiCwjX.js → DashboardPage-DTcVGG4e.js} +1 -1
  18. package/dashboard/dist/assets/{DevicePage-Bo8e-KVq.js → DevicePage-CZbrtJmb.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-gBd-BmR7.js → DialogClose-DMl8tAoX.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-haPz7CX9.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-B0UbsqD7.js → DialogTitle-BJxpp2t-.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-Dv2DtOPn.js → FadeIn-hU_MXl6R.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-1kEsgGdK.js → HeaderGithubStar-CM5RSxxN.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-DQlpkw2L.js → HoverTooltip-BZKfQyZQ.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-CFlkmSlA.js → IpCheckPage-BjDx-zja.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-DNs6mSij.js → LandingPage-CAt8BCDv.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-DSltCiRr.js → LeaderboardAvatar-DiCrakjI.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-CkjUMA-k.js → LeaderboardPage-Dj3pHv6U.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-DJjaD7Kz.js → LeaderboardProfileModal-DEWbcP9p.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-Bue7tJoi.js → LeaderboardProfilePage-CTbWHQkY.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-Ctbvl8Q8.js → LimitsPage-V-98OUhF.js} +1 -1
  32. package/dashboard/dist/assets/{LocalOnlyNotice-LjNI8j4I.js → LocalOnlyNotice-a4PqtUZY.js} +1 -1
  33. package/dashboard/dist/assets/LoginCard-oD1A2Ru8.js +1 -0
  34. package/dashboard/dist/assets/{LoginModal-CUmfCsHY.js → LoginModal-D8aSmxok.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-CjJb9qFM.js → LoginPage-DIjhBv2X.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-BWKEnBEW.js → PetPage-D7601BTX.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-DeXj3kM3.js → PopoverPopup-aPnmKopO.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-DL-oa-UM.js → ProviderIcon-rwyfcNlT.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-CeeKMN8H.js → ResetPasswordPage-AF3SBgW2.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-Bx5Z9tYf.js → SegmentedControl-fnlScNMT.js} +1 -1
  41. package/dashboard/dist/assets/{Select-DRbgbSPd.js → Select-DDjC3RjB.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-D792o2Et.js → SelectItemText-VhBWE2O8.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-NjQ1oGqx.js → ServiceStatusPage-BL3qqEsp.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-Cr2uHP91.js → SessionsPage-DXYiuP2x.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-Av8pVbqT.js → SettingsPage-DTeA_ahr.js} +2 -2
  46. package/dashboard/dist/assets/{SkillsPage-feW4j329.js → SkillsPage-CtTRwVQH.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-DX36F36Q.js → TokenGalaxy-CifK91V7.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-CzMlVMfI.js → ToolbarRootContext-ClGAIYoa.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-BosWE34g.js → TrendMonitor-D1evFHRM.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-DhoypTuq.js → WidgetsPage-Cq4RYJDE.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-DR6BoipR.js → WrappedPage-CoC5RJH7.js} +1 -1
  52. package/dashboard/dist/assets/__vite-browser-external-BDugA6I0.js +45 -0
  53. package/dashboard/dist/assets/{agent-logos-QSScjsfz.js → agent-logos-QLdA9OeA.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-BJQYadF-.js → arrow-up-right-BYLgB3bH.js} +1 -1
  55. package/dashboard/dist/assets/{check-CINIOTsF.js → check-DfnIvqZD.js} +1 -1
  56. package/dashboard/dist/assets/{composite-YV7-iFo_.js → composite--JllTpIX.js} +1 -1
  57. package/dashboard/dist/assets/{copy-Bp_U7bAZ.js → copy-ClKVBIgD.js} +1 -1
  58. package/dashboard/dist/assets/{download-CVOIkdjl.js → download-Di1-UFJX.js} +1 -1
  59. package/dashboard/dist/assets/{external-link-P6LDhEET.js → external-link-D7zPVFLO.js} +1 -1
  60. package/dashboard/dist/assets/{flame-AbZCJ878.js → flame-BMJTxjVg.js} +1 -1
  61. package/dashboard/dist/assets/{format-tokens-M5bvANSq.js → format-tokens-DCZM5Nfn.js} +1 -1
  62. package/dashboard/dist/assets/{icons-DpRn5Rxt.js → icons-I615W2IU.js} +1 -1
  63. package/dashboard/dist/assets/{index-CW2bybhs.js → index-BX3CXVdD.js} +1 -1
  64. package/dashboard/dist/assets/{index-KQ5H65Ds.js → index-BejnmdPe.js} +1 -1
  65. package/dashboard/dist/assets/{index-L3O7cNuj.js → index-CwczM3BZ.js} +1 -1
  66. package/dashboard/dist/assets/{info-GEiJwgYk.js → info-CFJPny5Q.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-D8KpexwS.js → link-2-D1ydWh_w.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-r0nEab80.js → loader-circle-Cq-J-OFg.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-azGbxGbz.js → maximize-2-kIoSq57I.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-Cke4_92g.js → modifiers.esm-CLojthuF.js} +1 -1
  71. package/dashboard/dist/assets/{monitor-smartphone-BnHiLGNs.js → monitor-smartphone-DX2qWHYV.js} +1 -1
  72. package/dashboard/dist/assets/{plus-CJbY6Uuk.js → plus-B-JEpo3V.js} +1 -1
  73. package/dashboard/dist/assets/{provider-display-B05KW8SE.js → provider-display-DJDdrTAS.js} +2 -2
  74. package/dashboard/dist/assets/{react-CingcaIm.js → react-Ctoe8iLl.js} +1 -1
  75. package/dashboard/dist/assets/{search-CyffdJFU.js → search-BTSQrlW-.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-CdDPrjMM.js → skills-api-DKQyM8h7.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-B48YEoXA.js → sortable.esm-pOEL13xU.js} +1 -1
  78. package/dashboard/dist/assets/{store-B0W0vLvA.js → store-B2RplzvW.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-CJ0D4hSx.js → terminal-BH_6_EW_.js} +1 -1
  80. package/dashboard/dist/assets/{trash-DH-_Q3A5.js → trash-CC444Mz6.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-y6OaeOVs.js → use-community-stats-CBXLdD_j.js} +1 -1
  82. package/dashboard/dist/assets/{use-latest-request-guard-DP7kE0Xa.js → use-latest-request-guard-DMuPfZQI.js} +1 -1
  83. package/dashboard/dist/assets/{use-limits-display-prefs-Bgcto2k1.js → use-limits-display-prefs-CmsGTBA4.js} +1 -1
  84. package/dashboard/dist/assets/{use-ordered-list-1L7o2taR.js → use-ordered-list-BNJNpXjB.js} +1 -1
  85. package/dashboard/dist/assets/use-reduced-motion-CNpG-Yoi.js +1 -0
  86. package/dashboard/dist/assets/{use-session-efficiency-pref-36sqzSZl.js → use-session-efficiency-pref-g_0ISxe1.js} +1 -1
  87. package/dashboard/dist/assets/{use-transform-DZgeA77H.js → use-transform-D36BHcXk.js} +1 -1
  88. package/dashboard/dist/assets/{use-usage-limits-C5P1O9oT.js → use-usage-limits-Dxw3vzsD.js} +1 -1
  89. package/dashboard/dist/assets/{useCurrency-Dwv89bBn.js → useCurrency-V2FE4TMo.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-BGeVB81B.js → useTokenFormat-CEOwvzMt.js} +1 -1
  91. package/dashboard/dist/assets/{zap-D18Nt3M5.js → zap-BS7ZU76I.js} +1 -1
  92. package/dashboard/dist/index.html +13 -11
  93. package/dashboard/dist/ip-check.html +1 -1
  94. package/dashboard/dist/leaderboard.html +1 -1
  95. package/dashboard/dist/llms.txt +3 -2
  96. package/dashboard/dist/share.html +1 -1
  97. package/package.json +2 -2
  98. package/src/commands/init.js +11 -0
  99. package/src/commands/status.js +30 -0
  100. package/src/commands/sync.js +37 -1
  101. package/src/lib/pricing/seed-snapshot.json +1 -1
  102. package/src/lib/rollout.js +648 -2
  103. package/src/lib/wsl-probe.js +55 -12
  104. package/dashboard/dist/assets/DialogDescription-CTEI9PvU.js +0 -1
  105. package/dashboard/dist/assets/LoginCard-U_3SnvtN.js +0 -1
  106. package/dashboard/dist/assets/__vite-browser-external-DYTQCEz4.js +0 -45
  107. package/dashboard/dist/assets/use-reduced-motion-BQxHT0gI.js +0 -1
@@ -3,6 +3,7 @@ const fssync = require("node:fs");
3
3
  const os = require("node:os");
4
4
  const path = require("node:path");
5
5
  const readline = require("node:readline");
6
+ const { isDeepStrictEqual } = require("node:util");
6
7
 
7
8
  const crypto = require("node:crypto");
8
9
  const { ensureDir, writeJson, chmod600IfPossible } = require("./fs");
@@ -6886,8 +6887,7 @@ async function parseClaudeScienceIncremental({
6886
6887
 
6887
6888
  const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
6888
6889
  const updatedAt = new Date().toISOString();
6889
- hourlyState.updatedAt = updatedAt;
6890
- state.updatedAt = updatedAt;
6890
+ if (bucketsQueued > 0) hourlyState.updatedAt = updatedAt;
6891
6891
  cursors.hourly = hourlyState;
6892
6892
  cursors.claudeScience = state;
6893
6893
  return { recordsProcessed, eventsAggregated, bucketsQueued };
@@ -23554,6 +23554,643 @@ async function parseDshIncremental({ sessionFiles, cursors, queuePath, onProgres
23554
23554
  }
23555
23555
 
23556
23556
 
23557
+ // ── Command Code (`cmd`, commandcode.ai) — passive session-log reader (issue #630) ──
23558
+ //
23559
+ // Command Code keeps one JSONL transcript per conversation under
23560
+ // `~/.commandcode/projects/<cwd-slug>/<session-id>.jsonl`. The first line is a
23561
+ // session header carrying the launch `cwd`; every completed assistant turn
23562
+ // appends a record whose TOP-LEVEL `model` and `usage` are Command Code's own
23563
+ // accounting for that request:
23564
+ //
23565
+ // {"type":"message","id":"…","parentId":"…","timestamp":"…","effort":"…",
23566
+ // "model":"deepseek/deepseek-v4.1-flash",
23567
+ // "usage":{"inputTokens":…,"outputTokens":…,"cacheReadTokens":…,
23568
+ // "cacheWriteTokens":…,"costUsd":…},"message":{…}}
23569
+ //
23570
+ // Two accounting conventions are load-bearing:
23571
+ //
23572
+ // 1. AI SDK-normalized `inputTokens` ALREADY INCLUDES cache reads and writes.
23573
+ // `uncached = inputTokens - cacheReadTokens - cacheWriteTokens`; keeping
23574
+ // either cache category in input double counts it in `total_tokens`.
23575
+ // 2. `costUsd` is Command Code's display-rate estimate, not a billed amount.
23576
+ // Ignore it and emit the zero cost sentinel; local readers and cloud
23577
+ // endpoints estimate cost from the shared model price table.
23578
+ //
23579
+ // Transcripts are append-only in practice, but a resume/compaction REWRITES the
23580
+ // file, so byte offsets are the wrong cursor shape here. This reader rebuilds a
23581
+ // per-file snapshot and reconciles it against a durable observation ledger keyed
23582
+ // by `recordId|timestamp`. Forks and clones copy those fields into transcripts
23583
+ // with new session headers, so the header cannot be part of the key. Files whose
23584
+ // (size, mtime) pair is unchanged can reuse their owned ledger records. A
23585
+ // non-owning duplicate is re-read rather than storing a second full ledger.
23586
+ const COMMAND_CODE_SOURCE = "command-code";
23587
+ const COMMAND_CODE_STATE_VERSION = 3;
23588
+ const COMMAND_CODE_FILE_CACHE_VERSION = 3;
23589
+ const COMMAND_CODE_HEADER_MAX_BYTES = 65536;
23590
+ const COMMAND_CODE_HOME_DIR = ".commandcode";
23591
+ const COMMAND_CODE_PROJECTS_DIR = "projects";
23592
+
23593
+ function isCommandCodeSessionLogName(name) {
23594
+ return (
23595
+ typeof name === "string" &&
23596
+ name.endsWith(".jsonl") &&
23597
+ !name.includes(".prompts.") &&
23598
+ !name.endsWith(".checkpoints.jsonl")
23599
+ );
23600
+ }
23601
+
23602
+ // Precedence mirrors the other passive readers: an explicit TokenTracker
23603
+ // override first, then the CLI's own `~/.commandcode`.
23604
+ function resolveCommandCodeHome(env = process.env) {
23605
+ const explicit = env?.TOKENTRACKER_COMMANDCODE_HOME;
23606
+ if (typeof explicit === "string" && explicit.trim()) return path.resolve(explicit.trim());
23607
+ return path.join(os.homedir(), COMMAND_CODE_HOME_DIR);
23608
+ }
23609
+
23610
+ // Windows users commonly run the `cmd` CLI inside WSL while TokenTracker itself
23611
+ // runs natively. Respect the repository-wide WSL mode contract and keep explicit
23612
+ // home overrides authoritative: an override is a complete user choice, not one
23613
+ // half of an automatic native/WSL discovery pair.
23614
+ function resolveCommandCodeHomes(env = process.env, deps = {}) {
23615
+ const override = env?.TOKENTRACKER_COMMANDCODE_HOME;
23616
+ const overridden = typeof override === "string" && override.trim().length > 0;
23617
+ const nativeHome = deps.nativeHome || resolveCommandCodeHome(env);
23618
+ const platform = deps.platform || process.platform;
23619
+ if (overridden || platform !== "win32") return [nativeHome];
23620
+
23621
+ const probePath = deps.existsSync || ((candidate) => fssync.statSync(candidate).isDirectory());
23622
+ const existsSync = (candidate) => {
23623
+ try { return probePath(candidate); }
23624
+ catch (error) {
23625
+ if (isCommandCodePathMissing(error)) return false;
23626
+ throw error;
23627
+ }
23628
+ };
23629
+ // Native errors remain visible. WSL is optional and must not suppress native
23630
+ // observations when its executable, identity or UNC existence probes fail.
23631
+ const nativeValue = wsl.shouldProbeNative(env) && existsSync(nativeHome) ? nativeHome : null;
23632
+ const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
23633
+ let wslValue = null;
23634
+ if (wsl.shouldProbeWsl(env)) {
23635
+ try {
23636
+ wslValue = discoverWslHome(COMMAND_CODE_HOME_DIR, { ...deps, env, existsSync, strict: true });
23637
+ } catch (_error) { }
23638
+ }
23639
+ const resolved = wsl.resolveAllWin32Paths({
23640
+ nativeValue,
23641
+ wslValue,
23642
+ env,
23643
+ platform,
23644
+ });
23645
+ return [...new Set([resolved.native, resolved.wsl].filter(Boolean))];
23646
+ }
23647
+
23648
+ // Walk `<home>/projects/<cwd-slug>/` for `<session-id>.jsonl` transcripts. The
23649
+ // sibling checkpoint snapshots and `.prompts.` sidecars are not transcripts
23650
+ // and must not inflate discovery or status session counts.
23651
+ async function resolveCommandCodeSessionFiles(env = process.env, deps = {}) {
23652
+ const out = new Set();
23653
+ const homes = resolveCommandCodeHomes(env, deps);
23654
+ const nativeHome = deps.nativeHome || resolveCommandCodeHome(env);
23655
+ const overridden = typeof env?.TOKENTRACKER_COMMANDCODE_HOME === "string" && env.TOKENTRACKER_COMMANDCODE_HOME.trim();
23656
+ for (const home of homes) {
23657
+ const rootFiles = [];
23658
+ try {
23659
+ const projectsRoot = path.join(home, COMMAND_CODE_PROJECTS_DIR);
23660
+ for (const project of await readCommandCodeDirectory(projectsRoot)) {
23661
+ if (!project.isDirectory()) continue;
23662
+ const projectDir = path.join(projectsRoot, project.name);
23663
+ for (const entry of await readCommandCodeDirectory(projectDir)) {
23664
+ if (!entry.isFile() || !isCommandCodeSessionLogName(entry.name)) continue;
23665
+ rootFiles.push(path.join(projectDir, entry.name));
23666
+ }
23667
+ }
23668
+ } catch (error) {
23669
+ const optionalWsl = !overridden && (deps.platform || process.platform) === "win32" && home !== nativeHome;
23670
+ if (!optionalWsl) throw error;
23671
+ // A selected wsl-first root may become inaccessible after discovery.
23672
+ // Fall back to native without changing shared WSL callers' mode rules.
23673
+ if (wsl.shouldProbeNative(env) && !homes.includes(nativeHome)) homes.push(nativeHome);
23674
+ continue;
23675
+ }
23676
+ for (const file of rootFiles) out.add(file);
23677
+ }
23678
+ return [...out].sort((a, b) => a.localeCompare(b));
23679
+ }
23680
+
23681
+ function isCommandCodePathMissing(error) {
23682
+ return error?.code === "ENOENT" || error?.code === "ENOTDIR" || error?.code === "EISDIR";
23683
+ }
23684
+
23685
+ async function readCommandCodeDirectory(directory) {
23686
+ try {
23687
+ return await fs.readdir(directory, { withFileTypes: true });
23688
+ } catch (error) {
23689
+ if (isCommandCodePathMissing(error)) return [];
23690
+ // Native errors stay visible; optional WSL errors are isolated by the caller.
23691
+ throw error;
23692
+ }
23693
+ }
23694
+
23695
+ // Row models are provider-qualified ("deepseek/deepseek-v4.1-flash"); the
23696
+ // pricing tables and bucket keys use the bare id, matching the dsh reader.
23697
+ function normalizeCommandCodeModelName(value) {
23698
+ if (typeof value !== "string") return null;
23699
+ const trimmed = value.trim();
23700
+ if (!trimmed) return null;
23701
+ const slash = trimmed.lastIndexOf("/");
23702
+ const name = slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
23703
+ return name || null;
23704
+ }
23705
+
23706
+ // Map Command Code's usage object onto disjoint queue columns. `inputTokens`
23707
+ // already includes cache reads and writes (see the section comment), so subtract
23708
+ // both back out first. Explicit numeric zero usage can correct an existing
23709
+ // observation; empty placeholders are not usage. Cost uses model-table pricing.
23710
+ function commandCodeUsageToTotals(usage) {
23711
+ if (!usage || typeof usage !== "object") return null;
23712
+ const inclusiveInput = toNonNegativeInt(usage.inputTokens);
23713
+ const cachedInput = toNonNegativeInt(usage.cacheReadTokens);
23714
+ const cacheWrite = toNonNegativeInt(usage.cacheWriteTokens);
23715
+ const output = toNonNegativeInt(usage.outputTokens);
23716
+ const input = Math.max(0, inclusiveInput - cachedInput - cacheWrite);
23717
+ const total = input + cachedInput + cacheWrite + output;
23718
+ const explicitZero = ["inputTokens", "outputTokens"].every((field) =>
23719
+ typeof usage[field] === "number" && Number.isFinite(usage[field]) && usage[field] === 0,
23720
+ ) && ["cacheReadTokens", "cacheWriteTokens"].every((field) =>
23721
+ usage[field] === undefined || (typeof usage[field] === "number" && usage[field] === 0),
23722
+ );
23723
+ if (total === 0 && !explicitZero) return null;
23724
+ return {
23725
+ input_tokens: input,
23726
+ cached_input_tokens: cachedInput,
23727
+ cache_creation_input_tokens: cacheWrite,
23728
+ output_tokens: output,
23729
+ reasoning_output_tokens: 0,
23730
+ total_tokens: total,
23731
+ billable_total_tokens: total,
23732
+ total_cost_usd: 0,
23733
+ conversation_count: total > 0 ? 1 : 0,
23734
+ };
23735
+ }
23736
+
23737
+ // Check record framing without JSON-decoding its body. Finding an early usage
23738
+ // field is not proof that an interrupted append completed the enclosing object.
23739
+ function isCompleteCommandCodeRecord(raw) {
23740
+ const text = raw.trim();
23741
+ if (text[0] !== "{") return false;
23742
+ const closers = [];
23743
+ let inString = false;
23744
+ for (let index = 0; index < text.length; index++) {
23745
+ const char = text[index];
23746
+ if (inString) {
23747
+ if (char === "\\") index += 1;
23748
+ else if (char === '"') inString = false;
23749
+ continue;
23750
+ }
23751
+ if (char === '"') inString = true;
23752
+ else if (char === "{") closers.push("}");
23753
+ else if (char === "[") closers.push("]");
23754
+ else if (char === "}" || char === "]") {
23755
+ if (char !== closers.pop()) return false;
23756
+ if (closers.length === 0) return index === text.length - 1;
23757
+ }
23758
+ }
23759
+ return false;
23760
+ }
23761
+
23762
+ // Extract selected top-level metadata from transcript text scanned locally.
23763
+ // findDshJsonProperty slices the needed fields; JSON decoding is limited to
23764
+ // selected metadata rather than whole message records or their bodies.
23765
+ // Raw transcript text is present in memory while scanning; prompt, reply and
23766
+ // code bodies are not persisted or uploaded by this reader. The parser test's
23767
+ // JSON.parse guard checks that body content is not JSON-decoded.
23768
+ function extractCommandCodeLine(line) {
23769
+ const raw = String(line || "");
23770
+ if (!isCompleteCommandCodeRecord(raw)) return null;
23771
+ const type = parseDshJsonString(findDshJsonProperty(raw, "type"));
23772
+ if (type === "session") {
23773
+ return {
23774
+ kind: "session",
23775
+ sessionId: parseDshJsonString(findDshJsonProperty(raw, "id")),
23776
+ cwd: parseDshJsonString(findDshJsonProperty(raw, "cwd")),
23777
+ };
23778
+ }
23779
+ if (type !== "message") return null;
23780
+ const id = parseDshJsonString(findDshJsonProperty(raw, "id"));
23781
+ const timestamp = parseDshJsonString(findDshJsonProperty(raw, "timestamp"));
23782
+ if (!id || !timestamp) return null;
23783
+ const usageRaw = findDshJsonProperty(raw, "usage");
23784
+ if (!usageRaw) return null;
23785
+ let usage = null;
23786
+ try {
23787
+ usage = JSON.parse(usageRaw);
23788
+ } catch (_error) {
23789
+ return null;
23790
+ }
23791
+ const totals = commandCodeUsageToTotals(usage);
23792
+ if (!totals) return null;
23793
+ const bucketStart = toUtcHalfHourStart(timestamp);
23794
+ if (!bucketStart) return null;
23795
+ const model = normalizeCommandCodeModelName(
23796
+ parseDshJsonString(findDshJsonProperty(raw, "model")),
23797
+ );
23798
+ return {
23799
+ kind: "message",
23800
+ id,
23801
+ timestamp,
23802
+ model: model || DEFAULT_MODEL,
23803
+ totals,
23804
+ bucketStart,
23805
+ };
23806
+ }
23807
+
23808
+ // Parse one transcript. A torn tail simply contributes nothing — the rebuild
23809
+ // reconciliation picks it up on the next sync once the record is complete.
23810
+ function extractCommandCodeSessionUsage(text) {
23811
+ const records = [];
23812
+ const headerRanges = [];
23813
+ let sessionId = null;
23814
+ let cwd = null;
23815
+ let byteOffset = 0;
23816
+ for (const line of String(text || "").split("\n")) {
23817
+ const length = Buffer.byteLength(line);
23818
+ const start = byteOffset;
23819
+ byteOffset += length + 1;
23820
+ const parsed = extractCommandCodeLine(line);
23821
+ if (!parsed) continue;
23822
+ if (parsed.kind === "session") {
23823
+ const headerLength = Buffer.byteLength(line.trimStart());
23824
+ headerRanges.push({ start: start + length - headerLength, length: headerLength });
23825
+ if (!sessionId && parsed.sessionId) sessionId = parsed.sessionId;
23826
+ if (!cwd && parsed.cwd) cwd = parsed.cwd;
23827
+ continue;
23828
+ }
23829
+ records.push(parsed);
23830
+ }
23831
+ return { sessionId, cwd, records, headerRanges };
23832
+ }
23833
+
23834
+ // Four independent counters are sufficient for current observations. Keep
23835
+ // legacy totals verbatim until a surviving session/id proves their replacement.
23836
+ function compactCommandCodeMessage(value, legacy = false) {
23837
+ const totals = legacy ? { ...value.totals } : {
23838
+ input_tokens: value.totals.input_tokens || 0,
23839
+ cached_input_tokens: value.totals.cached_input_tokens || 0,
23840
+ cache_creation_input_tokens: value.totals.cache_creation_input_tokens || 0,
23841
+ output_tokens: value.totals.output_tokens || 0,
23842
+ };
23843
+ return {
23844
+ totals, bucketStart: value.bucketStart, model: value.model,
23845
+ projectKey: value.projectKey || null, projectRef: value.projectRef || null,
23846
+ ...(legacy ? { legacy: true } : {}),
23847
+ };
23848
+ }
23849
+
23850
+ function expandCommandCodeTotals(totals) {
23851
+ const total = totals.input_tokens + totals.cached_input_tokens + totals.cache_creation_input_tokens + totals.output_tokens;
23852
+ return {
23853
+ ...totals,
23854
+ reasoning_output_tokens: totals.reasoning_output_tokens || 0,
23855
+ total_tokens: totals.total_tokens ?? total,
23856
+ billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens ?? total,
23857
+ total_cost_usd: 0,
23858
+ conversation_count: totals.conversation_count ?? (total > 0 ? 1 : 0),
23859
+ };
23860
+ }
23861
+
23862
+ function normalizeCommandCodeState(raw) {
23863
+ const messages = {};
23864
+ if (raw?.messages && typeof raw.messages === "object") {
23865
+ for (const [key, value] of Object.entries(raw.messages)) {
23866
+ if (!value?.totals || !value.bucketStart || !value.model) continue;
23867
+ messages[key] = compactCommandCodeMessage(value, !raw.version || raw.version < 2 || value.legacy === true);
23868
+ }
23869
+ }
23870
+ const files = {};
23871
+ // v2 already uses exact id|timestamp accounting. Legacy session/id caches
23872
+ // must be reread once, but absent legacy ledger rows are never discarded.
23873
+ if (raw?.version >= 2 && raw.files && typeof raw.files === "object") {
23874
+ for (const [key, value] of Object.entries(raw.files)) {
23875
+ if (!value || typeof value !== "object") continue;
23876
+ const size = Number(value.size);
23877
+ const mtimeMs = Number(value.mtimeMs);
23878
+ if (!Number.isFinite(size) || !Number.isFinite(mtimeMs)) continue;
23879
+ files[key] = { size, mtimeMs };
23880
+ }
23881
+ }
23882
+ const fileIndex = {};
23883
+ // Older caches omitted uncounted zero identities and accepted torn records.
23884
+ // Re-read them once while retaining the durable ledger above.
23885
+ if (raw?.fileCacheVersion === COMMAND_CODE_FILE_CACHE_VERSION && raw.fileIndex) {
23886
+ for (const [key, value] of Object.entries(raw.fileIndex)) {
23887
+ if (!files[key] || !Array.isArray(value?.messageKeys) || !Array.isArray(value.headerRanges)) continue;
23888
+ if (value.messageKeys.some((entry) => typeof entry !== "string")) continue;
23889
+ if (value.zeroKeys && (!Array.isArray(value.zeroKeys) || value.zeroKeys.some((entry) => typeof entry !== "string"))) continue;
23890
+ let end = 0;
23891
+ const validRanges = value.headerRanges.every((range) => {
23892
+ if (!Number.isSafeInteger(range?.start) || !Number.isSafeInteger(range.length)) return false;
23893
+ if (range.start < end || range.length < 0 || range.start + range.length > files[key].size) return false;
23894
+ end = range.start + range.length;
23895
+ return true;
23896
+ });
23897
+ if (!validRanges) continue;
23898
+ fileIndex[key] = {
23899
+ messageKeys: [...new Set(value.messageKeys)],
23900
+ headerRanges: value.headerRanges.map(({ start, length }) => ({ start, length })),
23901
+ completeOwnedSnapshot: value.completeOwnedSnapshot === true,
23902
+ ...(value.zeroKeys?.length ? { zeroKeys: [...new Set(value.zeroKeys)] } : {}),
23903
+ };
23904
+ }
23905
+ }
23906
+ return { version: COMMAND_CODE_STATE_VERSION, fileCacheVersion: COMMAND_CODE_FILE_CACHE_VERSION, messages, files, fileIndex };
23907
+ }
23908
+
23909
+ // Snapshot one transcript through a single descriptor: the change check and the
23910
+ // read share one handle, so a writer cannot swap the file between them (the
23911
+ // TOCTOU shape CodeQL reports as js/file-system-race). Returns null for a
23912
+ // missing or non-file path while preserving durable history. Other observation
23913
+ // failures propagate before any queues or cursors are published.
23914
+ async function readCommandCodeSessionSnapshot(filePath, previous = null, headerRanges = null) {
23915
+ let handle;
23916
+ try {
23917
+ handle = await fs.open(filePath, "r");
23918
+ let stat = await handle.stat();
23919
+ if (!stat.isFile()) return null;
23920
+ const metadata = { size: stat.size, mtimeMs: stat.mtimeMs };
23921
+ if (
23922
+ previous &&
23923
+ previous.size === metadata.size &&
23924
+ previous.mtimeMs === metadata.mtimeMs &&
23925
+ (!headerRanges || headerRanges.reduce((sum, range) => sum + range.length, 0) <= COMMAND_CODE_HEADER_MAX_BYTES)
23926
+ ) {
23927
+ if (!headerRanges) return { ...metadata, unchanged: true, text: null };
23928
+ // Store byte ranges, never cwd. Positional reads through this same handle
23929
+ // refresh only header lines, preserving Unicode and leading whitespace
23930
+ // without materializing any of the cached message bodies.
23931
+ const headers = [];
23932
+ let complete = true;
23933
+ for (const { start, length } of headerRanges) {
23934
+ const data = Buffer.alloc(length);
23935
+ let offset = 0;
23936
+ while (offset < length) {
23937
+ const { bytesRead } = await handle.read(data, offset, length - offset, start + offset);
23938
+ if (!bytesRead) { complete = false; break; }
23939
+ offset += bytesRead;
23940
+ }
23941
+ if (!complete) break;
23942
+ headers.push(data.toString("utf8"));
23943
+ }
23944
+ const finalStat = await handle.stat();
23945
+ if (complete && finalStat.size === stat.size && finalStat.mtimeMs === stat.mtimeMs) {
23946
+ return { ...metadata, unchanged: true, text: headers.join("\n") };
23947
+ }
23948
+ // The file changed during the header read. Rebuild through this handle;
23949
+ // positional reads above did not advance its full-read file position.
23950
+ stat = finalStat;
23951
+ }
23952
+ const data = await handle.readFile();
23953
+ const finalStat = await handle.stat();
23954
+ const finalMetadata = { size: finalStat.size, mtimeMs: finalStat.mtimeMs };
23955
+ // Appends and equal-size rewrites during a full read both need a retry:
23956
+ // old bytes must not be acknowledged under the rewritten file's metadata.
23957
+ if (
23958
+ finalMetadata.size !== data.length ||
23959
+ finalMetadata.size !== stat.size ||
23960
+ finalMetadata.mtimeMs !== stat.mtimeMs
23961
+ ) finalMetadata.mtimeMs = -1;
23962
+ return { ...finalMetadata, unchanged: false, text: data.toString("utf8") };
23963
+ } catch (error) {
23964
+ if (isCommandCodePathMissing(error)) return null;
23965
+ throw error;
23966
+ } finally {
23967
+ if (handle) await handle.close().catch(() => {});
23968
+ }
23969
+ }
23970
+
23971
+ // Rebuild-and-diff sync for `~/.commandcode/projects/**/*.jsonl`. A record's
23972
+ // identity is `recordId|timestamp`, so a rewritten or forked transcript
23973
+ // reconciles instead of double counting. Missing records retain their history;
23974
+ // only surviving observations can correct it. Cursor state is committed after
23975
+ // both queue appends succeed, so a failed write retries without losing or
23976
+ // inflating usage.
23977
+ async function parseCommandCodeIncremental({
23978
+ sessionFiles,
23979
+ cursors,
23980
+ queuePath,
23981
+ projectQueuePath,
23982
+ onProgress,
23983
+ publicRepoResolver,
23984
+ } = {}) {
23985
+ await ensureDir(path.dirname(queuePath));
23986
+ const files = Array.isArray(sessionFiles) ? sessionFiles : [];
23987
+ // Normalizers retain nested bucket objects. Isolate this provider's working
23988
+ // state so an append failure cannot publish totals or queuedKey changes
23989
+ // through the original cursors before both queues have succeeded.
23990
+ const hourlyState = normalizeHourlyState(structuredClone(cursors?.hourly));
23991
+ const state = normalizeCommandCodeState(cursors?.commandCode);
23992
+ const schemaMigrated = Boolean(cursors?.commandCode &&
23993
+ (cursors.commandCode.version !== COMMAND_CODE_STATE_VERSION ||
23994
+ cursors.commandCode.fileCacheVersion !== COMMAND_CODE_FILE_CACHE_VERSION));
23995
+ const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
23996
+ const projectState = projectEnabled
23997
+ ? normalizeProjectState(structuredClone(cursors?.projectHourly))
23998
+ : null;
23999
+ const projectTouchedBuckets = projectEnabled ? new Set() : null;
24000
+ const projectMetaCache = projectEnabled ? new Map() : null;
24001
+ const publicRepoCache = projectEnabled ? new Map() : null;
24002
+ const touchedBuckets = new Set();
24003
+ const cb = typeof onProgress === "function" ? onProgress : null;
24004
+
24005
+ const currentByKey = new Map();
24006
+ const currentOwners = new Map();
24007
+ const observedLegacyKeys = new Set();
24008
+ const nextFiles = {};
24009
+ const nextFileIndex = {};
24010
+ let recordsProcessed = 0;
24011
+
24012
+ for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
24013
+ const filePath = files[fileIdx];
24014
+ const index = state.fileIndex[filePath];
24015
+ const ownsSnapshot = index?.completeOwnedSnapshot &&
24016
+ index.messageKeys.every((key) => state.messages[key] && !state.messages[key].legacy) &&
24017
+ // A previously uncounted zero can become a correction when another copy
24018
+ // introduces the same identity. Re-read its actual model and counters.
24019
+ !(index.zeroKeys || []).some((key) => state.messages[key] || currentByKey.has(key));
24020
+ const snapshot = await readCommandCodeSessionSnapshot(
24021
+ filePath,
24022
+ ownsSnapshot ? state.files[filePath] : null,
24023
+ projectEnabled && ownsSnapshot ? index.headerRanges : null,
24024
+ );
24025
+ if (!snapshot) continue;
24026
+ nextFiles[filePath] = { size: snapshot.size, mtimeMs: snapshot.mtimeMs };
24027
+
24028
+ const parsed = extractCommandCodeSessionUsage(snapshot.text);
24029
+
24030
+ let projectKey = null;
24031
+ let projectRef = null;
24032
+ if (projectEnabled) {
24033
+ const rawCwd = typeof parsed.cwd === "string" ? parsed.cwd.trim() : "";
24034
+ if (rawCwd) {
24035
+ const startDir = wsl.mapWslCwdToUnc(rawCwd, filePath);
24036
+ const context = await resolveProjectContextForPath({
24037
+ startDir,
24038
+ projectMetaCache,
24039
+ publicRepoCache,
24040
+ publicRepoResolver,
24041
+ projectState,
24042
+ });
24043
+ projectKey = context?.projectKey || null;
24044
+ projectRef = context?.projectRef || null;
24045
+ }
24046
+ }
24047
+
24048
+ if (snapshot.unchanged) {
24049
+ nextFileIndex[filePath] = { ...index };
24050
+ for (const key of index.messageKeys) {
24051
+ const value = state.messages[key];
24052
+ currentByKey.set(key, projectEnabled ? { ...value, projectKey, projectRef } : value);
24053
+ currentOwners.set(key, filePath);
24054
+ }
24055
+ continue;
24056
+ }
24057
+
24058
+ const messageKeys = new Set();
24059
+ for (const record of parsed.records) {
24060
+ recordsProcessed += 1;
24061
+ const key = `${COMMAND_CODE_SOURCE}:${record.id}|${record.timestamp}`;
24062
+ messageKeys.add(key);
24063
+ if (parsed.sessionId) observedLegacyKeys.add(`${COMMAND_CODE_SOURCE}:${parsed.sessionId}|${record.id}`);
24064
+ currentOwners.set(key, filePath);
24065
+ currentByKey.set(key, compactCommandCodeMessage({
24066
+ totals: record.totals,
24067
+ bucketStart: record.bucketStart,
24068
+ model: record.model,
24069
+ projectKey,
24070
+ projectRef,
24071
+ }));
24072
+ }
24073
+ nextFileIndex[filePath] = { messageKeys: [...messageKeys], headerRanges: parsed.headerRanges };
24074
+
24075
+ if (cb && (fileIdx % 25 === 0 || fileIdx === files.length - 1)) {
24076
+ cb({
24077
+ index: fileIdx + 1,
24078
+ total: files.length,
24079
+ messagesProcessed: currentByKey.size,
24080
+ eventsAggregated: 0,
24081
+ bucketsQueued: 0,
24082
+ });
24083
+ }
24084
+ }
24085
+
24086
+ let eventsAggregated = 0;
24087
+
24088
+ function applyObservation(value, subtract = false) {
24089
+ const totals = expandCommandCodeTotals(value.totals);
24090
+ const apply = subtract ? subtractTotals : addTotals;
24091
+ apply(getHourlyBucket(hourlyState, COMMAND_CODE_SOURCE, value.model, value.bucketStart).totals, totals);
24092
+ touchedBuckets.add(bucketKey(COMMAND_CODE_SOURCE, value.model, value.bucketStart));
24093
+ if (projectEnabled && value.projectKey) {
24094
+ apply(getProjectBucket(projectState, value.projectKey, COMMAND_CODE_SOURCE, value.bucketStart, value.projectRef).totals, totals);
24095
+ projectTouchedBuckets.add(projectBucketKey(value.projectKey, COMMAND_CODE_SOURCE, value.bucketStart));
24096
+ }
24097
+ }
24098
+
24099
+ // Legacy session-based identities can only be removed when that session/id
24100
+ // actually survives. An empty or partially compacted source proves nothing.
24101
+ for (const [key, prev] of Object.entries(state.messages)) {
24102
+ if (!prev.legacy || !observedLegacyKeys.has(key)) continue;
24103
+ applyObservation(prev, true);
24104
+ delete state.messages[key];
24105
+ eventsAggregated += 1;
24106
+ }
24107
+
24108
+ for (const [key, prev] of Object.entries(state.messages)) {
24109
+ if (!currentByKey.has(key)) continue;
24110
+ const cur = currentByKey.get(key);
24111
+ const unchanged =
24112
+ totalsKey(expandCommandCodeTotals(prev.totals)) === totalsKey(expandCommandCodeTotals(cur.totals)) &&
24113
+ prev.bucketStart === cur.bucketStart && prev.model === cur.model &&
24114
+ prev.projectKey === cur.projectKey;
24115
+ if (!unchanged) {
24116
+ applyObservation(prev, true);
24117
+ applyObservation(cur);
24118
+ eventsAggregated += 1;
24119
+ }
24120
+ state.messages[key] = cur;
24121
+ }
24122
+
24123
+ for (const [key, cur] of currentByKey) {
24124
+ if (state.messages[key]) continue;
24125
+ // A standalone zero is an empty turn, not a new accounting event. Retain
24126
+ // zeros only as corrections to a previously counted exact-key record.
24127
+ if (expandCommandCodeTotals(cur.totals).total_tokens === 0) continue;
24128
+ applyObservation(cur);
24129
+ state.messages[key] = cur;
24130
+ eventsAggregated += 1;
24131
+ }
24132
+
24133
+ for (const [filePath, index] of Object.entries(nextFileIndex)) {
24134
+ // Check ownership before dropping uncounted keys: a positive record that
24135
+ // lost to a zero in another file is still a non-owning snapshot.
24136
+ const ownsAllRecords = index.messageKeys.every((key) => currentOwners.get(key) === filePath);
24137
+ index.completeOwnedSnapshot = ownsAllRecords;
24138
+ // Retain the identities of uncounted zeros without full message metadata.
24139
+ // They need no body reread until a conflicting observation appears.
24140
+ const zeroKeys = [...new Set([
24141
+ ...(index.zeroKeys || []),
24142
+ ...index.messageKeys.filter((key) => !state.messages[key]),
24143
+ ])];
24144
+ if (zeroKeys.length) index.zeroKeys = zeroKeys;
24145
+ index.messageKeys = index.messageKeys.filter((key) => state.messages[key]);
24146
+ }
24147
+
24148
+ // Ignore cost-only changes (including old display estimates). The zero
24149
+ // sentinel is written only when actual accounting otherwise queues a bucket.
24150
+ function prepareBuckets(touched, next, previous) {
24151
+ for (const key of touched) {
24152
+ const totals = next.buckets[key].totals;
24153
+ const old = previous?.buckets?.[key]?.totals;
24154
+ if (old && totalsKey({ ...old, total_cost_usd: 0 }) === totalsKey({ ...totals, total_cost_usd: 0 })) {
24155
+ touched.delete(key);
24156
+ continue;
24157
+ }
24158
+ totals.total_cost_usd = 0;
24159
+ }
24160
+ }
24161
+ prepareBuckets(touchedBuckets, hourlyState, cursors?.hourly);
24162
+ if (projectEnabled) prepareBuckets(projectTouchedBuckets, projectState, cursors?.projectHourly);
24163
+
24164
+ const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
24165
+ const projectBucketsQueued = projectEnabled
24166
+ ? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
24167
+ : 0;
24168
+
24169
+ const updatedAt = new Date().toISOString();
24170
+ if (bucketsQueued > 0) hourlyState.updatedAt = updatedAt;
24171
+ state.files = nextFiles;
24172
+ state.fileIndex = nextFileIndex;
24173
+ // Non-owning fork copies are reread to resolve conflicts, but those reads
24174
+ // alone need not rewrite an identical persistent ledger and file index.
24175
+ const cursorUnchanged = isDeepStrictEqual(state, cursors.commandCode);
24176
+ cursors.hourly = hourlyState;
24177
+ cursors.commandCode = state;
24178
+ if (projectState) {
24179
+ if (projectBucketsQueued > 0) projectState.updatedAt = updatedAt;
24180
+ cursors.projectHourly = projectState;
24181
+ }
24182
+
24183
+ return {
24184
+ recordsProcessed,
24185
+ messagesProcessed: currentByKey.size,
24186
+ eventsAggregated,
24187
+ bucketsQueued,
24188
+ projectBucketsQueued,
24189
+ schemaMigrated,
24190
+ cursorUnchanged,
24191
+ };
24192
+ }
24193
+
23557
24194
  module.exports = {
23558
24195
  listRolloutFiles,
23559
24196
  listRolloutFilesDeep,
@@ -23792,4 +24429,13 @@ module.exports = {
23792
24429
  dshUsageToTotals,
23793
24430
  extractDshSessionUsage,
23794
24431
  parseDshIncremental,
24432
+ // Command Code (`cmd`) — passive session-log reader
24433
+ resolveCommandCodeHome,
24434
+ resolveCommandCodeHomes,
24435
+ resolveCommandCodeSessionFiles,
24436
+ isCommandCodeSessionLogName,
24437
+ normalizeCommandCodeModelName,
24438
+ commandCodeUsageToTotals,
24439
+ extractCommandCodeSessionUsage,
24440
+ parseCommandCodeIncremental,
23795
24441
  };