@orquentra/codebuddy 2.8.0 → 2.14.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.
Files changed (73) hide show
  1. package/CLAUDE_CODEX.md +33 -0
  2. package/README.md +27 -3
  3. package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
  4. package/dist/{chunk-GKNYMNFM.js → chunk-4DKYKDHI.js} +6 -1
  5. package/dist/chunk-4DKYKDHI.js.map +1 -0
  6. package/dist/{chunk-6KGYAFG7.js → chunk-7QMHIJBA.js} +30 -3
  7. package/dist/chunk-7QMHIJBA.js.map +1 -0
  8. package/dist/{chunk-6NZEZJYD.js → chunk-ESC4Y3L7.js} +3 -5
  9. package/dist/{chunk-6NZEZJYD.js.map → chunk-ESC4Y3L7.js.map} +1 -1
  10. package/dist/chunk-GQKWUHMA.js +20 -0
  11. package/dist/chunk-GQKWUHMA.js.map +1 -0
  12. package/dist/{chunk-J4T6HHI4.js → chunk-L6BRYZ6G.js} +537 -316
  13. package/dist/chunk-L6BRYZ6G.js.map +1 -0
  14. package/dist/{chunk-UK6XTS2Y.js → chunk-LRQNPYCK.js} +13 -19
  15. package/dist/chunk-LRQNPYCK.js.map +1 -0
  16. package/dist/{chunk-GXOML4UZ.js → chunk-LW75SKZR.js} +2 -2
  17. package/dist/{chunk-EJQVGFH2.js → chunk-UOTKHSQ5.js} +26 -9
  18. package/dist/chunk-UOTKHSQ5.js.map +1 -0
  19. package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
  20. package/dist/chunk-V7PT6ER5.js.map +1 -0
  21. package/dist/{chunk-ZNLSCV3J.js → chunk-XSNW6Y3X.js} +1253 -315
  22. package/dist/chunk-XSNW6Y3X.js.map +1 -0
  23. package/dist/{chunk-6DONJW3W.js → chunk-YJPCS2GP.js} +274 -69
  24. package/dist/chunk-YJPCS2GP.js.map +1 -0
  25. package/dist/{chunk-XL2HLACE.js → chunk-YN7TEIUH.js} +1 -1
  26. package/dist/chunk-YN7TEIUH.js.map +1 -0
  27. package/dist/{chunk-TLWGZVPC.js → chunk-YNSTIMCF.js} +746 -66
  28. package/dist/chunk-YNSTIMCF.js.map +1 -0
  29. package/dist/{chunk-NY2YXP3R.js → chunk-YWDH6DQ6.js} +70 -4
  30. package/dist/chunk-YWDH6DQ6.js.map +1 -0
  31. package/dist/cli/index.js +23 -9
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
  34. package/dist/index.d.ts +6 -4
  35. package/dist/index.js +9 -9
  36. package/dist/install-EU35ERK5.js +30 -0
  37. package/dist/langgraph/index.d.ts +1 -1
  38. package/dist/lean-43ADYTBW.js +28 -0
  39. package/dist/{main-QUASSFP7.js → main-SFC5LV4E.js} +1333 -437
  40. package/dist/main-SFC5LV4E.js.map +1 -0
  41. package/dist/{operations-WJR7AZZF.js → operations-6Z6ZDVBP.js} +3 -3
  42. package/dist/{pretool-command-JTKX6G3M.js → pretool-command-B5GY5T6E.js} +3 -3
  43. package/dist/prompt-gate-TDTFFQW2.js +8 -0
  44. package/dist/{runtime-OQQ22VJF.js → runtime-W5AW32SD.js} +15 -7
  45. package/dist/{summaries-ML53R7EL.js → summaries-32PHOB6L.js} +3 -4
  46. package/dist/{zvec-XDZNZ2OL.js → zvec-24HLAFGN.js} +3 -4
  47. package/dist/zvec-24HLAFGN.js.map +1 -0
  48. package/package.json +11 -2
  49. package/dist/chunk-6DONJW3W.js.map +0 -1
  50. package/dist/chunk-6KGYAFG7.js.map +0 -1
  51. package/dist/chunk-EJQVGFH2.js.map +0 -1
  52. package/dist/chunk-GKNYMNFM.js.map +0 -1
  53. package/dist/chunk-ITQO632W.js +0 -673
  54. package/dist/chunk-ITQO632W.js.map +0 -1
  55. package/dist/chunk-J4T6HHI4.js.map +0 -1
  56. package/dist/chunk-NY2YXP3R.js.map +0 -1
  57. package/dist/chunk-TLWGZVPC.js.map +0 -1
  58. package/dist/chunk-UK6XTS2Y.js.map +0 -1
  59. package/dist/chunk-W7GAXL6C.js.map +0 -1
  60. package/dist/chunk-XL2HLACE.js.map +0 -1
  61. package/dist/chunk-ZNLSCV3J.js.map +0 -1
  62. package/dist/install-BXM56QCD.js +0 -19
  63. package/dist/lean-command-7DFW32MA.js +0 -19
  64. package/dist/main-QUASSFP7.js.map +0 -1
  65. /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
  66. /package/dist/{chunk-GXOML4UZ.js.map → chunk-LW75SKZR.js.map} +0 -0
  67. /package/dist/{install-BXM56QCD.js.map → install-EU35ERK5.js.map} +0 -0
  68. /package/dist/{lean-command-7DFW32MA.js.map → lean-43ADYTBW.js.map} +0 -0
  69. /package/dist/{operations-WJR7AZZF.js.map → operations-6Z6ZDVBP.js.map} +0 -0
  70. /package/dist/{pretool-command-JTKX6G3M.js.map → pretool-command-B5GY5T6E.js.map} +0 -0
  71. /package/dist/{runtime-OQQ22VJF.js.map → prompt-gate-TDTFFQW2.js.map} +0 -0
  72. /package/dist/{summaries-ML53R7EL.js.map → runtime-W5AW32SD.js.map} +0 -0
  73. /package/dist/{zvec-XDZNZ2OL.js.map → summaries-32PHOB6L.js.map} +0 -0
@@ -1,10 +1,16 @@
1
1
  import {
2
- renderLeanBlock,
3
- runLeanHookCommand
4
- } from "./chunk-UK6XTS2Y.js";
2
+ isDockerDaemonRunning,
3
+ tryStartDockerDesktop,
4
+ waitForDockerDaemon
5
+ } from "./chunk-5UH3BEQR.js";
6
+ import {
7
+ postgresDown,
8
+ postgresStatus,
9
+ postgresUp
10
+ } from "./chunk-CSRD5OQA.js";
5
11
  import {
6
12
  runPreToolHookCommand
7
- } from "./chunk-EJQVGFH2.js";
13
+ } from "./chunk-UOTKHSQ5.js";
8
14
  import {
9
15
  failSoft,
10
16
  readHookInput,
@@ -13,6 +19,7 @@ import {
13
19
  import {
14
20
  WORKFLOW_CLIENTS,
15
21
  clientDisplayName,
22
+ clientHookCapabilities,
16
23
  clientHooksInstalled,
17
24
  defaultRulesMode,
18
25
  detectClient,
@@ -29,10 +36,12 @@ import {
29
36
  removeCodexEntry,
30
37
  renderManagedBlock,
31
38
  renderWorkflowTemplate,
39
+ runBridgeHook,
40
+ runCodexHook,
32
41
  setHooks,
33
42
  uninstallClient,
34
43
  writeProjectMcpConfig
35
- } from "./chunk-J4T6HHI4.js";
44
+ } from "./chunk-L6BRYZ6G.js";
36
45
  import {
37
46
  AUTOMATION_MODES,
38
47
  CLIENT_ADAPTERS,
@@ -42,19 +51,14 @@ import {
42
51
  parseClient,
43
52
  parseMode,
44
53
  supportedModes
45
- } from "./chunk-W7GAXL6C.js";
46
- import {
47
- isDockerDaemonRunning,
48
- tryStartDockerDesktop,
49
- waitForDockerDaemon
50
- } from "./chunk-5UH3BEQR.js";
54
+ } from "./chunk-V7PT6ER5.js";
51
55
  import {
52
- postgresDown,
53
- postgresStatus,
54
- postgresUp
55
- } from "./chunk-CSRD5OQA.js";
56
+ renderLeanBlock,
57
+ runLeanHook
58
+ } from "./chunk-LRQNPYCK.js";
56
59
  import {
57
60
  TELEMETRY_NOTICE,
61
+ findReuse,
58
62
  flushTelemetry,
59
63
  generateSuggestions,
60
64
  migrateMemoryToFiles,
@@ -65,15 +69,17 @@ import {
65
69
  telemetryBlocked,
66
70
  telemetryStatePath,
67
71
  telemetryUrl,
72
+ traceCliCommand,
68
73
  watchRepository,
69
74
  writeTelemetryState
70
- } from "./chunk-6DONJW3W.js";
75
+ } from "./chunk-YJPCS2GP.js";
71
76
  import {
72
77
  DEFAULT_ZVEC_MODEL,
73
78
  buildZvecIndex,
74
79
  zvecStatus
75
- } from "./chunk-GXOML4UZ.js";
80
+ } from "./chunk-LW75SKZR.js";
76
81
  import {
82
+ HookStagingStore,
77
83
  PlanFileStore,
78
84
  PlanLifecycle,
79
85
  ReviewStore,
@@ -82,28 +88,40 @@ import {
82
88
  assessRisk,
83
89
  buildBeforeEditContext,
84
90
  buildBootstrapContext,
91
+ findCodexRollouts,
92
+ forgetJournal,
93
+ importCodexRollout,
94
+ journalLag,
95
+ latestUnfinished,
96
+ listJournals,
85
97
  openHookSemantics,
98
+ readCheckpoint,
99
+ renderCheckpointNow,
86
100
  renderContextBlock,
87
101
  resolveRiskPaths,
88
102
  runCaptureHook,
103
+ runCheckpointHook,
89
104
  runContextHook,
90
105
  runResetHook,
106
+ runResumeHook,
91
107
  runStageHook,
92
- runSyncHook
93
- } from "./chunk-ZNLSCV3J.js";
94
- import {
95
- SessionCapsuleLedger,
96
- detectTestCommand,
97
- estimateTokens
98
- } from "./chunk-GKNYMNFM.js";
108
+ runSyncHook,
109
+ syncReuseVectors
110
+ } from "./chunk-XSNW6Y3X.js";
99
111
  import {
100
112
  parseTraceLines,
101
113
  readTraces,
102
114
  tracePath
103
- } from "./chunk-XL2HLACE.js";
115
+ } from "./chunk-YN7TEIUH.js";
116
+ import {
117
+ SessionCapsuleLedger,
118
+ detectTestCommand,
119
+ estimateTokens
120
+ } from "./chunk-4DKYKDHI.js";
104
121
  import {
105
122
  hookRoot
106
123
  } from "./chunk-TGFTKWOR.js";
124
+ import "./chunk-GQKWUHMA.js";
107
125
  import "./chunk-DFV2SK6C.js";
108
126
  import {
109
127
  bootstrapDatabase,
@@ -116,11 +134,11 @@ import {
116
134
  pingDatabase,
117
135
  runDoctor,
118
136
  runMigrations
119
- } from "./chunk-NY2YXP3R.js";
137
+ } from "./chunk-YWDH6DQ6.js";
120
138
  import {
121
139
  MemoryFileStore,
122
140
  VERSION
123
- } from "./chunk-6KGYAFG7.js";
141
+ } from "./chunk-7QMHIJBA.js";
124
142
  import {
125
143
  computeFactHash,
126
144
  generateEntityId
@@ -133,25 +151,24 @@ import {
133
151
  import {
134
152
  loadLlmSummaries,
135
153
  summarizeRepository
136
- } from "./chunk-6NZEZJYD.js";
154
+ } from "./chunk-ESC4Y3L7.js";
137
155
  import {
138
156
  chatJson,
139
157
  coolingDown,
140
158
  usageToday
141
159
  } from "./chunk-ESZXCZIN.js";
142
- import {
143
- generateGraphifyGraph,
144
- loadGraphifyMap,
145
- setupGraphify,
146
- syncFileVectors
147
- } from "./chunk-TLWGZVPC.js";
148
160
  import {
149
161
  buildArchitectureMap,
162
+ generateGraphifyGraph,
150
163
  indexRepository,
151
164
  loadFreshIndex,
165
+ loadGraphifyMap,
166
+ loadProjectArchitectureMap,
152
167
  neighbours,
153
- queryMap
154
- } from "./chunk-ITQO632W.js";
168
+ queryMap,
169
+ setupGraphify,
170
+ syncFileVectors
171
+ } from "./chunk-YNSTIMCF.js";
155
172
  import {
156
173
  extractSymbolTable,
157
174
  supportsSymbols
@@ -186,12 +203,14 @@ import {
186
203
  setZvecEnabled,
187
204
  writeConfigFile
188
205
  } from "./chunk-KNR2P2UY.js";
189
- import "./chunk-CG5B3LYG.js";
206
+ import {
207
+ writeAtomic
208
+ } from "./chunk-CG5B3LYG.js";
190
209
  import "./chunk-PZ5AY32C.js";
191
210
 
192
211
  // src/cli/main.ts
193
212
  import { Command } from "commander";
194
- import pc23 from "picocolors";
213
+ import pc25 from "picocolors";
195
214
 
196
215
  // src/integrations/github.ts
197
216
  import { constants } from "fs";
@@ -769,6 +788,7 @@ var CONFORMANCE = {
769
788
  const servers = json(result.stdout);
770
789
  if (!Array.isArray(servers)) return [fail("mcp", "codex mcp list --json returned no JSON")];
771
790
  return [
791
+ await clientHooksInstalled(root, "codex") ? pass("hooks", ".codex/hooks.json is installed; review trust in Codex /hooks") : skip("hooks", "not installed in this repo"),
772
792
  servers.some((server) => server.name === name2) ? pass("mcp", `codex mcp list has ${name2}`) : skip("mcp", `no ${name2} entry (clients install codex --global adds it)`)
773
793
  ];
774
794
  },
@@ -862,7 +882,7 @@ function registerClientsCommands(program, runSafely2) {
862
882
  console.log(pc2.dim("Per-turn token cost per client: codebuddy clients cost"));
863
883
  });
864
884
  });
865
- clients.command("install").argument("<client>", `One of: ${CLIENT_IDS.join(", ")}.`).requiredOption("--mode <mode>", `One of: ${AUTOMATION_MODES.join(", ")}.`).option("--namespace <name>", "Namespace for the MCP entry (defaults to config/folder).").option("--global", "Also add the MCP entry to the global config (Codex, Hermes).").description("Apply one automation mode: rules block, hooks, and MCP config.").action(
885
+ clients.command("install").argument("<client>", `One of: ${CLIENT_IDS.join(", ")}.`).requiredOption("--mode <mode>", `One of: ${AUTOMATION_MODES.join(", ")}.`).option("--namespace <name>", "Namespace for the MCP entry (defaults to config/folder).").option("--global", "Also add the MCP entry to the global config (Codex, Hermes).").option("--capture", "Capture turns into memory even in lean mode (Claude Code).").option("--no-capture", "Inject context without capturing turns (Claude Code).").description("Apply one automation mode: rules block, hooks, and MCP config.").action(
866
886
  async (client, opts) => {
867
887
  await runSafely2(async () => {
868
888
  const mode = parseMode(opts.mode);
@@ -870,7 +890,8 @@ function registerClientsCommands(program, runSafely2) {
870
890
  client: parseClient(client),
871
891
  mode,
872
892
  ...opts.namespace ? { namespace: opts.namespace } : {},
873
- ...opts.global ? { global: true } : {}
893
+ ...opts.global ? { global: true } : {},
894
+ ...opts.capture !== void 0 ? { capture: opts.capture } : {}
874
895
  });
875
896
  printReport2(report, `${MODE_LABELS[mode]} mode`);
876
897
  });
@@ -982,6 +1003,30 @@ function registerContextCommands(program, runSafely2) {
982
1003
  printBootstrap(result);
983
1004
  });
984
1005
  });
1006
+ context.command("reuse").argument("<task...>", "What you are about to implement.").option("--json", "Print machine-readable JSON.").description(
1007
+ "Existing helpers to reuse for a task: signature, a call site and the covering test."
1008
+ ).action(async (taskParts, opts) => {
1009
+ await runSafely2(async () => {
1010
+ const root = process.cwd();
1011
+ const { manifest } = await loadFreshIndex(root);
1012
+ const picks = await findReuse(
1013
+ root,
1014
+ taskParts.join(" "),
1015
+ manifest,
1016
+ await loadProjectArchitectureMap(root, manifest)
1017
+ );
1018
+ if (opts.json) {
1019
+ console.log(JSON.stringify(picks, null, 2));
1020
+ return;
1021
+ }
1022
+ if (picks.length === 0) console.log(pc3.dim("no existing helper clearly matches"));
1023
+ for (const pick of picks) {
1024
+ console.log(`${pick.path}:${pick.startLine}-${pick.endLine} ${pick.signature}`);
1025
+ if (pick.usage) console.log(pc3.dim(` used at ${pick.usage}`));
1026
+ if (pick.test) console.log(pc3.dim(` test ${pick.test}`));
1027
+ }
1028
+ });
1029
+ });
985
1030
  context.command("preview").argument("[task...]", "The task you are about to work on.").option("--paths <paths>", "Comma-separated repo-relative paths to target.").option("--plan <id>", "Resolve target files from a CodeBuddy plan.").option("--git", "Resolve target files from current git changes.").option("--json", "Print machine-readable JSON.").description("Show the context CodeBuddy would assemble before editing the given files.").action(
986
1031
  async (taskParts, opts) => {
987
1032
  await runSafely2(async () => {
@@ -1199,7 +1244,7 @@ th.n { text-align: right; }
1199
1244
  .chip { display: inline-block; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; }
1200
1245
  .chip.recall { background: var(--recall-soft); color: var(--recall); }
1201
1246
  .chip.tool { background: var(--tool-soft); color: var(--tool); }
1202
- .chip.capture, .chip.sync, .chip.guard, .chip.kw { background: var(--line); color: var(--capture); }
1247
+ .chip.capture, .chip.sync, .chip.guard, .chip.cli, .chip.kw { background: var(--line); color: var(--capture); }
1203
1248
  .chip.err { background: var(--bad-soft); color: var(--bad); }
1204
1249
  .feed { display: grid; gap: 10px; max-height: 520px; overflow: auto; }
1205
1250
  .prompt { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
@@ -1719,15 +1764,22 @@ function registerGitHubCommands(program, runSafely2) {
1719
1764
  import { execFile as execFile2, spawn as spawn3 } from "child_process";
1720
1765
  import { relative as relative2 } from "path";
1721
1766
  import pc7 from "picocolors";
1767
+
1768
+ // src/hooks/lean-command.ts
1769
+ async function runLeanHookCommand() {
1770
+ await failSoft(async () => {
1771
+ await runLeanHook(await readHookInput(), writeStdout);
1772
+ });
1773
+ }
1774
+
1775
+ // src/cli/commands/hooks.ts
1722
1776
  function registerHooksCommands(program, runSafely2) {
1723
1777
  const hooks = program.command("hooks").description("Enforce automatic recall/capture via client lifecycle hooks.");
1724
1778
  hooks.command("install").option(
1725
1779
  "--client <client>",
1726
- "Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
1780
+ "Client with a CodeBuddy hook adapter (claude, codex, opencode, hermes).",
1727
1781
  "claude"
1728
- ).description(
1729
- "Add CodeBuddy's recall/capture hooks (.claude/settings.json, .opencode/plugins/codebuddy.js or .hermes/plugins/codebuddy/)."
1730
- ).action(async (opts) => {
1782
+ ).description("Add CodeBuddy's recall/capture hooks for the selected client.").action(async (opts) => {
1731
1783
  await runSafely2(async () => {
1732
1784
  const client = assertHookClient(opts.client);
1733
1785
  const step = await setHooks(process.cwd(), true, client);
@@ -1749,7 +1801,7 @@ function registerHooksCommands(program, runSafely2) {
1749
1801
  });
1750
1802
  hooks.command("uninstall").option(
1751
1803
  "--client <client>",
1752
- "Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
1804
+ "Client with a CodeBuddy hook adapter (claude, codex, opencode, hermes).",
1753
1805
  "claude"
1754
1806
  ).description("Remove CodeBuddy's hooks for a client (user hooks untouched).").action(async (opts) => {
1755
1807
  await runSafely2(async () => {
@@ -1763,14 +1815,43 @@ function registerHooksCommands(program, runSafely2) {
1763
1815
  });
1764
1816
  hooks.command("status").option(
1765
1817
  "--client <client>",
1766
- "Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
1818
+ "Client with a CodeBuddy hook adapter (claude, codex, opencode, hermes).",
1767
1819
  "claude"
1768
- ).description("Report whether CodeBuddy hooks are installed.").action(async (opts) => {
1820
+ ).description("Report which CodeBuddy hooks are installed and what they do.").action(async (opts) => {
1769
1821
  await runSafely2(async () => {
1770
- const installed = await clientHooksInstalled(process.cwd(), parseClient(opts.client));
1822
+ const caps = await clientHookCapabilities(process.cwd(), parseClient(opts.client));
1823
+ if (!caps?.injection) {
1824
+ console.log(pc7.dim("CodeBuddy hooks: not installed"));
1825
+ return;
1826
+ }
1827
+ console.log(pc7.green("CodeBuddy hooks: installed"));
1828
+ console.log(` injection: ${caps.injection}`);
1829
+ console.log(
1830
+ ` capture: ${caps.capture ? "on (edits staged, turn end captured)" : "off (turns are not captured into memory)"}`
1831
+ );
1771
1832
  console.log(
1772
- installed ? pc7.green("CodeBuddy hooks: installed") : pc7.dim("CodeBuddy hooks: not installed")
1833
+ ` recovery: ${caps.recovery ? "on (pre-compaction checkpoint, unfinished-task resume)" : "off"}`
1773
1834
  );
1835
+ console.log(` compaction reset: ${caps.reset ? "on" : "off"}`);
1836
+ console.log(` read guard: ${caps.guard ? "on" : "off"}`);
1837
+ });
1838
+ });
1839
+ hooks.command("codex").option("--injection <mode>", "full or lean", "full").option("--capture", "Capture visible lifecycle events", false).description("[hook] Native Codex event adapter (JSON output, no permission changes).").action(async (opts) => {
1840
+ let result = {};
1841
+ await failSoft(async () => {
1842
+ if (opts.injection !== "full" && opts.injection !== "lean")
1843
+ throw new Error("injection must be full or lean");
1844
+ result = await runCodexHook(
1845
+ await readHookInput(),
1846
+ opts.injection,
1847
+ opts.capture
1848
+ );
1849
+ });
1850
+ await writeStdout(JSON.stringify(result));
1851
+ });
1852
+ hooks.command("bridge").description("[hook] Capture normalized host events and restore checkpoint context.").action(async () => {
1853
+ await failSoft(async () => {
1854
+ await runBridgeHook(await readHookInput(), writeStdout);
1774
1855
  });
1775
1856
  });
1776
1857
  hooks.command("context").description("[hook] Emit compact project context for injection at prompt submit.").action(async () => {
@@ -1823,6 +1904,16 @@ function registerHooksCommands(program, runSafely2) {
1823
1904
  await runResetHook(await readHookInput(), writeStdout);
1824
1905
  });
1825
1906
  });
1907
+ hooks.command("checkpoint").description("[hook] Journal the transcript before compaction (PreCompact).").action(async () => {
1908
+ await failSoft(async () => {
1909
+ await runCheckpointHook(await readHookInput());
1910
+ });
1911
+ });
1912
+ hooks.command("resume").description("[hook] Print another session's unfinished task checkpoint, if any.").action(async () => {
1913
+ await failSoft(async () => {
1914
+ await runResumeHook(await readHookInput(), writeStdout);
1915
+ });
1916
+ });
1826
1917
  }
1827
1918
  function assertHookClient(value) {
1828
1919
  const client = parseClient(value);
@@ -2009,9 +2100,21 @@ function registerIndexCommands(program, runSafely2) {
2009
2100
  await runSafely2(async () => {
2010
2101
  const result = await indexRepository({ ...opts.full ? { full: true } : {} });
2011
2102
  const local = opts.vectors && (await loadRuntimeConfig(process.cwd(), { requireDatabase: false }).catch(() => null))?.provider.type === "local";
2103
+ const vectorStarted = Date.now();
2012
2104
  const vectors = local ? await syncFileVectors(process.cwd(), VECTOR_BUDGET_MS) : null;
2105
+ const reuseVectors = local ? await syncReuseVectors(process.cwd(), VECTOR_BUDGET_MS - (Date.now() - vectorStarted)) : null;
2013
2106
  if (opts.json) {
2014
- console.log(JSON.stringify({ ...result, ...vectors ? { vectors } : {} }, null, 2));
2107
+ console.log(
2108
+ JSON.stringify(
2109
+ {
2110
+ ...result,
2111
+ ...vectors ? { vectors } : {},
2112
+ ...reuseVectors ? { reuseVectors } : {}
2113
+ },
2114
+ null,
2115
+ 2
2116
+ )
2117
+ );
2015
2118
  return;
2016
2119
  }
2017
2120
  printResult(result);
@@ -2201,8 +2304,59 @@ async function runInitWizard() {
2201
2304
  }
2202
2305
  }
2203
2306
 
2204
- // src/cli/commands/learn.ts
2307
+ // src/cli/commands/journal.ts
2205
2308
  import pc10 from "picocolors";
2309
+ function registerJournalCommands(program, runSafely2) {
2310
+ const journal = program.command("journal").description("Inspect or delete session journals and active-task checkpoints.");
2311
+ journal.command("list", { isDefault: true }).description("List sessions with a checkpoint, newest first.").action(async () => {
2312
+ await runSafely2(async () => {
2313
+ const sessions = await listJournals(process.cwd());
2314
+ if (sessions.length === 0) console.log(pc10.dim("no session journals yet"));
2315
+ for (const { session, updatedAt } of sessions) console.log(`${updatedAt} ${session}`);
2316
+ });
2317
+ });
2318
+ journal.command("show").argument("<session>").description("Print a session's active-task checkpoint as JSON.").action(async (session) => {
2319
+ await runSafely2(async () => {
2320
+ const checkpoint = await readCheckpoint(process.cwd(), session);
2321
+ console.log(checkpoint ? JSON.stringify(checkpoint, null, 2) : pc10.dim("no checkpoint"));
2322
+ });
2323
+ });
2324
+ journal.command("resume").description("Print the newest unfinished task checkpoint (any session or agent).").action(async () => {
2325
+ await runSafely2(async () => {
2326
+ const checkpoint = await latestUnfinished(process.cwd(), void 0);
2327
+ console.log(
2328
+ checkpoint ? await renderCheckpointNow(
2329
+ process.cwd(),
2330
+ checkpoint,
2331
+ `unfinished, from session ${checkpoint.session}`
2332
+ ) : pc10.dim("no unfinished task in the last 3 days")
2333
+ );
2334
+ });
2335
+ });
2336
+ journal.command("import-codex").option("--all", "Import every Codex rollout for this repository, not only the newest.").description("Journal Codex rollouts (~/.codex/sessions) whose working directory is this repo.").action(async (opts) => {
2337
+ await runSafely2(async () => {
2338
+ const rollouts = await findCodexRollouts(process.cwd());
2339
+ if (rollouts.length === 0) {
2340
+ console.log(pc10.dim("no Codex rollout for this repository"));
2341
+ return;
2342
+ }
2343
+ for (const rollout of opts.all ? rollouts : rollouts.slice(0, 1)) {
2344
+ const result = await importCodexRollout(process.cwd(), rollout);
2345
+ console.log(`codex-${rollout.id}: ${result ? `${result.added} new events` : "busy"}`);
2346
+ }
2347
+ });
2348
+ });
2349
+ journal.command("forget").argument("[session]").option("--all", "Delete every session's journal.").description("Delete a session's journal, cursor and checkpoint.").action(async (session, opts) => {
2350
+ await runSafely2(async () => {
2351
+ if (!session && !opts.all) throw new Error("Name a session or pass --all.");
2352
+ await forgetJournal(process.cwd(), opts.all ? void 0 : session);
2353
+ console.log(pc10.yellow(opts.all ? "deleted all session journals" : `deleted ${session}`));
2354
+ });
2355
+ });
2356
+ }
2357
+
2358
+ // src/cli/commands/learn.ts
2359
+ import pc11 from "picocolors";
2206
2360
 
2207
2361
  // src/learning/outcomes.ts
2208
2362
  async function recordChangeOutcome(input) {
@@ -2263,15 +2417,15 @@ function registerLearnCommand(program, runSafely2) {
2263
2417
  ...opts.commit ? { commit: opts.commit } : {},
2264
2418
  ...opts.plan ? { planId: opts.plan } : {}
2265
2419
  });
2266
- console.log(`${pc10.green("recorded")} ${result.id} (${result.category})`);
2267
- console.log(pc10.dim(result.path));
2420
+ console.log(`${pc11.green("recorded")} ${result.id} (${result.category})`);
2421
+ console.log(pc11.dim(result.path));
2268
2422
  });
2269
2423
  }
2270
2424
  );
2271
2425
  }
2272
2426
 
2273
2427
  // src/cli/commands/map.ts
2274
- import pc11 from "picocolors";
2428
+ import pc12 from "picocolors";
2275
2429
  function registerMapCommands(program, runSafely2) {
2276
2430
  const map = program.command("map").description("Query the lightweight architecture map.");
2277
2431
  map.command("build").description("Scan the current repository and summarize the module graph.").action(async () => {
@@ -2291,7 +2445,7 @@ function registerMapCommands(program, runSafely2) {
2291
2445
  return;
2292
2446
  }
2293
2447
  if (edges.length === 0) {
2294
- console.log(pc11.dim("no dependencies found"));
2448
+ console.log(pc12.dim("no dependencies found"));
2295
2449
  return;
2296
2450
  }
2297
2451
  for (const edge of edges) console.log(`${edge.from} -> ${edge.to}`);
@@ -2306,7 +2460,7 @@ function registerMapCommands(program, runSafely2) {
2306
2460
  return;
2307
2461
  }
2308
2462
  if (result.edges.length === 0) {
2309
- console.log(pc11.dim("no dependents found"));
2463
+ console.log(pc12.dim("no dependents found"));
2310
2464
  return;
2311
2465
  }
2312
2466
  for (const edge of result.edges) console.log(`${edge.from} -> ${edge.to}`);
@@ -2317,7 +2471,7 @@ function registerMapCommands(program, runSafely2) {
2317
2471
  // src/cli/commands/memory-review.ts
2318
2472
  import { spawn as spawn4 } from "child_process";
2319
2473
  import { relative as relative3 } from "path";
2320
- import pc12 from "picocolors";
2474
+ import pc13 from "picocolors";
2321
2475
  function registerMemoryCommands(program, runSafely2) {
2322
2476
  const memory = program.command("memory").description("Inspect and curate CodeBuddy memory.");
2323
2477
  const review = memory.command("review").description("Review memory candidates captured automatically after turns.");
@@ -2329,11 +2483,11 @@ function registerMemoryCommands(program, runSafely2) {
2329
2483
  return;
2330
2484
  }
2331
2485
  if (items.length === 0) {
2332
- console.log(pc12.dim("no pending review items"));
2486
+ console.log(pc13.dim("no pending review items"));
2333
2487
  return;
2334
2488
  }
2335
2489
  for (const item of items) console.log(renderRow(item));
2336
- console.log(pc12.dim(`
2490
+ console.log(pc13.dim(`
2337
2491
  ${items.length} pending. approve <id> | reject <id> | show <id>`));
2338
2492
  });
2339
2493
  });
@@ -2347,7 +2501,7 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
2347
2501
  const factId = await approveReviewItem(id, {
2348
2502
  ...opts.incident !== void 0 ? { incidentId: opts.incident } : {}
2349
2503
  });
2350
- console.log(`${pc12.green("approved")} ${id} \u2192 ${factId}`);
2504
+ console.log(`${pc13.green("approved")} ${id} \u2192 ${factId}`);
2351
2505
  });
2352
2506
  });
2353
2507
  review.command("reject").argument("<id>").description("Discard a review candidate without saving it.").action(async (id) => {
@@ -2355,7 +2509,7 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
2355
2509
  const store = new ReviewStore();
2356
2510
  await store.read(id);
2357
2511
  await store.delete(id);
2358
- console.log(`${pc12.yellow("rejected")} ${id}`);
2512
+ console.log(`${pc13.yellow("rejected")} ${id}`);
2359
2513
  });
2360
2514
  });
2361
2515
  review.command("edit").argument("<id>").description("Open a review candidate in $EDITOR and re-validate on save.").action(async (id) => {
@@ -2364,44 +2518,44 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
2364
2518
  const item = await store.read(id);
2365
2519
  await openEditor(item.path);
2366
2520
  await store.read(id);
2367
- console.log(pc12.dim("saved and validated"));
2521
+ console.log(pc13.dim("saved and validated"));
2368
2522
  });
2369
2523
  });
2370
2524
  }
2371
2525
  function renderRow(item) {
2372
- const flag = item.sensitive ? pc12.red(" \u26A0 sensitive") : "";
2373
- return `${classColor(item.class)} ${pc12.dim(item.id)} ${truncate(item.content, 70)}${flag}`;
2526
+ const flag = item.sensitive ? pc13.red(" \u26A0 sensitive") : "";
2527
+ return `${classColor(item.class)} ${pc13.dim(item.id)} ${truncate(item.content, 70)}${flag}`;
2374
2528
  }
2375
2529
  function renderItem(item) {
2376
2530
  const lines = [
2377
- `${classColor(item.class)} ${pc12.dim(item.id)}`,
2378
- `${pc12.bold("reason:")} ${item.reason}`,
2379
- `${pc12.bold("confidence:")} ${item.confidence.toFixed(2)}`,
2380
- `${pc12.bold("triple:")} ${item.subject} \xB7 ${item.predicate} \xB7 ${item.object}`
2531
+ `${classColor(item.class)} ${pc13.dim(item.id)}`,
2532
+ `${pc13.bold("reason:")} ${item.reason}`,
2533
+ `${pc13.bold("confidence:")} ${item.confidence.toFixed(2)}`,
2534
+ `${pc13.bold("triple:")} ${item.subject} \xB7 ${item.predicate} \xB7 ${item.object}`
2381
2535
  ];
2382
- if (item.paths.length > 0) lines.push(`${pc12.bold("paths:")} ${item.paths.join(", ")}`);
2383
- if (item.severity) lines.push(`${pc12.bold("severity:")} ${item.severity}`);
2536
+ if (item.paths.length > 0) lines.push(`${pc13.bold("paths:")} ${item.paths.join(", ")}`);
2537
+ if (item.severity) lines.push(`${pc13.bold("severity:")} ${item.severity}`);
2384
2538
  if (item.resolvesIncidents) {
2385
2539
  lines.push(
2386
- `${pc12.bold("resolves:")} ${item.resolvesIncidents.join(", ") || "(no candidate; approve with --incident <id>)"}`
2540
+ `${pc13.bold("resolves:")} ${item.resolvesIncidents.join(", ") || "(no candidate; approve with --incident <id>)"}`
2387
2541
  );
2388
2542
  }
2389
- if (item.sensitive) lines.push(pc12.red(`\u26A0 sensitive: ${item.sensitiveReasons.join(", ")}`));
2390
- lines.push("", item.content, "", pc12.dim(relative3(process.cwd(), item.path)));
2543
+ if (item.sensitive) lines.push(pc13.red(`\u26A0 sensitive: ${item.sensitiveReasons.join(", ")}`));
2544
+ lines.push("", item.content, "", pc13.dim(relative3(process.cwd(), item.path)));
2391
2545
  return lines.join("\n");
2392
2546
  }
2393
2547
  function classColor(memoryClass) {
2394
2548
  switch (memoryClass) {
2395
2549
  case "incident":
2396
- return pc12.red(memoryClass);
2550
+ return pc13.red(memoryClass);
2397
2551
  case "decision":
2398
- return pc12.cyan(memoryClass);
2552
+ return pc13.cyan(memoryClass);
2399
2553
  case "fact":
2400
- return pc12.green(memoryClass);
2554
+ return pc13.green(memoryClass);
2401
2555
  case "note":
2402
- return pc12.yellow(memoryClass);
2556
+ return pc13.yellow(memoryClass);
2403
2557
  default:
2404
- return pc12.dim(memoryClass);
2558
+ return pc13.dim(memoryClass);
2405
2559
  }
2406
2560
  }
2407
2561
  function truncate(value, max) {
@@ -2421,7 +2575,7 @@ function openEditor(path) {
2421
2575
  import { existsSync } from "fs";
2422
2576
  import { join as join5 } from "path";
2423
2577
  import { spinner as spinner2 } from "@clack/prompts";
2424
- import pc13 from "picocolors";
2578
+ import pc14 from "picocolors";
2425
2579
 
2426
2580
  // evals/fact-relevance.json
2427
2581
  var fact_relevance_default = {
@@ -2685,9 +2839,9 @@ async function calibrateEmbedder(embed) {
2685
2839
  var EMBEDDERS = ["local", "huggingface", "openrouter"];
2686
2840
  var RERANKERS = ["off", "local", "huggingface", "openrouter"];
2687
2841
  var KEY_ENV2 = { huggingface: "HF_TOKEN", openrouter: "OPENROUTER_API_KEY" };
2688
- var ok = (text3) => `${pc13.green("\u2713")} ${text3}`;
2689
- var warn = (text3) => `${pc13.yellow("!")} ${text3}`;
2690
- var row = (label, value) => ` ${pc13.dim(label.padEnd(11))}${value}`;
2842
+ var ok = (text3) => `${pc14.green("\u2713")} ${text3}`;
2843
+ var warn = (text3) => `${pc14.yellow("!")} ${text3}`;
2844
+ var row = (label, value) => ` ${pc14.dim(label.padEnd(11))}${value}`;
2691
2845
  async function withSpinner(label, work, done) {
2692
2846
  const progress = spinner2();
2693
2847
  progress.start(label);
@@ -2696,7 +2850,7 @@ async function withSpinner(label, work, done) {
2696
2850
  progress.stop(done(value));
2697
2851
  return value;
2698
2852
  } catch (error) {
2699
- progress.error(pc13.red(`\u2717 ${label}`));
2853
+ progress.error(pc14.red(`\u2717 ${label}`));
2700
2854
  throw error;
2701
2855
  }
2702
2856
  }
@@ -2735,8 +2889,8 @@ async function printStatus() {
2735
2889
  const model = provider.type === "local" ? LOCAL_EMBEDDING_MODEL.id : provider.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS[provider.type];
2736
2890
  const floor = provider.relevanceFloor ?? getModelDefinition(model)?.relevanceFloor;
2737
2891
  const next = [];
2738
- console.log(pc13.bold("Embeddings"));
2739
- console.log(row("provider", `${provider.type} ${pc13.dim(model)}`));
2892
+ console.log(pc14.bold("Embeddings"));
2893
+ console.log(row("provider", `${provider.type} ${pc14.dim(model)}`));
2740
2894
  if (provider.type === "local" && !localRuntimeInstalled()) {
2741
2895
  console.log(
2742
2896
  row("runtime", warn("@huggingface/transformers is not installed: dense recall is off"))
@@ -2747,7 +2901,7 @@ async function printStatus() {
2747
2901
  console.log(
2748
2902
  row(
2749
2903
  "model",
2750
- ready ? ok(`downloaded ${pc13.dim(modelCacheDir())}`) : warn("not downloaded yet (~90 MB)")
2904
+ ready ? ok(`downloaded ${pc14.dim(modelCacheDir())}`) : warn("not downloaded yet (~90 MB)")
2751
2905
  )
2752
2906
  );
2753
2907
  if (!ready) next.push("codebuddy models pull");
@@ -2764,21 +2918,21 @@ async function printStatus() {
2764
2918
  console.log(
2765
2919
  row(
2766
2920
  "floor",
2767
- floor !== void 0 ? `${floor} ${pc13.dim(provider.relevanceFloor !== void 0 ? "(calibrated for this project)" : "(built-in)")}` : warn("not calibrated: dense recall is off for this model")
2921
+ floor !== void 0 ? `${floor} ${pc14.dim(provider.relevanceFloor !== void 0 ? "(calibrated for this project)" : "(built-in)")}` : warn("not calibrated: dense recall is off for this model")
2768
2922
  )
2769
2923
  );
2770
2924
  if (floor === void 0) next.push("codebuddy models calibrate");
2771
2925
  const rerank = file.rerank?.provider ?? "off";
2772
- console.log(pc13.bold("Reranker"));
2926
+ console.log(pc14.bold("Reranker"));
2773
2927
  if (rerank === "off") {
2774
2928
  console.log(
2775
- row("provider", `off ${pc13.dim("(default: no reranker beat dense order in evals)")}`)
2929
+ row("provider", `off ${pc14.dim("(default: no reranker beat dense order in evals)")}`)
2776
2930
  );
2777
2931
  } else {
2778
2932
  console.log(
2779
2933
  row(
2780
2934
  "provider",
2781
- `${rerank} ${pc13.dim(file.rerank?.model ?? DEFAULT_RERANK_MODELS[rerank].id)}`
2935
+ `${rerank} ${pc14.dim(file.rerank?.model ?? DEFAULT_RERANK_MODELS[rerank].id)}`
2782
2936
  )
2783
2937
  );
2784
2938
  if (rerank !== "local") {
@@ -2791,7 +2945,7 @@ async function printStatus() {
2791
2945
  );
2792
2946
  }
2793
2947
  }
2794
- console.log(pc13.bold("Memory index"));
2948
+ console.log(pc14.bold("Memory index"));
2795
2949
  if (process.env.CODEBUDDY_SEMANTIC === "0") {
2796
2950
  console.log(row("recall", warn("CODEBUDDY_SEMANTIC=0: lexical only")));
2797
2951
  }
@@ -2812,7 +2966,7 @@ async function printStatus() {
2812
2966
  console.log(
2813
2967
  row(
2814
2968
  "vectors",
2815
- `${counts.ready} ready` + (counts.pending ? `, ${counts.pending} pending` : "") + (counts.failed ? pc13.red(`, ${counts.failed} failed`) : "") + (counts.other ? pc13.yellow(`, ${counts.other} from another model`) : "")
2969
+ `${counts.ready} ready` + (counts.pending ? `, ${counts.pending} pending` : "") + (counts.failed ? pc14.red(`, ${counts.failed} failed`) : "") + (counts.other ? pc14.yellow(`, ${counts.other} from another model`) : "")
2816
2970
  )
2817
2971
  );
2818
2972
  if (counts.pending || counts.other)
@@ -2821,9 +2975,9 @@ async function printStatus() {
2821
2975
  }
2822
2976
  console.log(
2823
2977
  next.length > 0 ? `
2824
- ${pc13.bold("Next")}
2825
- ${next.map((step) => ` ${pc13.cyan(step)}`).join("\n")}` : `
2826
- ${ok("Semantic recall is ready.")} ${pc13.dim('Try: codebuddy context preview "<task>"')}`
2978
+ ${pc14.bold("Next")}
2979
+ ${next.map((step) => ` ${pc14.cyan(step)}`).join("\n")}` : `
2980
+ ${ok("Semantic recall is ready.")} ${pc14.dim('Try: codebuddy context preview "<task>"')}`
2827
2981
  );
2828
2982
  }
2829
2983
  function registerModelsCommands(program, runSafely2) {
@@ -2860,27 +3014,27 @@ Examples:
2860
3014
  ...opts.floor !== void 0 ? { relevanceFloor: opts.floor } : {}
2861
3015
  });
2862
3016
  const model = type === "local" ? LOCAL_EMBEDDING_MODEL.id : opts.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS[type];
2863
- console.log(ok(`Embeddings: ${type} ${pc13.dim(model)}`));
3017
+ console.log(ok(`Embeddings: ${type} ${pc14.dim(model)}`));
2864
3018
  const missingKey = type !== "local" && !keyPresent(type, current?.apiKey);
2865
3019
  if (missingKey) {
2866
3020
  console.log(
2867
3021
  warn(
2868
- `Set ${KEY_ENV2[type]} in your shell (keys are never stored in config), then run ${pc13.cyan("codebuddy hooks sync")}.`
3022
+ `Set ${KEY_ENV2[type]} in your shell (keys are never stored in config), then run ${pc14.cyan("codebuddy hooks sync")}.`
2869
3023
  )
2870
3024
  );
2871
- console.log(pc13.dim(" Existing vectors keep working until the new model can embed."));
3025
+ console.log(pc14.dim(" Existing vectors keep working until the new model can embed."));
2872
3026
  }
2873
3027
  if (opts.floor === void 0 && getModelDefinition(model)?.relevanceFloor === void 0) {
2874
3028
  console.log(
2875
3029
  warn(
2876
- `No floor for ${model} yet: run ${pc13.cyan("codebuddy models calibrate")} to turn on dense recall.`
3030
+ `No floor for ${model} yet: run ${pc14.cyan("codebuddy models calibrate")} to turn on dense recall.`
2877
3031
  )
2878
3032
  );
2879
3033
  }
2880
3034
  if (!missingKey) {
2881
3035
  syncInBackground(process.cwd(), true);
2882
3036
  console.log(
2883
- pc13.dim(" Re-embedding existing memory in the background (codebuddy hooks sync).")
3037
+ pc14.dim(" Re-embedding existing memory in the background (codebuddy hooks sync).")
2884
3038
  );
2885
3039
  }
2886
3040
  });
@@ -2907,13 +3061,13 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
2907
3061
  }
2908
3062
  const reranker = choice;
2909
3063
  console.log(
2910
- ok(`Reranker: ${reranker} ${pc13.dim(opts.model ?? DEFAULT_RERANK_MODELS[reranker].id)}`)
3064
+ ok(`Reranker: ${reranker} ${pc14.dim(opts.model ?? DEFAULT_RERANK_MODELS[reranker].id)}`)
2911
3065
  );
2912
3066
  if (reranker !== "local" && !keyPresent(reranker, (await readConfigFile()).provider?.apiKey)) {
2913
3067
  console.log(warn(`Set ${KEY_ENV2[reranker]}; without it reranking is skipped.`));
2914
3068
  }
2915
3069
  if (reranker === "local")
2916
- console.log(pc13.dim(" Run codebuddy models pull to download it now."));
3070
+ console.log(pc14.dim(" Run codebuddy models pull to download it now."));
2917
3071
  });
2918
3072
  });
2919
3073
  models.command("pull").description("Download the local models now instead of on first use.").action(async () => {
@@ -2924,7 +3078,7 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
2924
3078
  await withSpinner(
2925
3079
  `Downloading ${LOCAL_EMBEDDING_MODEL.id}`,
2926
3080
  () => new LocalModelProvider().embed({ input: "warm up" }),
2927
- () => ok(`${LOCAL_EMBEDDING_MODEL.id} ready ${pc13.dim(`${Date.now() - started} ms`)}`)
3081
+ () => ok(`${LOCAL_EMBEDDING_MODEL.id} ready ${pc14.dim(`${Date.now() - started} ms`)}`)
2928
3082
  );
2929
3083
  if (file.rerank?.provider === "local") {
2930
3084
  const id = file.rerank.model ?? DEFAULT_RERANK_MODELS.local.id;
@@ -2933,10 +3087,10 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
2933
3087
  await withSpinner(
2934
3088
  `Downloading ${id}`,
2935
3089
  () => rerankLocally({ id, ...revision ? { revision } : {} }, "warm", ["up"]),
2936
- () => ok(`${id} ready ${pc13.dim(`${Date.now() - started} ms`)}`)
3090
+ () => ok(`${id} ready ${pc14.dim(`${Date.now() - started} ms`)}`)
2937
3091
  );
2938
3092
  }
2939
- console.log(pc13.dim(` Cache: ${modelCacheDir()}`));
3093
+ console.log(pc14.dim(` Cache: ${modelCacheDir()}`));
2940
3094
  });
2941
3095
  });
2942
3096
  models.command("calibrate").option("--dry-run", "Measure only; do not save the floor.").description(
@@ -2954,19 +3108,19 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
2954
3108
  console.log(
2955
3109
  row(
2956
3110
  "ranking",
2957
- `AUC ${result.auc.toFixed(3)} \xB7 right fact first ${pct(result.recallAt1)} ${pc13.dim("(held-out tasks)")}`
3111
+ `AUC ${result.auc.toFixed(3)} \xB7 right fact first ${pct(result.recallAt1)} ${pc14.dim("(held-out tasks)")}`
2958
3112
  )
2959
3113
  );
2960
3114
  console.log(
2961
3115
  row(
2962
3116
  "floor",
2963
- `${result.best.floor}: precision ${pct(result.test.precision)}, recall ${pct(result.test.recall)}, silent on ${pct(result.test.abstain)} of unrelated tasks ${pc13.dim("(chosen on dev, measured on test)")}`
3117
+ `${result.best.floor}: precision ${pct(result.test.precision)}, recall ${pct(result.test.recall)}, silent on ${pct(result.test.abstain)} of unrelated tasks ${pc14.dim("(chosen on dev, measured on test)")}`
2964
3118
  )
2965
3119
  );
2966
3120
  console.log(
2967
3121
  row(
2968
3122
  "recall floor",
2969
- `${result.recall.dev.floor}: precision ${pct(result.recall.test.precision)}, recall ${pct(result.recall.test.recall)} ` + pc13.dim("(explicit recall: best F2 on dev, measured on test)")
3123
+ `${result.recall.dev.floor}: precision ${pct(result.recall.test.precision)}, recall ${pct(result.recall.test.recall)} ` + pc14.dim("(explicit recall: best F2 on dev, measured on test)")
2970
3124
  )
2971
3125
  );
2972
3126
  if (opts.dryRun) return;
@@ -2985,10 +3139,238 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
2985
3139
  });
2986
3140
  }
2987
3141
 
2988
- // src/cli/commands/plan.ts
3142
+ // src/cli/commands/output.ts
2989
3143
  import { spawn as spawn5 } from "child_process";
3144
+ import { constants as constants2 } from "os";
3145
+ import pc15 from "picocolors";
3146
+
3147
+ // src/output/reduce.ts
3148
+ import { createHash } from "crypto";
3149
+ import { mkdir as mkdir2, readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
3150
+ import { join as join6 } from "path";
3151
+ var OUTPUT_LIMITS = {
3152
+ /** At or below either, output is never reduced. */
3153
+ minLines: 80,
3154
+ minBytes: 6e3,
3155
+ head: 20,
3156
+ tail: 30,
3157
+ contextBefore: 2,
3158
+ contextAfter: 4,
3159
+ /** A reduction must save at least this share, or the original is printed. */
3160
+ minSaving: 0.3,
3161
+ /** Stored bytes per run; beyond this the stored copy says it was cut. */
3162
+ storeBytes: 2e7,
3163
+ retentionDays: 7,
3164
+ /** Lines one `output show` may print. */
3165
+ showLines: 400
3166
+ };
3167
+ var SIGNAL = /\b(error|errors|fail|failed|failure|failing|exception|traceback|panic|fatal|assert(?:ion)?|warn(?:ing)?|denied|not found|undefined|segfault|timeout|timed out)\b|✗|×|✖|FAIL|ERR!/i;
3168
+ function outputDir(root) {
3169
+ return join6(root, ".codebuddy", "cache", "outputs");
3170
+ }
3171
+ var shape = (line) => line.replace(/\d+/g, "#").trim();
3172
+ function reduceOutput(text3, id) {
3173
+ const lines = text3.split("\n");
3174
+ if (lines.length <= OUTPUT_LIMITS.minLines || Buffer.byteLength(text3) <= OUTPUT_LIMITS.minBytes) {
3175
+ return null;
3176
+ }
3177
+ if (text3.includes("\0")) return null;
3178
+ const keep = /* @__PURE__ */ new Set();
3179
+ for (let i = 0; i < Math.min(OUTPUT_LIMITS.head, lines.length); i++) keep.add(i);
3180
+ for (let i = Math.max(0, lines.length - OUTPUT_LIMITS.tail); i < lines.length; i++) keep.add(i);
3181
+ lines.forEach((line, index2) => {
3182
+ if (!SIGNAL.test(line)) return;
3183
+ const from = Math.max(0, index2 - OUTPUT_LIMITS.contextBefore);
3184
+ const to = Math.min(lines.length - 1, index2 + OUTPUT_LIMITS.contextAfter);
3185
+ for (let i = from; i <= to; i++) keep.add(i);
3186
+ });
3187
+ const out = [];
3188
+ let index = 0;
3189
+ while (index < lines.length) {
3190
+ if (!keep.has(index)) {
3191
+ const start = index;
3192
+ while (index < lines.length && !keep.has(index)) index++;
3193
+ out.push(
3194
+ `\u2026 ${index - start} lines omitted (codebuddy output show ${id} --range ${start + 1}:${index})`
3195
+ );
3196
+ continue;
3197
+ }
3198
+ const line = lines[index];
3199
+ const same = SIGNAL.test(line) ? (other) => other === line : (other) => shape(other) === shape(line);
3200
+ let run = index + 1;
3201
+ while (run < lines.length && keep.has(run) && same(lines[run])) run++;
3202
+ out.push(line);
3203
+ if (run - index > 2) {
3204
+ out.push(
3205
+ `\u2026 ${run - index - 1} similar lines (codebuddy output show ${id} --range ${index + 2}:${run})`
3206
+ );
3207
+ index = run;
3208
+ } else index++;
3209
+ }
3210
+ const reduced = out.join("\n");
3211
+ return Buffer.byteLength(reduced) <= Buffer.byteLength(text3) * (1 - OUTPUT_LIMITS.minSaving) ? reduced : null;
3212
+ }
3213
+ async function storeOutput(root, command, output, exitCode, now = /* @__PURE__ */ new Date()) {
3214
+ const truncated = Buffer.byteLength(output) > OUTPUT_LIMITS.storeBytes;
3215
+ const body = truncated ? `${output.slice(0, OUTPUT_LIMITS.storeBytes)}
3216
+ [codebuddy: stored copy cut at ${OUTPUT_LIMITS.storeBytes} bytes]` : output;
3217
+ const id = createHash("sha256").update(`${command}\0${now.toISOString()}\0${body}`).digest("hex").slice(0, 12);
3218
+ const meta = {
3219
+ id,
3220
+ command,
3221
+ exitCode,
3222
+ createdAt: now.toISOString(),
3223
+ expiresAt: new Date(now.getTime() + OUTPUT_LIMITS.retentionDays * 864e5).toISOString(),
3224
+ lines: body.split("\n").length,
3225
+ bytes: Buffer.byteLength(body),
3226
+ truncated
3227
+ };
3228
+ const dir = outputDir(root);
3229
+ await mkdir2(dir, { recursive: true, mode: 448 });
3230
+ await writeAtomic(join6(dir, `${id}.log`), body, { mode: 384 });
3231
+ await writeAtomic(join6(dir, `${id}.json`), `${JSON.stringify(meta)}
3232
+ `, { mode: 384 });
3233
+ await pruneOutputs(root, now).catch(() => void 0);
3234
+ return meta;
3235
+ }
3236
+ async function pruneOutputs(root, now = /* @__PURE__ */ new Date()) {
3237
+ const dir = outputDir(root);
3238
+ let removed = 0;
3239
+ for (const name2 of (await readdir(dir).catch(() => [])).filter(
3240
+ (n) => n.endsWith(".json")
3241
+ )) {
3242
+ const meta = await readMeta(root, name2.slice(0, -5));
3243
+ if (meta && Date.parse(meta.expiresAt) <= now.getTime()) {
3244
+ await rm2(join6(dir, `${meta.id}.log`), { force: true });
3245
+ await rm2(join6(dir, name2), { force: true });
3246
+ removed++;
3247
+ }
3248
+ }
3249
+ return removed;
3250
+ }
3251
+ async function readMeta(root, id) {
3252
+ if (!/^[a-f0-9]{12}$/.test(id)) return null;
3253
+ try {
3254
+ return JSON.parse(await readFile3(join6(outputDir(root), `${id}.json`), "utf8"));
3255
+ } catch {
3256
+ return null;
3257
+ }
3258
+ }
3259
+ async function showOutput(root, id, options = {}) {
3260
+ const meta = await readMeta(root, id);
3261
+ if (!meta) return { status: "missing", id };
3262
+ if (Date.parse(meta.expiresAt) <= (options.now ?? /* @__PURE__ */ new Date()).getTime()) {
3263
+ return { status: "expired", id };
3264
+ }
3265
+ const body = await readFile3(join6(outputDir(root), `${id}.log`), "utf8").catch(() => null);
3266
+ if (body === null) return { status: "missing", id };
3267
+ const lines = body.split("\n");
3268
+ let picked;
3269
+ if (options.grep) {
3270
+ const pattern = new RegExp(options.grep, "i");
3271
+ const set = /* @__PURE__ */ new Set();
3272
+ lines.forEach((line, index) => {
3273
+ if (!pattern.test(line)) return;
3274
+ for (let i = Math.max(0, index - 2); i <= Math.min(lines.length - 1, index + 2); i++)
3275
+ set.add(i);
3276
+ });
3277
+ picked = [...set].sort((a, b) => a - b);
3278
+ } else {
3279
+ const [from, to] = options.range ?? [1, lines.length];
3280
+ picked = [];
3281
+ for (let i = Math.max(1, from); i <= Math.min(lines.length, to); i++) picked.push(i - 1);
3282
+ }
3283
+ const shown = picked.slice(0, OUTPUT_LIMITS.showLines);
3284
+ return {
3285
+ status: "ok",
3286
+ meta,
3287
+ text: shown.map((i) => `${i + 1}: ${lines[i]}`).join("\n"),
3288
+ more: picked.length > shown.length
3289
+ };
3290
+ }
3291
+ async function listOutputs(root) {
3292
+ const names = await readdir(outputDir(root)).catch(() => []);
3293
+ const metas = await Promise.all(
3294
+ names.filter((n) => n.endsWith(".json")).map((n) => readMeta(root, n.slice(0, -5)))
3295
+ );
3296
+ return metas.filter((meta) => meta !== null).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
3297
+ }
3298
+
3299
+ // src/cli/commands/output.ts
3300
+ function registerOutputCommands(program, runSafely2) {
3301
+ program.command("run").argument("<command...>", "The command and its arguments (after --).").description(
3302
+ "Run a command; print a reduced view of long output and keep the original for `codebuddy output show`."
3303
+ ).action(async (parts) => {
3304
+ await runSafely2(async () => {
3305
+ const command = parts.join(" ");
3306
+ const { output: output2, exitCode } = await capture(parts);
3307
+ const root = process.cwd();
3308
+ const provisional = reduceOutput(output2, "xxxxxxxxxxxx");
3309
+ if (provisional === null) {
3310
+ process.stdout.write(output2);
3311
+ } else {
3312
+ const stored = await storeOutput(root, command, output2, exitCode);
3313
+ const reduced = reduceOutput(output2, stored.id) ?? output2;
3314
+ process.stdout.write(`${reduced}
3315
+ `);
3316
+ process.stdout.write(
3317
+ `[codebuddy run: exit ${exitCode ?? "signal"}; ${stored.lines} lines shown as ${reduced.split("\n").length}; full output: codebuddy output show ${stored.id} [--grep RE | --range A:B], kept until ${stored.expiresAt.slice(0, 10)}]
3318
+ `
3319
+ );
3320
+ }
3321
+ process.exitCode = exitCode ?? 1;
3322
+ });
3323
+ });
3324
+ const output = program.command("output").description("Stored originals of `codebuddy run` output.");
3325
+ output.command("list", { isDefault: true }).description("List stored outputs, newest first.").action(async () => {
3326
+ await runSafely2(async () => {
3327
+ const stored = await listOutputs(process.cwd());
3328
+ if (stored.length === 0) console.log(pc15.dim("no stored outputs"));
3329
+ for (const meta of stored) {
3330
+ console.log(
3331
+ `${meta.id} exit ${meta.exitCode ?? "-"} ${meta.lines} lines until ${meta.expiresAt.slice(0, 10)} ${meta.command.slice(0, 80)}`
3332
+ );
3333
+ }
3334
+ });
3335
+ });
3336
+ output.command("show").argument("<id>").option("--range <a:b>", "1-based inclusive line range.").option("--grep <pattern>", "Lines matching a regular expression, with 2 lines of context.").description("Print exact stored lines (at most 400 per call).").action(async (id, opts) => {
3337
+ await runSafely2(async () => {
3338
+ const range = opts.range?.match(/^(\d+):(\d+)$/);
3339
+ if (opts.range && !range) throw new Error("--range must look like 120:180");
3340
+ const result = await showOutput(process.cwd(), id, {
3341
+ ...range ? { range: [Number(range[1]), Number(range[2])] } : {},
3342
+ ...opts.grep ? { grep: opts.grep } : {}
3343
+ });
3344
+ if (result.status !== "ok") {
3345
+ throw new Error(`stored output ${id} is ${result.status}`);
3346
+ }
3347
+ console.log(result.text);
3348
+ if (result.more) console.log(pc15.dim("\u2026 more lines; narrow --range or --grep"));
3349
+ });
3350
+ });
3351
+ }
3352
+ function capture(parts) {
3353
+ return new Promise((resolve6, reject) => {
3354
+ const [file, ...args] = parts;
3355
+ const child = parts.length === 1 ? spawn5(file, { shell: true, stdio: ["inherit", "pipe", "pipe"] }) : spawn5(file, args, { stdio: ["inherit", "pipe", "pipe"] });
3356
+ const chunks = [];
3357
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
3358
+ child.stderr.on("data", (chunk) => chunks.push(chunk));
3359
+ child.on("error", reject);
3360
+ child.on(
3361
+ "close",
3362
+ (code, signal) => resolve6({
3363
+ output: Buffer.concat(chunks).toString("utf8"),
3364
+ exitCode: code ?? (signal ? 128 + (constants2.signals[signal] ?? 0) : null)
3365
+ })
3366
+ );
3367
+ });
3368
+ }
3369
+
3370
+ // src/cli/commands/plan.ts
3371
+ import { spawn as spawn6 } from "child_process";
2990
3372
  import { relative as relative4 } from "path";
2991
- import pc14 from "picocolors";
3373
+ import pc16 from "picocolors";
2992
3374
  function makeLifecycle(cwd = process.cwd()) {
2993
3375
  const store = new PlanFileStore(cwd);
2994
3376
  return { store, lifecycle: new PlanLifecycle(store) };
@@ -2996,67 +3378,67 @@ function makeLifecycle(cwd = process.cwd()) {
2996
3378
  function statusColor(status) {
2997
3379
  switch (status) {
2998
3380
  case "draft":
2999
- return pc14.dim(status);
3381
+ return pc16.dim(status);
3000
3382
  case "approved":
3001
- return pc14.cyan(status);
3383
+ return pc16.cyan(status);
3002
3384
  case "executing":
3003
- return pc14.yellow(status);
3385
+ return pc16.yellow(status);
3004
3386
  case "complete":
3005
- return pc14.green(status);
3387
+ return pc16.green(status);
3006
3388
  case "abandoned":
3007
- return pc14.red(status);
3389
+ return pc16.red(status);
3008
3390
  }
3009
3391
  }
3010
3392
  function renderPlan(plan) {
3011
3393
  const lines = [];
3012
- lines.push(`${pc14.bold(plan.title)} ${statusColor(plan.status)}`);
3013
- lines.push(pc14.dim(plan.id));
3394
+ lines.push(`${pc16.bold(plan.title)} ${statusColor(plan.status)}`);
3395
+ lines.push(pc16.dim(plan.id));
3014
3396
  lines.push("");
3015
- lines.push(`${pc14.bold("Brief:")} ${plan.brief}`);
3397
+ lines.push(`${pc16.bold("Brief:")} ${plan.brief}`);
3016
3398
  if (plan.filesToTouch.length > 0) {
3017
3399
  lines.push("");
3018
- lines.push(pc14.bold("Files to touch:"));
3400
+ lines.push(pc16.bold("Files to touch:"));
3019
3401
  for (const file of plan.filesToTouch) {
3020
3402
  lines.push(` ${file.action.padEnd(6)} ${file.path}${file.notes ? ` \u2014 ${file.notes}` : ""}`);
3021
3403
  }
3022
3404
  }
3023
3405
  if (plan.testsToAdd.length > 0) {
3024
3406
  lines.push("");
3025
- lines.push(pc14.bold("Tests to add:"));
3407
+ lines.push(pc16.bold("Tests to add:"));
3026
3408
  for (const test of plan.testsToAdd) {
3027
3409
  lines.push(` ${test.path}${test.description ? ` \u2014 ${test.description}` : ""}`);
3028
3410
  }
3029
3411
  }
3030
3412
  if (plan.outOfScope.length > 0) {
3031
3413
  lines.push("");
3032
- lines.push(pc14.bold("Out of scope:"));
3414
+ lines.push(pc16.bold("Out of scope:"));
3033
3415
  for (const item of plan.outOfScope) lines.push(` - ${item}`);
3034
3416
  }
3035
3417
  if (plan.risks.length > 0) {
3036
3418
  lines.push("");
3037
- lines.push(pc14.bold("Risks:"));
3419
+ lines.push(pc16.bold("Risks:"));
3038
3420
  for (const item of plan.risks) lines.push(` - ${item}`);
3039
3421
  }
3040
3422
  if (plan.commitSha) {
3041
3423
  lines.push("");
3042
- lines.push(`${pc14.bold("Commit:")} ${plan.commitSha}`);
3424
+ lines.push(`${pc16.bold("Commit:")} ${plan.commitSha}`);
3043
3425
  }
3044
3426
  lines.push("");
3045
- lines.push(pc14.dim("\u2500".repeat(40)));
3427
+ lines.push(pc16.dim("\u2500".repeat(40)));
3046
3428
  lines.push(plan.body);
3047
3429
  return lines.join("\n");
3048
3430
  }
3049
3431
  function openEditor2(path) {
3050
3432
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3051
3433
  return new Promise((resolve6, reject) => {
3052
- const child = spawn5(editor, [path], { stdio: "inherit" });
3434
+ const child = spawn6(editor, [path], { stdio: "inherit" });
3053
3435
  child.on("error", reject);
3054
3436
  child.on("close", () => resolve6());
3055
3437
  });
3056
3438
  }
3057
3439
  function runGit(args) {
3058
3440
  return new Promise((resolve6, reject) => {
3059
- const child = spawn5("git", args, { stdio: ["ignore", "pipe", "pipe"] });
3441
+ const child = spawn6("git", args, { stdio: ["ignore", "pipe", "pipe"] });
3060
3442
  let stdout = "";
3061
3443
  let stderr = "";
3062
3444
  child.stdout.on("data", (c) => {
@@ -3081,14 +3463,14 @@ function registerPlanCommands(program, runSafely2) {
3081
3463
  title: opts.title ?? brief,
3082
3464
  brief
3083
3465
  });
3084
- console.log(`${pc14.green("created")} ${created.id} (${statusColor(created.status)})`);
3466
+ console.log(`${pc16.green("created")} ${created.id} (${statusColor(created.status)})`);
3085
3467
  if (opts.edit !== false && process.stdin.isTTY) {
3086
3468
  await openEditor2(created.path);
3087
3469
  const { store } = makeLifecycle();
3088
3470
  await store.readPlan(created.id);
3089
- console.log(pc14.dim("saved and validated"));
3471
+ console.log(pc16.dim("saved and validated"));
3090
3472
  }
3091
- console.log(pc14.dim(relative4(process.cwd(), created.path)));
3473
+ console.log(pc16.dim(relative4(process.cwd(), created.path)));
3092
3474
  });
3093
3475
  });
3094
3476
  plan.command("list").option("--status <status>", "Filter by status.").option("--all", "Include completed and abandoned plans.").description("List plans in the current namespace.").action(async (opts) => {
@@ -3100,11 +3482,11 @@ function registerPlanCommands(program, runSafely2) {
3100
3482
  else if (!opts.all)
3101
3483
  plans = plans.filter((p) => p.status !== "complete" && p.status !== "abandoned");
3102
3484
  if (plans.length === 0) {
3103
- console.log(pc14.dim("no plans"));
3485
+ console.log(pc16.dim("no plans"));
3104
3486
  return;
3105
3487
  }
3106
3488
  for (const p of plans) {
3107
- console.log(`${statusColor(p.status).padEnd(20)} ${pc14.bold(p.title)} ${pc14.dim(p.id)}`);
3489
+ console.log(`${statusColor(p.status).padEnd(20)} ${pc16.bold(p.title)} ${pc16.dim(p.id)}`);
3108
3490
  }
3109
3491
  });
3110
3492
  });
@@ -3120,7 +3502,7 @@ function registerPlanCommands(program, runSafely2) {
3120
3502
  const existing = await store.readPlan(id);
3121
3503
  await openEditor2(existing.path);
3122
3504
  await store.readPlan(id);
3123
- console.log(pc14.dim("saved and validated"));
3505
+ console.log(pc16.dim("saved and validated"));
3124
3506
  });
3125
3507
  });
3126
3508
  plan.command("approve").argument("<id>").description("Promote a draft to approved (one active plan per namespace).").action(async (id) => {
@@ -3144,7 +3526,7 @@ function registerPlanCommands(program, runSafely2) {
3144
3526
  ...opts.commit ? { commitSha: opts.commit } : {},
3145
3527
  ...opts.notes ? { notes: opts.notes } : {}
3146
3528
  });
3147
- if (!opts.commit) console.log(pc14.yellow("note: no --commit supplied"));
3529
+ if (!opts.commit) console.log(pc16.yellow("note: no --commit supplied"));
3148
3530
  console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
3149
3531
  });
3150
3532
  });
@@ -3163,7 +3545,7 @@ function registerPlanCommands(program, runSafely2) {
3163
3545
  const args = ["diff", opts.from, ...opts.to ? [opts.to] : [], "--", relPath];
3164
3546
  const result = await runGit(args);
3165
3547
  if (result.stdout.trim().length === 0) {
3166
- console.log(pc14.dim("no changes"));
3548
+ console.log(pc16.dim("no changes"));
3167
3549
  } else {
3168
3550
  process.stdout.write(result.stdout);
3169
3551
  }
@@ -3180,13 +3562,13 @@ function registerPlanCommands(program, runSafely2) {
3180
3562
  throw new Error(`Invalid policy "${mode}". Use one of: ${PLAN_POLICIES.join(", ")}.`);
3181
3563
  }
3182
3564
  await setPlanPolicy(mode);
3183
- console.log(`${pc14.green("plan policy")} = ${mode}`);
3565
+ console.log(`${pc16.green("plan policy")} = ${mode}`);
3184
3566
  });
3185
3567
  });
3186
3568
  }
3187
3569
 
3188
3570
  // src/cli/commands/risk.ts
3189
- import pc15 from "picocolors";
3571
+ import pc17 from "picocolors";
3190
3572
  function registerRiskCommands(program, runSafely2) {
3191
3573
  const risk = program.command("risk").description("Assess evidence-backed change risk.");
3192
3574
  risk.command("assess").option("--plan <id>", "Assess paths listed in a CodeBuddy plan.").option("--paths <paths>", "Comma-separated repo-relative paths to assess.").option("--git", "Assess staged, unstaged, and untracked git changes.").option("--limit <n>", "Maximum risk items to show.", Number).option("--json", "Print machine-readable JSON.").option("--explain", "Show evidence for each risk item.").description("Assess risk for a plan, explicit paths, or current git changes.").action(
@@ -3214,31 +3596,31 @@ function parsePaths3(value) {
3214
3596
  }
3215
3597
  function printAssessment(assessment, explain) {
3216
3598
  if (assessment.items.length === 0) {
3217
- console.log(pc15.dim("no risk evidence found"));
3599
+ console.log(pc17.dim("no risk evidence found"));
3218
3600
  return;
3219
3601
  }
3220
3602
  for (const item of assessment.items) {
3221
3603
  console.log(
3222
- `${pc15.bold(item.path)} score ${scoreColor2(item.score)} category ${pc15.cyan(item.category)}`
3604
+ `${pc17.bold(item.path)} score ${scoreColor2(item.score)} category ${pc17.cyan(item.category)}`
3223
3605
  );
3224
3606
  console.log(` ${item.reason}`);
3225
3607
  if (explain) {
3226
3608
  for (const evidence of item.evidence) console.log(` \u21B3 ${formatEvidence(evidence)}`);
3227
3609
  } else {
3228
- console.log(pc15.dim(` ${item.evidence.length} evidence item(s)`));
3610
+ console.log(pc17.dim(` ${item.evidence.length} evidence item(s)`));
3229
3611
  }
3230
3612
  }
3231
3613
  console.log(
3232
- pc15.dim(
3614
+ pc17.dim(
3233
3615
  `
3234
3616
  ${assessment.stats.itemsProduced} item(s), ${assessment.stats.signalsRun} signal(s), ${assessment.stats.pathsConsidered} path(s) considered`
3235
3617
  )
3236
3618
  );
3237
3619
  }
3238
3620
  function scoreColor2(score) {
3239
- if (score >= 75) return pc15.red(String(score));
3240
- if (score >= 50) return pc15.yellow(String(score));
3241
- return pc15.green(String(score));
3621
+ if (score >= 75) return pc17.red(String(score));
3622
+ if (score >= 50) return pc17.yellow(String(score));
3623
+ return pc17.green(String(score));
3242
3624
  }
3243
3625
  function formatEvidence(evidence) {
3244
3626
  switch (evidence.kind) {
@@ -3260,7 +3642,7 @@ function truncate2(value, max) {
3260
3642
 
3261
3643
  // src/cli/commands/rules.ts
3262
3644
  import { relative as relative5 } from "path";
3263
- import pc16 from "picocolors";
3645
+ import pc18 from "picocolors";
3264
3646
  async function resolveMode(client, value) {
3265
3647
  if (value) {
3266
3648
  const mode = parseMode(value);
@@ -3300,7 +3682,7 @@ function registerRulesCommands(program, runSafely2) {
3300
3682
  `${actionLabel(result.action)} ${clientDisplayName(client)} workflow in ${where}`
3301
3683
  );
3302
3684
  if (result.action !== "unchanged") {
3303
- console.log(pc16.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
3685
+ console.log(pc18.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
3304
3686
  }
3305
3687
  });
3306
3688
  });
@@ -3308,20 +3690,20 @@ function registerRulesCommands(program, runSafely2) {
3308
3690
  function actionLabel(action) {
3309
3691
  switch (action) {
3310
3692
  case "created":
3311
- return pc16.green("created");
3693
+ return pc18.green("created");
3312
3694
  case "added":
3313
- return pc16.green("added");
3695
+ return pc18.green("added");
3314
3696
  case "updated":
3315
- return pc16.cyan("updated");
3697
+ return pc18.cyan("updated");
3316
3698
  case "removed":
3317
- return pc16.yellow("removed");
3699
+ return pc18.yellow("removed");
3318
3700
  default:
3319
- return pc16.dim("unchanged");
3701
+ return pc18.dim("unchanged");
3320
3702
  }
3321
3703
  }
3322
3704
 
3323
3705
  // src/cli/commands/suggest.ts
3324
- import pc17 from "picocolors";
3706
+ import pc19 from "picocolors";
3325
3707
  function parsePaths4(value) {
3326
3708
  return value.split(",").map((path) => path.trim()).filter(Boolean);
3327
3709
  }
@@ -3343,7 +3725,7 @@ function registerSuggestCommand(program, runSafely2) {
3343
3725
  }
3344
3726
  printSuggestions(result.suggestions);
3345
3727
  console.log(
3346
- pc17.dim(
3728
+ pc19.dim(
3347
3729
  `
3348
3730
  ${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} path(s)`
3349
3731
  )
@@ -3354,72 +3736,72 @@ ${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} pa
3354
3736
  }
3355
3737
  function printSuggestions(suggestions) {
3356
3738
  if (suggestions.length === 0) {
3357
- console.log(pc17.dim("no suggestions \u2014 nothing high-signal to report"));
3739
+ console.log(pc19.dim("no suggestions \u2014 nothing high-signal to report"));
3358
3740
  return;
3359
3741
  }
3360
3742
  for (const suggestion of suggestions) {
3361
- console.log(`${severityBadge2(suggestion.severity)} ${pc17.bold(suggestion.title)}`);
3743
+ console.log(`${severityBadge2(suggestion.severity)} ${pc19.bold(suggestion.title)}`);
3362
3744
  console.log(` ${suggestion.detail}`);
3363
3745
  for (const evidence of suggestion.evidence) {
3364
- console.log(pc17.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
3746
+ console.log(pc19.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
3365
3747
  }
3366
- if (suggestion.commandHint) console.log(pc17.dim(` \u2192 ${suggestion.commandHint}`));
3748
+ if (suggestion.commandHint) console.log(pc19.dim(` \u2192 ${suggestion.commandHint}`));
3367
3749
  }
3368
3750
  }
3369
3751
  function severityBadge2(severity) {
3370
3752
  switch (severity) {
3371
3753
  case "high":
3372
- return pc17.red("[high]");
3754
+ return pc19.red("[high]");
3373
3755
  case "medium":
3374
- return pc17.yellow("[med ]");
3756
+ return pc19.yellow("[med ]");
3375
3757
  case "low":
3376
- return pc17.cyan("[low ]");
3758
+ return pc19.cyan("[low ]");
3377
3759
  default:
3378
- return pc17.dim("[info]");
3760
+ return pc19.dim("[info]");
3379
3761
  }
3380
3762
  }
3381
3763
 
3382
3764
  // src/cli/commands/symbols.ts
3383
- import { readFile as readFile3 } from "fs/promises";
3765
+ import { readFile as readFile4 } from "fs/promises";
3384
3766
  import { resolve as resolve5 } from "path";
3385
- import pc18 from "picocolors";
3767
+ import pc20 from "picocolors";
3386
3768
  function registerSymbolsCommand(program, runSafely2) {
3387
3769
  program.command("symbols").argument("<path>", "Repo-relative or absolute path to a code file.").option("--json", "Print machine-readable JSON.").description("List a file's top-level symbols (functions, classes, types, exports).").action(async (path, opts) => {
3388
3770
  await runSafely2(async () => {
3389
3771
  if (!supportsSymbols(path)) {
3390
- console.log(pc18.dim(`no symbol extraction for ${path} (unsupported language)`));
3772
+ console.log(pc20.dim(`no symbol extraction for ${path} (unsupported language)`));
3391
3773
  return;
3392
3774
  }
3393
- const content = await readFile3(resolve5(path), "utf8");
3775
+ const content = await readFile4(resolve5(path), "utf8");
3394
3776
  const table = extractSymbolTable(content, path);
3395
3777
  if (opts.json) {
3396
3778
  console.log(JSON.stringify({ path, symbols: table }, null, 2));
3397
3779
  return;
3398
3780
  }
3399
3781
  if (table.length === 0) {
3400
- console.log(pc18.dim("no top-level symbols found"));
3782
+ console.log(pc20.dim("no top-level symbols found"));
3401
3783
  return;
3402
3784
  }
3403
3785
  for (const symbol of table) {
3404
- const badge = symbol.exported ? pc18.green("export") : pc18.dim("local ");
3786
+ const badge = symbol.exported ? pc20.green("export") : pc20.dim("local ");
3405
3787
  console.log(
3406
- `${badge} ${pc18.cyan(symbol.kind.padEnd(9))} ${pc18.bold(symbol.name)} ${pc18.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
3788
+ `${badge} ${pc20.cyan(symbol.kind.padEnd(9))} ${pc20.bold(symbol.name)} ${pc20.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
3407
3789
  );
3408
3790
  }
3409
- console.log(pc18.dim(`
3791
+ console.log(pc20.dim(`
3410
3792
  ${table.length} symbol(s)`));
3411
3793
  });
3412
3794
  });
3413
3795
  }
3414
3796
 
3415
3797
  // src/cli/commands/telemetry.ts
3416
- import pc19 from "picocolors";
3798
+ import pc21 from "picocolors";
3417
3799
 
3418
3800
  // src/telemetry/collector.ts
3419
- import { createHash, timingSafeEqual } from "crypto";
3420
- import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4 } from "fs/promises";
3801
+ import { createHash as createHash2, timingSafeEqual } from "crypto";
3802
+ import { appendFile, mkdir as mkdir3, readdir as readdir2, readFile as readFile5 } from "fs/promises";
3421
3803
  import { createServer as createServer2 } from "http";
3422
- import { join as join6 } from "path";
3804
+ import { join as join7 } from "path";
3423
3805
  import { z as z2 } from "zod";
3424
3806
 
3425
3807
  // src/telemetry/admin-page.ts
@@ -3431,101 +3813,174 @@ var ADMIN_HTML = `<!doctype html>
3431
3813
  <title>CodeBuddy Usage</title>
3432
3814
  <style>
3433
3815
  :root {
3434
- --bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --line: #e2e1dc;
3435
- --bar: #2a78d6; --bar-soft: #e3edfa; --bad: #b42318;
3816
+ --bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --faint: #8a8984; --line: #e4e3de;
3817
+ --bar: #2a78d6; --bar-soft: #e3edfa; --hook: #eb6834; --cli: #1baf7a;
3818
+ --good: #0a7d0a; --bad: #c23030; --bad-soft: #fbeaea;
3436
3819
  --sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
3437
3820
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
3438
3821
  }
3439
3822
  @media (prefers-color-scheme: dark) { :root {
3440
- --bg: #121211; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --line: #2e2e2b;
3441
- --bar: #3987e5; --bar-soft: #1d2b3d; --bad: #f97066; color-scheme: dark } }
3823
+ --bg: #111110; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --faint: #8f8e86; --line: #2e2e2b;
3824
+ --bar: #3987e5; --bar-soft: #1d2b3d; --hook: #d95926; --cli: #199e70;
3825
+ --good: #3fbf3f; --bad: #f07070; --bad-soft: #3a1a1a; color-scheme: dark } }
3442
3826
  * { box-sizing: border-box; }
3443
3827
  body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 var(--sans); }
3444
- .wrap { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid; gap: 16px; }
3828
+ a { color: inherit; }
3829
+ .wrap { max-width: 1240px; margin: 0 auto; padding: 0 16px 56px; display: grid; gap: 16px; }
3445
3830
  .wrap > * { min-width: 0; }
3446
- header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
3447
- h1 { font-size: 20px; margin: 0; letter-spacing: -.01em; }
3448
- h1 small { font: 500 13px var(--mono); color: var(--muted); margin-left: 8px; }
3449
- h2 { font-size: 13px; margin: 0 0 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
3831
+ .top { position: sticky; top: 0; z-index: 5; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); }
3832
+ .bar-inner { max-width: 1240px; margin: 0 auto; padding: 12px 16px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
3833
+ .brand { display: flex; align-items: center; gap: 10px; font-weight: 650; font-size: 16px; letter-spacing: -.01em; }
3834
+ .mark { width: 22px; height: 22px; border-radius: 6px; background: var(--bar); display: grid; place-items: center; color: #fff; font: 700 12px var(--mono); }
3835
+ .updated { font: 12px var(--mono); color: var(--faint); }
3836
+ .tabs { display: flex; gap: 4px; }
3837
+ .tabs a { text-decoration: none; color: var(--muted); font-weight: 500; font-size: 13px; padding: 6px 10px; border-radius: 7px; }
3838
+ .tabs a[aria-current="page"] { background: var(--panel); color: var(--fg); box-shadow: inset 0 0 0 1px var(--line); }
3839
+ .tabs a:focus-visible, .seg button:focus-visible, .btn:focus-visible, input:focus-visible, th button:focus-visible, tr.link:focus-visible { outline: 2px solid var(--bar); outline-offset: 1px; }
3450
3840
  .spacer { flex: 1; }
3451
3841
  .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
3452
- .seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 12px; cursor: pointer; }
3842
+ .seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 11px; cursor: pointer; }
3453
3843
  .seg button + button { border-left: 1px solid var(--line); }
3454
3844
  .seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
3455
- .seg button:focus-visible { outline: 2px solid var(--bar); outline-offset: -2px; }
3456
- .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
3457
- .kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
3845
+ h2 { font-size: 12px; margin: 0; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
3846
+ .head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 12px; }
3847
+ .head .note { font-size: 12px; color: var(--faint); }
3848
+ .hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 12px; padding-top: 16px; }
3849
+ .kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; }
3458
3850
  .kpi .label { font-size: 12px; color: var(--muted); }
3459
- .kpi .value { font: 600 28px/1.2 var(--mono); font-variant-numeric: tabular-nums; margin-top: 4px; }
3460
- .kpi .sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
3461
- .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; position: relative; }
3462
- .cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
3463
- .chart svg { display: block; width: 100%; height: 180px; }
3464
- .chart rect.hit { fill: transparent; }
3465
- .chart rect.hit:hover + rect.mark, .chart rect.mark:hover { opacity: .75; }
3466
- svg text { fill: var(--muted); font: 11px var(--mono); }
3467
- .tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 4px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 2; }
3468
- .rows { display: grid; gap: 6px; }
3469
- .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; font-size: 13px; }
3470
- .row .track { grid-column: 1 / -1; height: 6px; background: var(--bar-soft); border-radius: 3px; }
3471
- .row .fill { height: 6px; background: var(--bar); border-radius: 3px; }
3851
+ .kpi .value { font: 650 32px/1.15 var(--mono); font-variant-numeric: tabular-nums; margin-top: 6px; letter-spacing: -.02em; }
3852
+ .kpi .sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
3853
+ .up { color: var(--good); font-weight: 600; }
3854
+ .down { color: var(--bad); font-weight: 600; }
3855
+ .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; min-width: 0; }
3856
+ .grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 16px; }
3857
+ .grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 16px; }
3858
+ .chart svg { display: block; width: 100%; height: 190px; overflow: visible; }
3859
+ svg text { fill: var(--faint); font: 11px var(--mono); }
3860
+ .hit { fill: transparent; cursor: crosshair; }
3861
+ .hit:hover { fill: color-mix(in srgb, var(--fg) 6%, transparent); }
3862
+ .tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 5px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 10; }
3863
+ .rows { display: grid; gap: 8px; }
3864
+ .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; font-size: 13px; }
3865
+ .row .track { grid-column: 1 / -1; height: 8px; background: var(--bar-soft); border-radius: 4px; overflow: hidden; }
3866
+ .row .fill { height: 8px; background: var(--bar); border-radius: 4px; }
3472
3867
  .row .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
3868
+ .funnel .row { font-size: 14px; }
3869
+ .funnel .track { height: 22px; border-radius: 6px; }
3870
+ .funnel .fill { height: 22px; border-radius: 6px; }
3473
3871
  .scroll { overflow-x: auto; }
3474
3872
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
3475
3873
  th { text-align: left; font: 600 11px var(--sans); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
3476
- td { padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
3874
+ th button { all: unset; cursor: pointer; }
3875
+ th button::after { content: attr(data-dir); margin-left: 4px; color: var(--faint); }
3876
+ td { padding: 7px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
3477
3877
  tr:last-child td { border-bottom: 0; }
3478
3878
  td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
3879
+ tr.link { cursor: pointer; }
3880
+ tr.link:hover td { background: var(--bar-soft); }
3881
+ .inline { display: inline-block; height: 6px; background: var(--bar); border-radius: 3px; vertical-align: middle; margin-left: 8px; }
3882
+ .heat td { text-align: center; font: 12px var(--mono); padding: 6px 4px; border: 2px solid var(--panel); border-radius: 6px; min-width: 44px; }
3883
+ .heat td.lab { text-align: left; color: var(--muted); font-family: var(--sans); white-space: nowrap; }
3884
+ .heat td.empty { background: transparent; color: var(--faint); }
3479
3885
  .bad { color: var(--bad); }
3480
3886
  .muted { color: var(--muted); }
3481
3887
  .mono { font-family: var(--mono); }
3482
- .btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 6px 12px; border-radius: 8px; cursor: pointer; }
3483
- .btn:focus-visible { outline: 2px solid var(--bar); outline-offset: 2px; }
3484
- .users { max-height: 560px; overflow: auto; }
3485
- .users thead th { position: sticky; top: 0; background: var(--panel); }
3486
- .empty { padding: 32px 16px; text-align: center; color: var(--muted); }
3888
+ .empty-state { padding: 36px 16px; text-align: center; color: var(--muted); }
3889
+ .toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
3890
+ input[type="search"] { font: 13px var(--sans); padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0; flex: 1 1 200px; }
3891
+ .btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 7px 12px; border-radius: 8px; cursor: pointer; text-decoration: none; }
3892
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
3893
+ .chip { display: inline-flex; align-items: center; gap: 6px; font: 600 11px var(--mono); padding: 2px 8px; border-radius: 999px; background: var(--bar-soft); color: var(--fg); }
3894
+ .sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
3895
+ .sessions { display: grid; gap: 6px; max-height: 520px; overflow: auto; }
3896
+ .session { all: unset; display: grid; gap: 2px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; font-size: 12px; }
3897
+ .session[aria-pressed="true"] { border-color: var(--bar); box-shadow: inset 3px 0 0 var(--bar); }
3898
+ .session:focus-visible { outline: 2px solid var(--bar); }
3899
+ .split { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 16px; }
3900
+ @media (max-width: 820px) { .split { grid-template-columns: 1fr; } .wf .step { grid-template-columns: 120px 52px minmax(0, 1fr) 56px; } }
3901
+ .wf { display: grid; gap: 3px; font-size: 12px; }
3902
+ .wf .step { display: grid; grid-template-columns: 170px 64px minmax(0, 1fr) 70px; gap: 10px; align-items: center; }
3903
+ .wf .lane { position: relative; height: 14px; background: var(--bg); border-radius: 3px; }
3904
+ .wf .seg-bar { position: absolute; top: 2px; height: 10px; min-width: 3px; border-radius: 3px; }
3905
+ .wf .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
3906
+ .wf .at { font-family: var(--mono); color: var(--faint); text-align: right; }
3907
+ .wf .t { text-align: right; font-family: var(--mono); color: var(--muted); }
3908
+ .legend { display: flex; gap: 14px; font-size: 12px; color: var(--muted); margin-bottom: 10px; flex-wrap: wrap; }
3909
+ .legend span { display: inline-flex; align-items: center; gap: 6px; }
3910
+ footer { font-size: 12px; color: var(--faint); }
3911
+ [hidden] { display: none !important; }
3487
3912
  </style>
3488
3913
  </head>
3489
3914
  <body>
3915
+ <div class="top"><div class="bar-inner">
3916
+ <div class="brand"><span class="mark">cb</span>CodeBuddy usage</div>
3917
+ <nav class="tabs" aria-label="Views">
3918
+ <a href="#overview" data-view="overview">Overview</a>
3919
+ <a href="#users" data-view="users">Users</a>
3920
+ <a href="#events" data-view="events">Events</a>
3921
+ </nav>
3922
+ <span class="spacer"></span>
3923
+ <span class="updated" id="updated">loading\u2026</span>
3924
+ <div class="seg" role="group" aria-label="Time range" id="range">
3925
+ <button data-days="7">7d</button><button data-days="30" aria-pressed="true">30d</button><button data-days="90">90d</button><button data-days="365">1y</button>
3926
+ </div>
3927
+ </div></div>
3928
+
3490
3929
  <div class="wrap">
3491
- <header>
3492
- <h1>CodeBuddy usage<small id="updated"></small></h1>
3493
- <span class="spacer"></span>
3494
- <div class="seg" role="group" aria-label="Time range" id="range">
3495
- <button data-days="7">7 days</button>
3496
- <button data-days="30" aria-pressed="true">30 days</button>
3497
- <button data-days="90">90 days</button>
3498
- <button data-days="365">1 year</button>
3930
+ <section id="view-overview">
3931
+ <div class="hero" id="hero" aria-label="Headline numbers"></div>
3932
+ <div class="grid2" style="margin-top:16px">
3933
+ <section class="panel"><div class="head"><h2>Total installs</h2><span class="note">all-time, by first-seen day</span></div><div class="chart" id="cumulative"></div></section>
3934
+ <section class="panel"><div class="head"><h2>Daily active installs</h2><span class="note">installs that sent any event</span></div><div class="chart" id="active"></div></section>
3499
3935
  </div>
3500
- </header>
3501
- <section class="kpis" id="kpis" aria-label="Summary"></section>
3502
- <div class="cols">
3503
- <section class="panel"><h2>Daily active installs</h2><div class="chart" id="active"></div></section>
3504
- <section class="panel"><h2>npm downloads per day</h2><div class="chart" id="npm"></div></section>
3505
- </div>
3506
- <section class="panel"><h2>Feature usage</h2><div class="scroll" id="features"></div></section>
3507
- <div class="cols">
3508
- <section class="panel"><h2>Agent clients</h2><div class="rows" id="clients"></div></section>
3509
- <section class="panel"><h2>Versions</h2><div class="rows" id="versions"></div></section>
3510
- <section class="panel"><h2>Platforms</h2><div class="rows" id="platforms"></div></section>
3511
- </div>
3512
- <section class="panel">
3513
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px"><h2 style="margin:0">Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span><button class="btn" id="csv">Download CSV</button></div>
3514
- <div class="scroll users" id="users"></div>
3936
+ <div class="grid2" style="margin-top:16px">
3937
+ <section class="panel"><div class="head"><h2>Activation</h2><span class="note">installs active in range reaching each step</span></div><div class="rows funnel" id="funnel"></div></section>
3938
+ <section class="panel"><div class="head"><h2>Retention by weekly cohort</h2><span class="note">share of each week's new installs active in later weeks</span></div><div class="scroll" id="cohorts"></div></section>
3939
+ </div>
3940
+ <section class="panel" style="margin-top:16px"><div class="head"><h2>Feature usage</h2><span class="note">MCP tools, hook steps (recall, capture\u2026) and CLI commands</span></div><div class="scroll" id="features"></div></section>
3941
+ <div class="grid3" style="margin-top:16px">
3942
+ <section class="panel"><div class="head"><h2>Agent clients</h2></div><div class="rows" id="clients"></div></section>
3943
+ <section class="panel"><div class="head"><h2>Versions</h2></div><div class="rows" id="versions"></div></section>
3944
+ <section class="panel"><div class="head"><h2>Platforms</h2></div><div class="rows" id="platforms"></div></section>
3945
+ </div>
3946
+ <section class="panel" style="margin-top:16px"><div class="head"><h2>npm downloads per day</h2><span class="note" id="npmNote"></span></div><div class="chart" id="npm"></div></section>
3947
+ </section>
3948
+
3949
+ <section id="view-users" class="panel" style="margin-top:16px" hidden>
3950
+ <div class="toolbar">
3951
+ <h2>Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span>
3952
+ <input type="search" id="userSearch" placeholder="Filter by id, client, version, platform\u2026" aria-label="Filter users">
3953
+ <button class="btn" id="csv">Download CSV</button>
3954
+ </div>
3955
+ <div class="scroll" id="users"></div>
3515
3956
  </section>
3516
- <section class="panel"><h2>Latest events</h2><div class="scroll" id="recent"></div></section>
3517
- <p class="muted" style="margin:0;font-size:12px">Anonymous telemetry from installs that did not opt out: random install ids, tool names, timings and versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</p>
3957
+
3958
+ <section id="view-install" style="margin-top:16px" hidden></section>
3959
+
3960
+ <section id="view-events" class="panel" style="margin-top:16px" hidden>
3961
+ <div class="toolbar"><h2>Latest events</h2><span class="spacer"></span><div class="chips" id="kindFilter"></div></div>
3962
+ <div class="scroll" id="recent"></div>
3963
+ </section>
3964
+
3965
+ <footer>Anonymous telemetry from installs that did not opt out: random install ids, tool and command names, timings, versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</footer>
3518
3966
  </div>
3519
- <div class="tip" id="tip"></div>
3967
+ <div class="tip" id="tip" role="status"></div>
3520
3968
  <script>
3521
3969
  (function () {
3522
3970
  var days = 30;
3523
- // Works at "/" and under a proxy mount such as "/admin" (with or without a trailing slash).
3524
- var base = location.pathname.replace(/\\/?$/, "/");
3525
- var summary = null;
3526
- var npm = null;
3971
+ var summary = null, npm = null, install = null;
3972
+ var userSort = { key: "lastSeen", dir: -1 };
3973
+ var kindShown = "all";
3974
+ var selectedSession = null;
3975
+ var path = location.pathname;
3976
+ var base = path.charAt(path.length - 1) === "/" ? path : path + "/";
3527
3977
  var tip = document.getElementById("tip");
3978
+ var NL = String.fromCharCode(10);
3979
+ var KIND_COLOR = { tool: "var(--bar)", cli: "var(--cli)" };
3980
+ function kindColor(kind) { return KIND_COLOR[kind] || "var(--hook)"; }
3981
+ function kindGroup(kind) { return kind === "tool" ? "MCP tool" : kind === "cli" ? "CLI command" : "hook step"; }
3528
3982
 
3983
+ function $(id) { return document.getElementById(id); }
3529
3984
  function esc(value) {
3530
3985
  return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
3531
3986
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
@@ -3533,36 +3988,57 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
3533
3988
  }
3534
3989
  function num(n) { return Math.round(n).toLocaleString(); }
3535
3990
  function compact(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e4 ? (n / 1e3).toFixed(1) + "k" : num(n); }
3536
- function pct(n) { return (n * 100).toFixed(n < 0.1 ? 1 : 0) + "%"; }
3537
- function ms(n) { return n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
3991
+ function pct(n) { return (n * 100).toFixed(n > 0 && n < 0.1 ? 1 : 0) + "%"; }
3992
+ function plural(n, word) { return num(n) + " " + word + (n === 1 ? "" : "s"); }
3993
+ function offset(n) { var s = Math.round(n / 1000); return "+" + (s >= 60 ? Math.floor(s / 60) + "m " + (s % 60) + "s" : s + "s"); }
3994
+ function ms(n) { return n >= 60000 ? (n / 60000).toFixed(1) + " min" : n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
3995
+ function date(iso) { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" }); }
3996
+ function ago(iso) {
3997
+ var s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
3998
+ return s < 60 ? "just now" : s < 3600 ? Math.floor(s / 60) + "m ago" : s < 86400 ? Math.floor(s / 3600) + "h ago" : Math.floor(s / 86400) + "d ago";
3999
+ }
4000
+ function get(url) { return fetch(base + url).then(function (r) { return r.ok ? r.json() : null; }); }
3538
4001
 
3539
4002
  function kpi(label, value, sub) {
3540
- return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) +
3541
- '</div><div class="sub">' + esc(sub || "") + "</div></div>";
4003
+ return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) + '</div><div class="sub">' + (sub || "") + "</div></div>";
4004
+ }
4005
+ function delta(now, before) {
4006
+ if (!before) return now ? '<span class="up">\u25B2 new</span> vs previous 7 days' : "no activity yet";
4007
+ var change = (now - before) / before;
4008
+ return '<span class="' + (change >= 0 ? "up" : "down") + '">' + (change >= 0 ? "\u25B2 " : "\u25BC ") + pct(Math.abs(change)) + "</span> vs previous 7 days";
3542
4009
  }
3543
4010
 
3544
- /** Bars of one measure over days; hover shows the exact value. */
3545
- function bars(points, label) {
3546
- if (!points.length) return '<div class="empty">No data yet.</div>';
3547
- var w = 600, h = 180, top = 10, bottom = 22, left = 36;
4011
+ /** Bars or a filled line over days, one measure; every point has a hover target. */
4012
+ /** Draw into a container at its own pixel width, so labels never stretch. */
4013
+ function draw(id, points, label, line) {
4014
+ var el = $(id);
4015
+ if (el) el.innerHTML = chart(points, label, line, el.clientWidth);
4016
+ }
4017
+ function chart(points, label, line, width) {
4018
+ if (!points.length) return '<div class="empty-state">No data yet.</div>';
4019
+ var w = Math.max(280, width || 640), h = 190, top = 10, bottom = 22, left = 40, plot = h - top - bottom;
3548
4020
  var max = Math.max.apply(null, points.map(function (p) { return p.value; }).concat([1]));
3549
4021
  var step = (w - left) / points.length;
3550
- var bw = Math.max(1, step - 2);
4022
+ var y = function (v) { return top + plot * (1 - v / max); };
3551
4023
  var out = '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" role="img" aria-label="' + esc(label) + '">';
3552
4024
  [0, 0.5, 1].forEach(function (f) {
3553
- var y = top + (h - top - bottom) * (1 - f);
3554
- out += '<line x1="' + left + '" x2="' + w + '" y1="' + y + '" y2="' + y + '" stroke="var(--line)" stroke-width="1"/>';
3555
- out += '<text x="' + (left - 6) + '" y="' + (y + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
4025
+ out += '<line x1="' + left + '" x2="' + w + '" y1="' + y(max * f) + '" y2="' + y(max * f) + '" stroke="var(--line)"/>';
4026
+ out += '<text x="' + (left - 6) + '" y="' + (y(max * f) + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
3556
4027
  });
4028
+ if (line) {
4029
+ var pts = points.map(function (p, i) { return (left + i * step + step / 2).toFixed(1) + "," + y(p.value).toFixed(1); });
4030
+ out += '<path d="M' + (left + step / 2) + "," + (top + plot) + " L" + pts.join(" L") + " L" + (left + (points.length - 0.5) * step) + "," + (top + plot) + ' Z" fill="var(--bar-soft)"/>';
4031
+ out += '<polyline points="' + pts.join(" ") + '" fill="none" stroke="var(--bar)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round"/>';
4032
+ }
3557
4033
  points.forEach(function (p, i) {
3558
- var bh = (h - top - bottom) * (p.value / max);
3559
- var x = left + i * step + 1;
3560
- var tipText = esc(p.date + " \xB7 " + num(p.value) + " " + label);
3561
- out += '<rect class="hit" x="' + (left + i * step) + '" y="' + top + '" width="' + step + '" height="' + (h - top - bottom) + '" data-tip="' + tipText + '"/>';
3562
- out += '<rect class="mark" x="' + x + '" y="' + (h - bottom - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)" data-tip="' + tipText + '"/>';
4034
+ var x = left + i * step;
4035
+ if (!line) {
4036
+ var bh = plot * (p.value / max), bw = Math.max(1, step - 2);
4037
+ out += '<rect x="' + (x + 1) + '" y="' + (top + plot - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)"/>';
4038
+ }
4039
+ out += '<rect class="hit" x="' + x + '" y="' + top + '" width="' + step + '" height="' + plot + '" data-tip="' + esc(p.date + " \xB7 " + num(p.value) + " " + label) + '"/>';
3563
4040
  });
3564
- var first = points[0].date, last = points[points.length - 1].date;
3565
- out += '<text x="' + left + '" y="' + (h - 6) + '">' + esc(first) + '</text><text x="' + w + '" y="' + (h - 6) + '" text-anchor="end">' + esc(last) + "</text>";
4041
+ out += '<text x="' + left + '" y="' + (h - 5) + '">' + esc(points[0].date) + '</text><text x="' + w + '" y="' + (h - 5) + '" text-anchor="end">' + esc(points[points.length - 1].date) + "</text>";
3566
4042
  return out + "</svg>";
3567
4043
  }
3568
4044
 
@@ -3570,85 +4046,230 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
3570
4046
  if (!items.length) return '<div class="muted">No data yet.</div>';
3571
4047
  return items.slice(0, 8).map(function (item) {
3572
4048
  var share = total ? item.installs / total : 0;
3573
- return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) +
3574
- '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
4049
+ return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
3575
4050
  }).join("");
3576
4051
  }
3577
4052
 
3578
- function npmPoints() {
3579
- if (!npm || !npm.downloads) return [];
3580
- return npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; });
4053
+ function renderOverview() {
4054
+ var t = summary.totals;
4055
+ var npm30 = npmTotal(30);
4056
+ $("hero").innerHTML =
4057
+ kpi("Weekly active installs", num(t.wau), delta(t.wau, t.wauPrevious)) +
4058
+ kpi("Total installs", num(t.allTimeInstalls), "+" + num(t.newInRange) + " in the last " + summary.days + " days \xB7 " + num(t.newToday) + " today") +
4059
+ kpi("Week-1 retention", t.week1Retention == null ? "\u2013" : pct(t.week1Retention), t.week1Retention == null ? "needs a cohort older than 7 days" : "new installs active again the next week") +
4060
+ kpi("Agent sessions", compact(t.sessions), num(t.dau) + " installs active today \xB7 " + num(t.mau) + " this month") +
4061
+ kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm && npm.error ? esc(npm.error) : num(npmTotal(365) || 0) + " in the last year");
4062
+ draw("cumulative", summary.daily.map(function (d) { return { date: d.date, value: d.totalInstalls }; }), "total installs", true);
4063
+ draw("active", summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs", false);
4064
+
4065
+ var active = summary.funnel[0] ? summary.funnel[0].installs : 0;
4066
+ $("funnel").innerHTML = active ? summary.funnel.map(function (step) {
4067
+ var share = active ? step.installs / active : 0;
4068
+ return '<div class="row"><span>' + esc(step.label) + '</span><span class="n">' + num(step.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
4069
+ }).join("") : '<div class="empty-state">No active installs in this range.</div>';
4070
+
4071
+ var cohorts = summary.cohorts.filter(function (c) { return c.size > 0; });
4072
+ var weeks = summary.cohorts.length;
4073
+ $("cohorts").innerHTML = cohorts.length
4074
+ ? '<table class="heat"><thead><tr><th>Cohort</th><th class="n">Installs</th>' + Array.from({ length: weeks }, function (_, i) { return '<th class="n">W' + i + "</th>"; }).join("") + "</tr></thead><tbody>" +
4075
+ cohorts.map(function (c) {
4076
+ var cells = "";
4077
+ for (var i = 0; i < weeks; i += 1) {
4078
+ var v = c.weeks[i];
4079
+ if (v == null) { cells += '<td class="empty">\xB7</td>'; continue; }
4080
+ var strength = Math.round(12 + v * 88);
4081
+ cells += '<td style="background:color-mix(in oklab, var(--bar) ' + strength + '%, var(--panel));color:' + (v > 0.5 ? "#fff" : "var(--fg)") + '" title="' + esc(c.start + " cohort, week " + i + ": " + pct(v)) + '">' + pct(v) + "</td>";
4082
+ }
4083
+ return '<tr><td class="lab">' + esc(date(c.start)) + '</td><td class="n">' + num(c.size) + "</td>" + cells + "</tr>";
4084
+ }).join("") + "</tbody></table>"
4085
+ : '<div class="empty-state">Cohorts appear once new installs report in.</div>';
4086
+
4087
+ var maxCalls = Math.max.apply(null, summary.features.map(function (f) { return f.calls; }).concat([1]));
4088
+ $("features").innerHTML = summary.features.length
4089
+ ? '<table><thead><tr><th>Feature</th><th class="n">Calls</th><th class="n">Installs</th><th class="n">Median</th><th class="n">p95</th><th class="n">Errors</th></tr></thead><tbody>' +
4090
+ summary.features.map(function (f) {
4091
+ return '<tr><td class="mono">' + esc(f.label) + '<span class="inline" style="width:' + Math.max(2, (f.calls / maxCalls) * 120).toFixed(0) + 'px"></span></td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
4092
+ '</td><td class="n">' + ms(f.p50ms) + '</td><td class="n">' + ms(f.p95ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
4093
+ }).join("") + "</tbody></table>"
4094
+ : '<div class="empty-state">No events yet. Installs send a batch at most every 10 minutes.</div>';
4095
+ var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
4096
+ $("clients").innerHTML = rows(summary.clients, installs);
4097
+ $("versions").innerHTML = rows(summary.versions, installs);
4098
+ $("platforms").innerHTML = rows(summary.platforms, installs);
4099
+ renderNpm();
3581
4100
  }
4101
+
3582
4102
  function npmTotal(n) {
3583
4103
  if (!npm || !npm.downloads) return null;
3584
4104
  return npm.downloads.slice(-n).reduce(function (sum, d) { return sum + d.downloads; }, 0);
3585
4105
  }
4106
+ function renderNpm() {
4107
+ if (!npm) return;
4108
+ if (npm.error) { $("npm").innerHTML = '<div class="empty-state">' + esc(npm.error) + "</div>"; return; }
4109
+ $("npmNote").textContent = npm.package + " \xB7 public npm registry";
4110
+ draw("npm", npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; }), "downloads", false);
4111
+ }
3586
4112
 
3587
- function render() {
3588
- if (!summary) return;
3589
- var t = summary.totals;
3590
- var npm30 = npmTotal(30), npm365 = npmTotal(365);
3591
- document.getElementById("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
3592
- document.getElementById("kpis").innerHTML =
3593
- kpi("Active installs \xB7 7 days", num(t.active7d), num(t.active1d) + " today") +
3594
- kpi("Active installs \xB7 30 days", num(t.active30d), num(t.newToday) + " new today") +
3595
- kpi("Installs in range", num(t.installs), num(t.projects) + " projects") +
3596
- kpi("Agent sessions", num(t.sessions), "in " + summary.days + " days") +
3597
- kpi("Events", compact(t.events), pct(1 - t.errorRate) + " succeeded") +
3598
- kpi("Tokens delivered", compact(t.tokensOut), "context sent to agents") +
3599
- kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm365 == null ? (npm && npm.error ? npm.error : "") : num(npm365) + " in the last year");
3600
- document.getElementById("active").innerHTML = bars(summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs");
3601
- document.getElementById("npm").innerHTML = npm && npm.error ? '<div class="empty">' + esc(npm.error) + "</div>" : bars(npmPoints(), "downloads");
3602
- document.getElementById("features").innerHTML = summary.features.length
3603
- ? "<table><thead><tr><th>Feature</th><th class=\\"n\\">Calls</th><th class=\\"n\\">Installs</th><th class=\\"n\\">Median</th><th class=\\"n\\">Errors</th></tr></thead><tbody>" +
3604
- summary.features.map(function (f) {
3605
- return "<tr><td class=\\"mono\\">" + esc(f.label) + '</td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
3606
- '</td><td class="n">' + ms(f.p50ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
3607
- }).join("") + "</tbody></table>"
3608
- : '<div class="empty">No events yet. Installs send a batch at most every 10 minutes.</div>';
3609
- var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
3610
- document.getElementById("clients").innerHTML = rows(summary.clients, installs);
3611
- document.getElementById("versions").innerHTML = rows(summary.versions, installs);
3612
- document.getElementById("platforms").innerHTML = rows(summary.platforms, installs);
3613
- document.getElementById("userCount").textContent = num(summary.users.length) + " installs seen in " + summary.days + " days";
3614
- document.getElementById("users").innerHTML = summary.users.length
3615
- ? "<table><thead><tr><th>Install</th><th>First seen</th><th>Last active</th><th class=\\"n\\">Events</th><th class=\\"n\\">Sessions</th><th class=\\"n\\">Projects</th><th>Client</th><th>Version</th><th>Platform</th><th>Top feature</th></tr></thead><tbody>" +
3616
- summary.users.map(function (u) {
3617
- return '<tr><td class="mono">' + esc(u.install) + "</td><td>" + esc(new Date(u.firstSeen).toLocaleDateString()) + "</td><td>" + esc(new Date(u.lastSeen).toLocaleString()) +
3618
- '</td><td class="n">' + num(u.events) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.projects) + "</td><td>" + esc(u.client) +
3619
- '</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
4113
+ var USER_COLS = [
4114
+ ["install", "Install"], ["firstSeen", "First seen"], ["lastSeen", "Last active"], ["activeDays", "Active days", 1],
4115
+ ["sessions", "Sessions", 1], ["events", "Events", 1], ["errors", "Errors", 1], ["client", "Client"], ["version", "Version"], ["platform", "Platform"], ["topFeature", "Top feature"]
4116
+ ];
4117
+ function filteredUsers() {
4118
+ var q = $("userSearch").value.trim().toLowerCase();
4119
+ return summary.users.filter(function (u) {
4120
+ return !q || [u.install, u.client, u.version, u.platform, u.topFeature].join(" ").toLowerCase().indexOf(q) !== -1;
4121
+ }).sort(function (a, b) {
4122
+ var x = a[userSort.key], y = b[userSort.key];
4123
+ return (x < y ? -1 : x > y ? 1 : 0) * userSort.dir;
4124
+ });
4125
+ }
4126
+ function renderUsers() {
4127
+ var list = filteredUsers();
4128
+ $("userCount").textContent = num(summary.users.length) + " installs active in the last " + summary.days + " days";
4129
+ $("users").innerHTML = list.length
4130
+ ? "<table><thead><tr>" + USER_COLS.map(function (c) {
4131
+ return "<th" + (c[2] ? ' class="n"' : "") + '><button data-sort="' + c[0] + '" data-dir="' + (userSort.key === c[0] ? (userSort.dir > 0 ? "\u2191" : "\u2193") : "") + '">' + c[1] + "</button></th>";
4132
+ }).join("") + "</tr></thead><tbody>" +
4133
+ list.map(function (u) {
4134
+ return '<tr class="link" tabindex="0" data-install="' + esc(u.install) + '"><td class="mono">' + esc(u.install) + "</td><td>" + esc(date(u.firstSeen)) + "</td><td>" + esc(ago(u.lastSeen)) +
4135
+ '</td><td class="n">' + num(u.activeDays) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.events) + '</td><td class="n' + (u.errors ? " bad" : "") + '">' + num(u.errors) +
4136
+ "</td><td>" + esc(u.client) + '</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
3620
4137
  }).join("") + "</tbody></table>"
3621
- : '<div class="empty">No users yet.</div>';
3622
- document.getElementById("recent").innerHTML = summary.recent.length
3623
- ? "<table><thead><tr><th>Received</th><th>Install</th><th>Kind</th><th>Name</th><th>Client</th><th>Version</th><th class=\\"n\\">Time</th><th>Result</th></tr></thead><tbody>" +
3624
- summary.recent.map(function (e) {
3625
- return "<tr><td>" + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + "</td><td>" + esc(e.kind) +
3626
- '</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) +
3627
- '</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "error") + "</td></tr>";
4138
+ : '<div class="empty-state">' + (summary.users.length ? "No installs match this filter." : "No users yet.") + "</div>";
4139
+ }
4140
+
4141
+ function renderInstall() {
4142
+ var el = $("view-install");
4143
+ if (!install) { el.innerHTML = '<div class="panel empty-state">Loading\u2026</div>'; return; }
4144
+ if (install.missing) { el.innerHTML = '<div class="panel empty-state">No events for this install in the last 90 days. <a href="#users">Back to users</a></div>'; return; }
4145
+ var errors = install.events.filter(function (e) { return !e.ok; }).length;
4146
+ var perDay = {};
4147
+ install.perDay.forEach(function (d) { perDay[d.date] = d.events; });
4148
+ var points = [];
4149
+ for (var i = 89; i >= 0; i -= 1) {
4150
+ var d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
4151
+ points.push({ date: d, value: perDay[d] || 0 });
4152
+ }
4153
+ if (!selectedSession || !install.sessions.some(function (s) { return s.id === selectedSession; })) selectedSession = install.sessions[0] ? install.sessions[0].id : null;
4154
+ el.innerHTML =
4155
+ '<div class="toolbar"><a class="btn" href="#users">\u2190 Users</a><h2 style="font-size:14px;color:var(--fg);text-transform:none;letter-spacing:0">Install <span class="mono">' + esc(install.install) + "</span></h2>" +
4156
+ '<span class="chips"><span class="chip">' + esc(install.client) + '</span><span class="chip">v' + esc(install.version) + '</span><span class="chip">' + esc(install.platform) + '</span><span class="chip">node ' + esc(install.node) + "</span></span></div>" +
4157
+ '<div class="hero" style="padding-top:0">' + kpi("Events \xB7 90 days", num(install.events.length), errors ? '<span class="bad">' + plural(errors, "error") + "</span>" : "no errors") +
4158
+ kpi("Sessions", num(install.sessions.length), "agent sessions with CodeBuddy") + kpi("Active days", num(install.perDay.length), "in the last 90 days") + kpi("Projects", num(install.projects), "hashed, never named") + "</div>" +
4159
+ '<section class="panel" style="margin-top:16px"><div class="head"><h2>Activity</h2><span class="note">events per day, last 90 days</span></div><div class="chart" id="installActivity"></div></section>' +
4160
+ '<section class="panel" style="margin-top:16px"><div class="head"><h2>Session traces</h2><span class="note">each step ends at its recorded time; bar length is its duration</span></div>' +
4161
+ '<div class="split"><div class="sessions" id="sessionList">' + install.sessions.map(function (s) {
4162
+ return '<button class="session" data-session="' + esc(s.id) + '" aria-pressed="' + (s.id === selectedSession) + '"><strong class="mono">' + esc(s.id === "none" ? "no session id" : s.id.slice(0, 8)) + "</strong>" +
4163
+ '<span class="muted">' + esc(new Date(s.start).toLocaleString()) + " \xB7 " + num(s.events) + " steps" + (s.errors ? ' \xB7 <span class="bad">' + plural(s.errors, "error") + "</span>" : "") + "</span></button>";
4164
+ }).join("") + '</div><div id="waterfall"></div></div></section>';
4165
+ draw("installActivity", points, "events", false);
4166
+ renderWaterfall();
4167
+ }
4168
+
4169
+ function renderWaterfall() {
4170
+ var target = $("waterfall");
4171
+ if (!target || !install) return;
4172
+ var steps = install.events.filter(function (e) { return e.session === selectedSession; })
4173
+ .sort(function (a, b) { return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0; }).slice(-200);
4174
+ if (!steps.length) { target.innerHTML = '<div class="empty-state">Pick a session.</div>'; return; }
4175
+ var start = Math.min.apply(null, steps.map(function (e) { return Date.parse(e.ts) - e.ms; }));
4176
+ var end = Math.max.apply(null, steps.map(function (e) { return Date.parse(e.ts); }));
4177
+ var span = Math.max(1, end - start);
4178
+ target.innerHTML = '<div class="legend"><span><i class="sw" style="background:var(--bar)"></i>MCP tool</span><span><i class="sw" style="background:var(--hook)"></i>hook step</span><span><i class="sw" style="background:var(--cli)"></i>CLI command</span><span class="spacer"></span><span>' + esc(ms(span)) + " total</span></div>" +
4179
+ '<div class="wf">' + steps.map(function (e) {
4180
+ var s = Date.parse(e.ts) - e.ms;
4181
+ var left = ((s - start) / span) * 100, width = Math.max(0.8, (e.ms / span) * 100);
4182
+ return '<div class="step"><span class="name' + (e.ok ? "" : " bad") + '" title="' + esc(kindGroup(e.kind) + ": " + e.name) + '">' + (e.ok ? "" : "\u2715 ") + esc(e.feature) + '</span><span class="at">' + esc(offset(s - start)) + '</span><span class="lane"><span class="seg-bar" style="left:' + left.toFixed(2) + "%;width:" + width.toFixed(2) + "%;background:" + kindColor(e.kind) + '" data-tip="' + esc(e.feature + " \xB7 " + ms(e.ms) + (e.ok ? "" : " \xB7 error") + (e.tokensOut ? " \xB7 " + num(e.tokensOut) + " tokens out" : "")) + '"></span></span><span class="t">' + esc(ms(e.ms)) + "</span></div>";
4183
+ }).join("") + "</div>";
4184
+ }
4185
+
4186
+ function renderEvents() {
4187
+ var kinds = ["all"];
4188
+ summary.recent.forEach(function (e) { if (kinds.indexOf(e.kind) === -1) kinds.push(e.kind); });
4189
+ $("kindFilter").innerHTML = '<div class="seg" role="group" aria-label="Event kind">' + kinds.map(function (k) {
4190
+ return '<button data-kind="' + esc(k) + '" aria-pressed="' + (k === kindShown) + '">' + esc(k) + "</button>";
4191
+ }).join("") + "</div>";
4192
+ var list = summary.recent.filter(function (e) { return kindShown === "all" || e.kind === kindShown; });
4193
+ $("recent").innerHTML = list.length
4194
+ ? '<table><thead><tr><th>Received</th><th>Install</th><th>Kind</th><th>Name</th><th>Client</th><th>Version</th><th class="n">Time</th><th>Result</th></tr></thead><tbody>' +
4195
+ list.map(function (e) {
4196
+ return '<tr class="link" tabindex="0" data-install="' + esc(e.install) + '"><td>' + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + '</td><td><span class="sw" style="background:' + kindColor(e.kind) + '"></span> ' + esc(e.kind) +
4197
+ '</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) + '</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "\u2715 error") + "</td></tr>";
3628
4198
  }).join("") + "</tbody></table>"
3629
- : '<div class="empty">No events yet.</div>';
4199
+ : '<div class="empty-state">No events yet.</div>';
3630
4200
  }
3631
4201
 
3632
- function load() {
3633
- fetch(base + "api/summary?days=" + days).then(function (r) { return r.json(); }).then(function (s) { summary = s; render(); });
4202
+ function route() {
4203
+ var hash = location.hash.replace("#", "") || "overview";
4204
+ var view = hash.indexOf("install/") === 0 ? "install" : ["overview", "users", "events"].indexOf(hash) !== -1 ? hash : "overview";
4205
+ ["overview", "users", "install", "events"].forEach(function (v) { $("view-" + v).hidden = v !== view; });
4206
+ document.querySelectorAll(".tabs a").forEach(function (a) {
4207
+ var current = a.dataset.view === view || (view === "install" && a.dataset.view === "users");
4208
+ if (current) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current");
4209
+ });
4210
+ $("range").hidden = view === "install";
4211
+ if (view === "install") {
4212
+ var id = hash.slice(8);
4213
+ if (!install || install.install !== id) {
4214
+ install = null;
4215
+ renderInstall();
4216
+ get("api/install?id=" + encodeURIComponent(id)).then(function (data) { install = data || { install: id, missing: true }; renderInstall(); });
4217
+ } else renderInstall();
4218
+ } else render();
3634
4219
  }
3635
- fetch(base + "api/npm").then(function (r) { return r.json(); }).then(function (n) { npm = n; render(); });
3636
4220
 
3637
- document.getElementById("range").addEventListener("click", function (event) {
4221
+ function render() {
4222
+ if (!summary) return;
4223
+ $("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
4224
+ var hash = location.hash.replace("#", "") || "overview";
4225
+ if (hash === "users") renderUsers();
4226
+ else if (hash === "events") renderEvents();
4227
+ else if (hash.indexOf("install/") !== 0) renderOverview();
4228
+ }
4229
+
4230
+ function load() { get("api/summary?days=" + days).then(function (s) { if (s) { summary = s; render(); } }); }
4231
+ get("api/npm").then(function (n) { npm = n; if (summary) render(); });
4232
+
4233
+ $("range").addEventListener("click", function (event) {
3638
4234
  var button = event.target.closest("button");
3639
4235
  if (!button) return;
3640
4236
  days = Number(button.dataset.days);
3641
4237
  document.querySelectorAll("#range button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
3642
4238
  load();
3643
4239
  });
3644
- document.getElementById("csv").addEventListener("click", function () {
4240
+ $("userSearch").addEventListener("input", function () { if (summary) renderUsers(); });
4241
+ document.addEventListener("click", function (event) {
4242
+ var sort = event.target.closest("[data-sort]");
4243
+ if (sort) {
4244
+ var key = sort.dataset.sort;
4245
+ userSort = { key: key, dir: userSort.key === key ? -userSort.dir : -1 };
4246
+ renderUsers();
4247
+ return;
4248
+ }
4249
+ var row = event.target.closest("tr[data-install]");
4250
+ if (row) { location.hash = "install/" + row.dataset.install; return; }
4251
+ var session = event.target.closest("[data-session]");
4252
+ if (session) {
4253
+ selectedSession = session.dataset.session;
4254
+ document.querySelectorAll("#sessionList .session").forEach(function (b) { b.setAttribute("aria-pressed", String(b === session)); });
4255
+ renderWaterfall();
4256
+ return;
4257
+ }
4258
+ var kind = event.target.closest("[data-kind]");
4259
+ if (kind) { kindShown = kind.dataset.kind; renderEvents(); }
4260
+ });
4261
+ document.addEventListener("keydown", function (event) {
4262
+ var row = event.target.closest && event.target.closest("tr[data-install]");
4263
+ if (row && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); location.hash = "install/" + row.dataset.install; }
4264
+ });
4265
+ $("csv").addEventListener("click", function () {
3645
4266
  if (!summary) return;
3646
- var cols = ["install", "firstSeen", "lastSeen", "events", "sessions", "projects", "client", "version", "platform", "topFeature"];
3647
- var lines = [cols.join(",")].concat(summary.users.map(function (u) {
4267
+ var cols = USER_COLS.map(function (c) { return c[0]; });
4268
+ var lines = [cols.join(",")].concat(filteredUsers().map(function (u) {
3648
4269
  return cols.map(function (c) { return '"' + String(u[c]).replace(/"/g, '""') + '"'; }).join(",");
3649
4270
  }));
3650
4271
  var link = document.createElement("a");
3651
- link.href = URL.createObjectURL(new Blob([lines.join("\\n")], { type: "text/csv" }));
4272
+ link.href = URL.createObjectURL(new Blob([lines.join(NL)], { type: "text/csv" }));
3652
4273
  link.download = "codebuddy-users-" + new Date().toISOString().slice(0, 10) + ".csv";
3653
4274
  link.click();
3654
4275
  });
@@ -3657,10 +4278,16 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
3657
4278
  if (!text) { tip.style.display = "none"; return; }
3658
4279
  tip.textContent = text;
3659
4280
  tip.style.display = "block";
3660
- var x = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8);
3661
- tip.style.left = x + "px";
3662
- tip.style.top = (event.pageY - 32) + "px";
4281
+ tip.style.left = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8) + "px";
4282
+ tip.style.top = (event.pageY - 34) + "px";
4283
+ });
4284
+ var resizeTimer = 0;
4285
+ window.addEventListener("resize", function () {
4286
+ clearTimeout(resizeTimer);
4287
+ resizeTimer = setTimeout(function () { if (location.hash.indexOf("#install/") === 0) renderInstall(); else render(); }, 150);
3663
4288
  });
4289
+ window.addEventListener("hashchange", route);
4290
+ route();
3664
4291
  load();
3665
4292
  setInterval(load, 60000);
3666
4293
  })();
@@ -3699,7 +4326,8 @@ var batchSchema = z2.object({
3699
4326
  });
3700
4327
  async function startCollector(options) {
3701
4328
  const { dataDir } = options;
3702
- await mkdir2(dataDir, { recursive: true });
4329
+ await mkdir3(dataDir, { recursive: true });
4330
+ const registry = await loadRegistry(dataDir);
3703
4331
  let summaryCache;
3704
4332
  let npmCache;
3705
4333
  const perInstall = /* @__PURE__ */ new Map();
@@ -3743,10 +4371,18 @@ async function startCollector(options) {
3743
4371
  return;
3744
4372
  }
3745
4373
  const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
4374
+ if (!registry.has(batch.installId)) {
4375
+ registry.set(batch.installId, receivedAt);
4376
+ await appendFile(
4377
+ join7(dataDir, "installs.jsonl"),
4378
+ `${JSON.stringify({ installId: batch.installId, firstSeen: receivedAt })}
4379
+ `
4380
+ );
4381
+ }
3746
4382
  const { events, ...meta } = batch;
3747
4383
  const lines = events.map((event) => JSON.stringify({ ...meta, ...event, receivedAt }));
3748
4384
  await appendFile(
3749
- join6(dataDir, `events-${receivedAt.slice(0, 10)}.jsonl`),
4385
+ join7(dataDir, `events-${receivedAt.slice(0, 10)}.jsonl`),
3750
4386
  `${lines.join("\n")}
3751
4387
  `
3752
4388
  );
@@ -3790,12 +4426,29 @@ async function startCollector(options) {
3790
4426
  summaryCache = {
3791
4427
  at: Date.now(),
3792
4428
  days,
3793
- value: summarize(await readEvents(dataDir, days), days)
4429
+ value: summarize(
4430
+ await readEvents(dataDir, Math.max(days, COHORT_WEEKS * 7)),
4431
+ days,
4432
+ Date.now(),
4433
+ registry
4434
+ )
3794
4435
  };
3795
4436
  }
3796
4437
  json2(summaryCache.value);
3797
4438
  return;
3798
4439
  }
4440
+ if (url.pathname === "/api/install") {
4441
+ const trace = installTrace(
4442
+ await readEvents(dataDir, 90),
4443
+ (url.searchParams.get("id") ?? "").toLowerCase()
4444
+ );
4445
+ if (!trace) {
4446
+ response.writeHead(404, { "content-type": "application/json" }).end("null");
4447
+ return;
4448
+ }
4449
+ json2(trace);
4450
+ return;
4451
+ }
3799
4452
  if (url.pathname === "/api/npm") {
3800
4453
  if (!npmCache || Date.now() - npmCache.at > 36e5) {
3801
4454
  npmCache = { at: Date.now(), value: await npmDownloads(options.fetchImpl ?? fetch) };
@@ -3826,7 +4479,7 @@ function passwordMatches(request, password3) {
3826
4479
  if (!header.startsWith("Basic ")) return false;
3827
4480
  const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
3828
4481
  const given = decoded.slice(decoded.indexOf(":") + 1);
3829
- const digest = (value) => createHash("sha256").update(value).digest();
4482
+ const digest = (value) => createHash2("sha256").update(value).digest();
3830
4483
  return timingSafeEqual(digest(given), digest(password3));
3831
4484
  }
3832
4485
  function readBody(request) {
@@ -3850,10 +4503,10 @@ function listen2(server, port) {
3850
4503
  }
3851
4504
  async function readEvents(dataDir, days, now = Date.now()) {
3852
4505
  const since = new Date(now - (days - 1) * 864e5).toISOString().slice(0, 10);
3853
- const files = (await readdir(dataDir).catch(() => [])).filter((file) => /^events-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && file.slice(7, 17) >= since).sort();
4506
+ const files = (await readdir2(dataDir).catch(() => [])).filter((file) => /^events-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && file.slice(7, 17) >= since).sort();
3854
4507
  const events = [];
3855
4508
  for (const file of files) {
3856
- for (const line of (await readFile4(join6(dataDir, file), "utf8")).split("\n")) {
4509
+ for (const line of (await readFile5(join7(dataDir, file), "utf8")).split("\n")) {
3857
4510
  if (!line) continue;
3858
4511
  try {
3859
4512
  events.push(JSON.parse(line));
@@ -3863,64 +4516,125 @@ async function readEvents(dataDir, days, now = Date.now()) {
3863
4516
  }
3864
4517
  return events;
3865
4518
  }
3866
- function summarize(events, days, now = Date.now()) {
3867
- const day = (iso) => iso.slice(0, 10);
3868
- const today = day(new Date(now).toISOString());
3869
- const activeWithin = (window) => {
3870
- const since = day(new Date(now - (window - 1) * 864e5).toISOString());
3871
- return new Set(events.filter((e) => day(e.receivedAt) >= since).map((e) => e.installId)).size;
4519
+ var DAY_MS = 864e5;
4520
+ var dayOf = (iso) => iso.slice(0, 10);
4521
+ var dayNumber = (iso) => Math.floor(Date.parse(iso) / DAY_MS);
4522
+ var COHORT_WEEKS = 8;
4523
+ function featureOf(event) {
4524
+ if (event.kind === "tool") return event.name;
4525
+ if (event.kind === "cli") return `cli:${event.name}`;
4526
+ return event.kind;
4527
+ }
4528
+ var GOT_CONTEXT = (e) => e.kind === "recall" || e.kind === "tool" && /^(context_(pack|bootstrap|before_edit)|recall)$/.test(e.name);
4529
+ var SAVED_MEMORY = (e) => e.kind === "capture" || e.kind === "tool" && /^(context_after_turn|remember)$/.test(e.name);
4530
+ function summarize(events, days, now = Date.now(), registry = /* @__PURE__ */ new Map()) {
4531
+ const today = Math.floor(now / DAY_MS);
4532
+ const rangeStart = today - days + 1;
4533
+ const firstSeenOf = (install, fallback) => registry.get(install) ?? fallback;
4534
+ const activeDays = /* @__PURE__ */ new Map();
4535
+ for (const event of events) {
4536
+ const set = activeDays.get(event.installId) ?? /* @__PURE__ */ new Set();
4537
+ set.add(dayNumber(event.receivedAt));
4538
+ activeDays.set(event.installId, set);
4539
+ }
4540
+ const activeBetween = (from, to) => {
4541
+ let count = 0;
4542
+ for (const set of activeDays.values()) {
4543
+ for (const d of set) {
4544
+ if (d >= from && d <= to) {
4545
+ count += 1;
4546
+ break;
4547
+ }
4548
+ }
4549
+ }
4550
+ return count;
3872
4551
  };
4552
+ const inRange = events.filter((e) => dayNumber(e.receivedAt) >= rangeStart);
3873
4553
  const daily = /* @__PURE__ */ new Map();
3874
- for (let index = days - 1; index >= 0; index -= 1) {
3875
- daily.set(day(new Date(now - index * 864e5).toISOString()), {
3876
- installs: /* @__PURE__ */ new Set(),
3877
- events: 0
3878
- });
3879
- }
3880
- const firstSeen = /* @__PURE__ */ new Map();
3881
- const tools = /* @__PURE__ */ new Map();
3882
- const kinds = /* @__PURE__ */ new Map();
4554
+ for (let d = rangeStart; d <= today; d += 1) daily.set(d, { installs: /* @__PURE__ */ new Set(), events: 0 });
4555
+ const features = /* @__PURE__ */ new Map();
3883
4556
  const latest = /* @__PURE__ */ new Map();
3884
4557
  const users = /* @__PURE__ */ new Map();
4558
+ const funnel = {
4559
+ active: /* @__PURE__ */ new Set(),
4560
+ context: /* @__PURE__ */ new Set(),
4561
+ memory: /* @__PURE__ */ new Set()
4562
+ };
3885
4563
  let errors = 0;
3886
4564
  let tokensOut = 0;
3887
- for (const event of events) {
3888
- const bucket = daily.get(day(event.receivedAt));
4565
+ for (const event of inRange) {
4566
+ const d = dayNumber(event.receivedAt);
4567
+ const bucket = daily.get(d);
3889
4568
  if (bucket) {
3890
4569
  bucket.installs.add(event.installId);
3891
4570
  bucket.events += 1;
3892
4571
  }
3893
- const seen = firstSeen.get(event.installId);
3894
- if (!seen || event.receivedAt < seen) firstSeen.set(event.installId, event.receivedAt);
3895
4572
  latest.set(event.installId, event);
4573
+ const feature = featureOf(event);
3896
4574
  const user = users.get(event.installId) ?? {
4575
+ firstEvent: event.receivedAt,
3897
4576
  lastSeen: event.receivedAt,
3898
4577
  events: 0,
4578
+ errors: 0,
3899
4579
  sessions: /* @__PURE__ */ new Set(),
3900
4580
  projects: /* @__PURE__ */ new Set(),
3901
- features: /* @__PURE__ */ new Map()
4581
+ features: /* @__PURE__ */ new Map(),
4582
+ days: /* @__PURE__ */ new Set()
3902
4583
  };
3903
4584
  if (event.receivedAt > user.lastSeen) user.lastSeen = event.receivedAt;
3904
4585
  user.events += 1;
4586
+ if (!event.ok) user.errors += 1;
3905
4587
  if (event.session) user.sessions.add(event.session);
3906
4588
  user.projects.add(event.project);
3907
- const feature = event.kind === "tool" ? event.name : event.kind;
3908
4589
  user.features.set(feature, (user.features.get(feature) ?? 0) + 1);
4590
+ user.days.add(d);
3909
4591
  users.set(event.installId, user);
3910
- kinds.set(event.kind, (kinds.get(event.kind) ?? 0) + 1);
4592
+ funnel.active.add(event.installId);
4593
+ if (GOT_CONTEXT(event)) funnel.context.add(event.installId);
4594
+ if (SAVED_MEMORY(event)) funnel.memory.add(event.installId);
3911
4595
  if (!event.ok) errors += 1;
3912
4596
  tokensOut += event.tokensOut ?? 0;
3913
- const key = event.kind === "tool" ? event.name : `${event.kind}`;
3914
- const tool = tools.get(key) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
3915
- tool.calls += 1;
3916
- if (!event.ok) tool.errors += 1;
3917
- tool.installs.add(event.installId);
3918
- tool.ms.push(event.ms);
3919
- tools.set(key, tool);
4597
+ const stats = features.get(feature) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
4598
+ stats.calls += 1;
4599
+ if (!event.ok) stats.errors += 1;
4600
+ stats.installs.add(event.installId);
4601
+ stats.ms.push(event.ms);
4602
+ features.set(feature, stats);
4603
+ }
4604
+ const firstSeenDay = /* @__PURE__ */ new Map();
4605
+ for (const [install, seen] of registry) firstSeenDay.set(install, dayNumber(seen));
4606
+ for (const [install, user] of users) {
4607
+ if (!firstSeenDay.has(install)) firstSeenDay.set(install, dayNumber(user.firstEvent));
4608
+ }
4609
+ for (const [install, set] of activeDays) {
4610
+ if (!firstSeenDay.has(install)) firstSeenDay.set(install, Math.min(...set));
3920
4611
  }
3921
4612
  const newPerDay = /* @__PURE__ */ new Map();
3922
- for (const seen of firstSeen.values())
3923
- newPerDay.set(day(seen), (newPerDay.get(day(seen)) ?? 0) + 1);
4613
+ for (const d of firstSeenDay.values()) newPerDay.set(d, (newPerDay.get(d) ?? 0) + 1);
4614
+ let cumulative = [...firstSeenDay.values()].filter((d) => d < rangeStart).length;
4615
+ const cohorts = [];
4616
+ for (let age = COHORT_WEEKS - 1; age >= 0; age -= 1) {
4617
+ const from = today - (age + 1) * 7 + 1;
4618
+ const members = [...firstSeenDay].filter(([, d]) => d >= from && d < from + 7).map(([install]) => install);
4619
+ const weeks = [];
4620
+ for (let week = 0; week <= age; week += 1) {
4621
+ const wFrom = from + week * 7;
4622
+ const active = members.filter((install) => {
4623
+ const set = activeDays.get(install);
4624
+ if (!set) return week === 0;
4625
+ for (const d of set) if (d >= wFrom && d < wFrom + 7) return true;
4626
+ return false;
4627
+ }).length;
4628
+ weeks.push(members.length ? active / members.length : 0);
4629
+ }
4630
+ cohorts.push({
4631
+ start: new Date(from * DAY_MS).toISOString().slice(0, 10),
4632
+ size: members.length,
4633
+ weeks
4634
+ });
4635
+ }
4636
+ const week1 = cohorts.filter((c) => c.size > 0 && c.weeks.length > 1);
4637
+ const week1Retention = week1.length ? week1.reduce((sum, c) => sum + (c.weeks[1] ?? 0) * c.size, 0) / week1.reduce((sum, c) => sum + c.size, 0) : null;
3924
4638
  const share = (pick) => {
3925
4639
  const counts = /* @__PURE__ */ new Map();
3926
4640
  for (const event of latest.values()) {
@@ -3932,47 +4646,72 @@ function summarize(events, days, now = Date.now()) {
3932
4646
  const sorted = [...values].sort((a, b) => a - b);
3933
4647
  return sorted[Math.floor(sorted.length / 2)] ?? 0;
3934
4648
  };
4649
+ const p95 = (values) => {
4650
+ const sorted = [...values].sort((a, b) => a - b);
4651
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
4652
+ };
4653
+ const wau = activeBetween(today - 6, today);
4654
+ const wauPrevious = activeBetween(today - 13, today - 7);
4655
+ const returned = [...users.values()].filter((user) => user.days.size >= 2).length;
3935
4656
  return {
3936
4657
  generatedAt: new Date(now).toISOString(),
3937
4658
  days,
3938
4659
  totals: {
3939
- installs: firstSeen.size,
3940
- projects: new Set(events.map((e) => `${e.installId}:${e.project}`)).size,
3941
- sessions: new Set(events.filter((e) => e.session).map((e) => e.session)).size,
3942
- events: events.length,
3943
- errorRate: events.length ? errors / events.length : 0,
4660
+ allTimeInstalls: firstSeenDay.size,
4661
+ installs: users.size,
4662
+ newInRange: [...firstSeenDay.values()].filter((d) => d >= rangeStart).length,
4663
+ newToday: newPerDay.get(today) ?? 0,
4664
+ projects: new Set(inRange.map((e) => `${e.installId}:${e.project}`)).size,
4665
+ sessions: new Set(inRange.filter((e) => e.session).map((e) => e.session)).size,
4666
+ events: inRange.length,
4667
+ errorRate: inRange.length ? errors / inRange.length : 0,
3944
4668
  tokensOut,
3945
- active1d: activeWithin(1),
3946
- active7d: activeWithin(7),
3947
- active30d: activeWithin(30),
3948
- newToday: newPerDay.get(today) ?? 0
4669
+ dau: activeBetween(today, today),
4670
+ wau,
4671
+ wauPrevious,
4672
+ mau: activeBetween(today - 29, today),
4673
+ week1Retention
3949
4674
  },
3950
- daily: [...daily].map(([date, bucket]) => ({
3951
- date,
3952
- installs: bucket.installs.size,
3953
- events: bucket.events,
3954
- newInstalls: newPerDay.get(date) ?? 0
3955
- })),
3956
- features: [...tools].map(([label, tool]) => ({
4675
+ daily: [...daily].map(([d, bucket]) => {
4676
+ cumulative += newPerDay.get(d) ?? 0;
4677
+ return {
4678
+ date: new Date(d * DAY_MS).toISOString().slice(0, 10),
4679
+ installs: bucket.installs.size,
4680
+ events: bucket.events,
4681
+ newInstalls: newPerDay.get(d) ?? 0,
4682
+ totalInstalls: cumulative
4683
+ };
4684
+ }),
4685
+ /** Installs active in the range that reached each step. */
4686
+ funnel: [
4687
+ { label: "Active", installs: funnel.active.size },
4688
+ { label: "Received context", installs: funnel.context.size },
4689
+ { label: "Saved memory", installs: funnel.memory.size },
4690
+ { label: "Came back another day", installs: returned }
4691
+ ],
4692
+ cohorts,
4693
+ features: [...features].map(([label, stats]) => ({
3957
4694
  label,
3958
- calls: tool.calls,
3959
- installs: tool.installs.size,
3960
- errorRate: tool.errors / tool.calls,
3961
- p50ms: median(tool.ms)
3962
- })).sort((a, b) => b.calls - a.calls).slice(0, 20),
3963
- kinds: [...kinds].map(([label, count]) => ({ label, count })).sort((a, b) => b.count - a.count),
4695
+ calls: stats.calls,
4696
+ installs: stats.installs.size,
4697
+ errorRate: stats.errors / stats.calls,
4698
+ p50ms: median(stats.ms),
4699
+ p95ms: p95(stats.ms)
4700
+ })).sort((a, b) => b.calls - a.calls).slice(0, 25),
3964
4701
  clients: share((e) => e.clients.length ? e.clients : ["unknown"]),
3965
4702
  versions: share((e) => [e.version]),
3966
4703
  platforms: share((e) => [`${e.platform}/${e.arch}`]),
3967
4704
  nodes: share((e) => [`node ${e.node}`]),
3968
- /** One row per install, most recently active first. */
4705
+ /** One row per install active in the range, most recently active first. */
3969
4706
  users: [...users].map(([installId, user]) => {
3970
4707
  const last = latest.get(installId);
3971
4708
  return {
3972
4709
  install: installId.slice(0, 8),
3973
- firstSeen: firstSeen.get(installId) ?? user.lastSeen,
4710
+ firstSeen: firstSeenOf(installId, user.firstEvent),
3974
4711
  lastSeen: user.lastSeen,
4712
+ activeDays: user.days.size,
3975
4713
  events: user.events,
4714
+ errors: user.errors,
3976
4715
  sessions: user.sessions.size,
3977
4716
  projects: user.projects.size,
3978
4717
  client: last?.clients.join(",") || "unknown",
@@ -3981,7 +4720,7 @@ function summarize(events, days, now = Date.now()) {
3981
4720
  topFeature: [...user.features].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""
3982
4721
  };
3983
4722
  }).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)).slice(0, 1e3),
3984
- recent: events.slice(-50).reverse().map((e) => ({
4723
+ recent: inRange.slice(-50).reverse().map((e) => ({
3985
4724
  receivedAt: e.receivedAt,
3986
4725
  install: e.installId.slice(0, 8),
3987
4726
  kind: e.kind,
@@ -3993,6 +4732,77 @@ function summarize(events, days, now = Date.now()) {
3993
4732
  }))
3994
4733
  };
3995
4734
  }
4735
+ function installTrace(events, prefix) {
4736
+ const own = events.filter((e) => e.installId.startsWith(prefix));
4737
+ const first = own[0];
4738
+ if (!first || !/^[0-9a-f]{8}$/.test(prefix)) return null;
4739
+ const sessions = /* @__PURE__ */ new Map();
4740
+ const perDay = /* @__PURE__ */ new Map();
4741
+ for (const event of own) {
4742
+ perDay.set(dayOf(event.receivedAt), (perDay.get(dayOf(event.receivedAt)) ?? 0) + 1);
4743
+ const key = event.session ?? "none";
4744
+ const session = sessions.get(key) ?? {
4745
+ start: event.ts,
4746
+ end: event.ts,
4747
+ events: 0,
4748
+ errors: 0,
4749
+ ms: 0,
4750
+ features: /* @__PURE__ */ new Set()
4751
+ };
4752
+ if (event.ts < session.start) session.start = event.ts;
4753
+ if (event.ts > session.end) session.end = event.ts;
4754
+ session.events += 1;
4755
+ if (!event.ok) session.errors += 1;
4756
+ session.ms += event.ms;
4757
+ session.features.add(featureOf(event));
4758
+ sessions.set(key, session);
4759
+ }
4760
+ const last = own[own.length - 1] ?? first;
4761
+ return {
4762
+ install: prefix,
4763
+ client: last.clients.join(",") || "unknown",
4764
+ version: last.version,
4765
+ platform: `${last.platform}/${last.arch}`,
4766
+ node: last.node,
4767
+ projects: new Set(own.map((e) => e.project)).size,
4768
+ perDay: [...perDay].map(([date, count]) => ({ date, events: count })),
4769
+ sessions: [...sessions].map(([id, session]) => ({ id, ...session, features: [...session.features] })).sort((a, b) => b.end.localeCompare(a.end)).slice(0, 50),
4770
+ events: own.slice(-1e3).map((e) => ({
4771
+ ts: e.ts,
4772
+ kind: e.kind,
4773
+ name: e.name,
4774
+ feature: featureOf(e),
4775
+ ms: e.ms,
4776
+ ok: e.ok,
4777
+ session: e.session ?? "none",
4778
+ tokensOut: e.tokensOut ?? 0
4779
+ }))
4780
+ };
4781
+ }
4782
+ async function loadRegistry(dataDir) {
4783
+ const registry = /* @__PURE__ */ new Map();
4784
+ const path = join7(dataDir, "installs.jsonl");
4785
+ const raw = await readFile5(path, "utf8").catch(() => null);
4786
+ if (raw !== null) {
4787
+ for (const line of raw.split("\n")) {
4788
+ try {
4789
+ const entry = JSON.parse(line);
4790
+ if (!registry.has(entry.installId)) registry.set(entry.installId, entry.firstSeen);
4791
+ } catch {
4792
+ }
4793
+ }
4794
+ return registry;
4795
+ }
4796
+ for (const event of await readEvents(dataDir, 3650)) {
4797
+ if (!registry.has(event.installId)) registry.set(event.installId, event.receivedAt);
4798
+ }
4799
+ const lines = [...registry].map(
4800
+ ([installId, firstSeen]) => JSON.stringify({ installId, firstSeen })
4801
+ );
4802
+ await appendFile(path, lines.length ? `${lines.join("\n")}
4803
+ ` : "");
4804
+ return registry;
4805
+ }
3996
4806
  async function npmDownloads(fetchImpl) {
3997
4807
  const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3998
4808
  const from = new Date(Date.now() - 364 * 864e5).toISOString().slice(0, 10);
@@ -4017,10 +4827,10 @@ function registerTelemetryCommands(program, runSafely2) {
4017
4827
  const state = await readTelemetryState();
4018
4828
  const blocked = telemetryBlocked(state);
4019
4829
  console.log(
4020
- blocked ? `${pc19.yellow("\u25CB")} telemetry off (${blocked})` : `${pc19.green("\u25CF")} telemetry on`
4830
+ blocked ? `${pc21.yellow("\u25CB")} telemetry off (${blocked})` : `${pc21.green("\u25CF")} telemetry on`
4021
4831
  );
4022
- console.log(pc19.dim(` install id ${state.installId} (${telemetryStatePath()})`));
4023
- if (telemetryUrl()) console.log(pc19.dim(` collector ${telemetryUrl()}`));
4832
+ console.log(pc21.dim(` install id ${state.installId} (${telemetryStatePath()})`));
4833
+ if (telemetryUrl()) console.log(pc21.dim(` collector ${telemetryUrl()}`));
4024
4834
  });
4025
4835
  });
4026
4836
  telemetry.command("enable").description("Allow anonymous usage data from this machine.").action(async () => {
@@ -4031,7 +4841,7 @@ function registerTelemetryCommands(program, runSafely2) {
4031
4841
  noticeAt: state.noticeAt ?? (/* @__PURE__ */ new Date()).toISOString()
4032
4842
  });
4033
4843
  console.log(TELEMETRY_NOTICE);
4034
- console.log(pc19.green("Telemetry enabled."));
4844
+ console.log(pc21.green("Telemetry enabled."));
4035
4845
  });
4036
4846
  });
4037
4847
  telemetry.command("disable").description("Stop sending anonymous usage data from this machine.").action(async () => {
@@ -4059,14 +4869,14 @@ function registerTelemetryCommands(program, runSafely2) {
4059
4869
  adminPort: opts.adminPort,
4060
4870
  ...process.env.CODEBUDDY_ADMIN_PASSWORD ? { adminPassword: process.env.CODEBUDDY_ADMIN_PASSWORD } : {}
4061
4871
  });
4062
- console.log(`${pc19.green("\u25CF")} ingest ${pc19.bold(collector.ingestUrl)}`);
4063
- console.log(`${pc19.green("\u25CF")} admin ${pc19.bold(collector.adminUrl)}`);
4872
+ console.log(`${pc21.green("\u25CF")} ingest ${pc21.bold(collector.ingestUrl)}`);
4873
+ console.log(`${pc21.green("\u25CF")} admin ${pc21.bold(collector.adminUrl)}`);
4064
4874
  console.log(
4065
- pc19.dim(
4875
+ pc21.dim(
4066
4876
  process.env.CODEBUDDY_ADMIN_PASSWORD ? " admin panel is password-protected (CODEBUDDY_ADMIN_PASSWORD)." : " admin panel has no password: keep it private, or set CODEBUDDY_ADMIN_PASSWORD."
4067
4877
  )
4068
4878
  );
4069
- console.log(pc19.dim(` events in ${opts.data}. Ctrl+C to stop.`));
4879
+ console.log(pc21.dim(` events in ${opts.data}. Ctrl+C to stop.`));
4070
4880
  await new Promise((resolve6) => {
4071
4881
  const stop = () => void collector.close().then(resolve6);
4072
4882
  process.once("SIGINT", stop);
@@ -4079,12 +4889,12 @@ function registerTelemetryCommands(program, runSafely2) {
4079
4889
  // src/cli/commands/use-command.ts
4080
4890
  import { basename as basename2 } from "path";
4081
4891
  import { confirm as confirm2, isCancel as isCancel2, log as log2, note as note2, password as password2, select, spinner as spinner3, text as text2 } from "@clack/prompts";
4082
- import pc20 from "picocolors";
4892
+ import pc22 from "picocolors";
4083
4893
 
4084
4894
  // src/core/global-config.ts
4085
- import { chmod, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
4895
+ import { chmod, mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
4086
4896
  import { homedir } from "os";
4087
- import { dirname as dirname3, join as join7 } from "path";
4897
+ import { dirname as dirname3, join as join8 } from "path";
4088
4898
  import { z as z3 } from "zod";
4089
4899
  var GLOBAL_DIR = ".codebuddy";
4090
4900
  var GLOBAL_FILE = "global.json";
@@ -4097,12 +4907,12 @@ var globalConfigSchema = z3.object({
4097
4907
  installPath: z3.string().optional()
4098
4908
  });
4099
4909
  function globalConfigPath(home = homedir()) {
4100
- return join7(home, GLOBAL_DIR, GLOBAL_FILE);
4910
+ return join8(home, GLOBAL_DIR, GLOBAL_FILE);
4101
4911
  }
4102
4912
  async function readGlobalConfig(home = homedir()) {
4103
4913
  const path = globalConfigPath(home);
4104
4914
  try {
4105
- const raw = await readFile5(path, "utf8");
4915
+ const raw = await readFile6(path, "utf8");
4106
4916
  return globalConfigSchema.parse(JSON.parse(raw));
4107
4917
  } catch (error) {
4108
4918
  if (error.code === "ENOENT") return {};
@@ -4111,7 +4921,7 @@ async function readGlobalConfig(home = homedir()) {
4111
4921
  }
4112
4922
  async function writeGlobalConfig(next, home = homedir()) {
4113
4923
  const path = globalConfigPath(home);
4114
- await mkdir3(dirname3(path), { recursive: true, mode: 448 });
4924
+ await mkdir4(dirname3(path), { recursive: true, mode: 448 });
4115
4925
  await writeFile3(path, `${JSON.stringify(next, null, 2)}
4116
4926
  `, { mode: 384 });
4117
4927
  await chmod(path, 384);
@@ -4163,15 +4973,15 @@ async function runUseCommand(options = {}) {
4163
4973
  })
4164
4974
  ));
4165
4975
  note2(
4166
- `Project folder: ${pc20.bold(process.cwd())}
4167
- Namespace: ${pc20.bold(namespace)}`,
4976
+ `Project folder: ${pc22.bold(process.cwd())}
4977
+ Namespace: ${pc22.bold(namespace)}`,
4168
4978
  "codebuddy use"
4169
4979
  );
4170
4980
  const stored = await readGlobalConfig();
4171
4981
  const projectConfig = await readConfigFile();
4172
4982
  let embeddings = projectConfig.provider?.type;
4173
4983
  if (embeddings) {
4174
- log2.info(`Embeddings: ${embeddings} (change with ${pc20.cyan("codebuddy models use")}).`);
4984
+ log2.info(`Embeddings: ${embeddings} (change with ${pc22.cyan("codebuddy models use")}).`);
4175
4985
  } else if (!interactive) {
4176
4986
  embeddings = "local";
4177
4987
  } else {
@@ -4212,7 +5022,7 @@ Namespace: ${pc20.bold(namespace)}`,
4212
5022
  log2.info("Using HF_TOKEN from environment.");
4213
5023
  } else if (stored.hfToken) {
4214
5024
  hfToken = stored.hfToken;
4215
- log2.info(`Reusing saved Hugging Face token (${pc20.dim("~/.codebuddy/global.json")}).`);
5025
+ log2.info(`Reusing saved Hugging Face token (${pc22.dim("~/.codebuddy/global.json")}).`);
4216
5026
  } else if (!interactive) {
4217
5027
  throw new Error(
4218
5028
  "Hugging Face embeddings need a token: set HF_TOKEN (no terminal to prompt for it)."
@@ -4346,7 +5156,7 @@ Namespace: ${pc20.bold(namespace)}`,
4346
5156
  databaseUrl
4347
5157
  });
4348
5158
  log2.success(
4349
- `Registered ${pc20.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
5159
+ `Registered ${pc22.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
4350
5160
  );
4351
5161
  } catch (error) {
4352
5162
  log2.warn(`Claude Desktop registration skipped: ${error.message}`);
@@ -4361,7 +5171,7 @@ Namespace: ${pc20.bold(namespace)}`,
4361
5171
  databaseUrl
4362
5172
  });
4363
5173
  log2.success(
4364
- `Registered ${pc20.bold(install.serverKey)} in Codex. Restart Codex to activate.`
5174
+ `Registered ${pc22.bold(install.serverKey)} in Codex. Restart Codex to activate.`
4365
5175
  );
4366
5176
  } catch (error) {
4367
5177
  log2.warn(`Codex registration skipped: ${error.message}`);
@@ -4369,24 +5179,24 @@ Namespace: ${pc20.bold(namespace)}`,
4369
5179
  }
4370
5180
  note2(
4371
5181
  [
4372
- `${pc20.green("\u2713")} CodeBuddy is ready for ${pc20.bold(folderName)}.`,
4373
- `${pc20.green("\u2713")} Scaffolded ${pc20.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
4374
- `${pc20.green("\u2713")} ${graphifyMessage}`,
4375
- `${pc20.green("\u2713")} Added agent workflow instructions to ${pc20.cyan("CLAUDE.md")} and ${pc20.cyan("AGENTS.md")}.`,
5182
+ `${pc22.green("\u2713")} CodeBuddy is ready for ${pc22.bold(folderName)}.`,
5183
+ `${pc22.green("\u2713")} Scaffolded ${pc22.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
5184
+ `${pc22.green("\u2713")} ${graphifyMessage}`,
5185
+ `${pc22.green("\u2713")} Added agent workflow instructions to ${pc22.cyan("CLAUDE.md")} and ${pc22.cyan("AGENTS.md")}.`,
4376
5186
  ``,
4377
5187
  `Next:`,
4378
5188
  ` 1. Restart Claude Desktop and Codex.`,
4379
- ` 2. Ask Claude or Codex to use the ${pc20.cyan("context_pack")} tool for each coding task.`,
5189
+ ` 2. Ask Claude or Codex to use the ${pc22.cyan("context_pack")} tool for each coding task.`,
4380
5190
  ` 3. Indexing, Graphify, Markdown memory import, and embeddings now update automatically while the MCP server is running.`,
4381
- ` 4. Run ${pc20.cyan("codebuddy doctor")} or ${pc20.cyan("codebuddy db doctor")} if you want a full health check.`,
5191
+ ` 4. Run ${pc22.cyan("codebuddy doctor")} or ${pc22.cyan("codebuddy db doctor")} if you want a full health check.`,
4382
5192
  ``,
4383
5193
  `Manage:`,
4384
- ` codebuddy claude list ${pc20.dim("# see every project wired up")}`,
4385
- ` codebuddy codex list ${pc20.dim("# see Codex entries")}`,
4386
- ` codebuddy db test ${pc20.dim("# quick Postgres connectivity check")}`,
5194
+ ` codebuddy claude list ${pc22.dim("# see every project wired up")}`,
5195
+ ` codebuddy codex list ${pc22.dim("# see Codex entries")}`,
5196
+ ` codebuddy db test ${pc22.dim("# quick Postgres connectivity check")}`,
4387
5197
  ` codebuddy claude remove codebuddy-${namespace}`,
4388
5198
  ` codebuddy codex remove codebuddy-${namespace}`,
4389
- ` codebuddy postgres down ${pc20.dim("# stop the DB (data preserved)")}`
5199
+ ` codebuddy postgres down ${pc22.dim("# stop the DB (data preserved)")}`
4390
5200
  ].join("\n"),
4391
5201
  "Done"
4392
5202
  );
@@ -4397,7 +5207,7 @@ Namespace: ${pc20.bold(namespace)}`,
4397
5207
 
4398
5208
  // src/cli/commands/worklog.ts
4399
5209
  import { relative as relative6 } from "path";
4400
- import pc21 from "picocolors";
5210
+ import pc23 from "picocolors";
4401
5211
  function registerWorklogCommands(program, runSafely2) {
4402
5212
  const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
4403
5213
  worklog.command("list", { isDefault: true }).option("--limit <n>", "Maximum entries to show.", "20").option("--json", "Print machine-readable JSON.").description("List worklog entries, newest first.").action(async (opts) => {
@@ -4408,7 +5218,7 @@ function registerWorklogCommands(program, runSafely2) {
4408
5218
  return;
4409
5219
  }
4410
5220
  if (entries.length === 0) {
4411
- console.log(pc21.dim("no worklog entries yet"));
5221
+ console.log(pc23.dim("no worklog entries yet"));
4412
5222
  return;
4413
5223
  }
4414
5224
  for (const entry of entries) console.log(renderRow2(entry));
@@ -4422,36 +5232,36 @@ function registerWorklogCommands(program, runSafely2) {
4422
5232
  worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
4423
5233
  await runSafely2(async () => {
4424
5234
  const [entry] = await new WorklogStore().list();
4425
- console.log(entry ? renderEntry(entry) : pc21.dim("no worklog entries yet"));
5235
+ console.log(entry ? renderEntry(entry) : pc23.dim("no worklog entries yet"));
4426
5236
  });
4427
5237
  });
4428
5238
  }
4429
5239
  function renderRow2(entry) {
4430
5240
  const summary = entry.summary.replace(/\s+/g, " ");
4431
5241
  const short2 = summary.length <= 70 ? summary : `${summary.slice(0, 69)}\u2026`;
4432
- return `${pc21.dim(entry.createdAt.slice(0, 16))} ${pc21.dim(entry.id)} ${short2} ${pc21.dim(`(${entry.files.length} file(s))`)}`;
5242
+ return `${pc23.dim(entry.createdAt.slice(0, 16))} ${pc23.dim(entry.id)} ${short2} ${pc23.dim(`(${entry.files.length} file(s))`)}`;
4433
5243
  }
4434
5244
  function renderEntry(entry) {
4435
- const lines = [`${pc21.bold(entry.id)} ${pc21.dim(entry.createdAt)} ${pc21.dim(entry.namespace)}`];
4436
- if (entry.sourceSessionId) lines.push(`${pc21.bold("session:")} ${entry.sourceSessionId}`);
4437
- if (entry.sourcePlanId) lines.push(`${pc21.bold("plan:")} ${entry.sourcePlanId}`);
4438
- if (entry.commitSha) lines.push(`${pc21.bold("commit:")} ${entry.commitSha}`);
4439
- if (entry.files.length > 0) lines.push(`${pc21.bold("files:")} ${entry.files.join(", ")}`);
5245
+ const lines = [`${pc23.bold(entry.id)} ${pc23.dim(entry.createdAt)} ${pc23.dim(entry.namespace)}`];
5246
+ if (entry.sourceSessionId) lines.push(`${pc23.bold("session:")} ${entry.sourceSessionId}`);
5247
+ if (entry.sourcePlanId) lines.push(`${pc23.bold("plan:")} ${entry.sourcePlanId}`);
5248
+ if (entry.commitSha) lines.push(`${pc23.bold("commit:")} ${entry.commitSha}`);
5249
+ if (entry.files.length > 0) lines.push(`${pc23.bold("files:")} ${entry.files.join(", ")}`);
4440
5250
  if (entry.verification.length > 0) {
4441
- lines.push(`${pc21.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
5251
+ lines.push(`${pc23.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
4442
5252
  }
4443
5253
  if (entry.savedMemory.length > 0) {
4444
- lines.push(`${pc21.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
5254
+ lines.push(`${pc23.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
4445
5255
  }
4446
5256
  if (entry.queuedForReview.length > 0) {
4447
- lines.push(`${pc21.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
5257
+ lines.push(`${pc23.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
4448
5258
  }
4449
- lines.push("", entry.summary, "", pc21.dim(relative6(process.cwd(), entry.path)));
5259
+ lines.push("", entry.summary, "", pc23.dim(relative6(process.cwd(), entry.path)));
4450
5260
  return lines.join("\n");
4451
5261
  }
4452
5262
 
4453
5263
  // src/cli/commands/zvec.ts
4454
- import pc22 from "picocolors";
5264
+ import pc24 from "picocolors";
4455
5265
  function registerZvecCommands(program, runSafely2) {
4456
5266
  const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
4457
5267
  zvec.command("status", { isDefault: true }).option("--json", "Print machine-readable JSON.").description("Show whether zvec-grep is enabled, installed, supported and indexed.").action(async (opts) => {
@@ -4461,22 +5271,22 @@ function registerZvecCommands(program, runSafely2) {
4461
5271
  console.log(JSON.stringify(report, null, 2));
4462
5272
  return;
4463
5273
  }
4464
- const mark = (ok2) => ok2 ? pc22.green("yes") : pc22.yellow("no");
5274
+ const mark = (ok2) => ok2 ? pc24.green("yes") : pc24.yellow("no");
4465
5275
  console.log(`enabled: ${mark(report.enabled)}`);
4466
5276
  console.log(`binary: ${report.binary}`);
4467
5277
  console.log(
4468
- `version: ${report.version ?? pc22.yellow("not found")}${report.version && !report.supported ? pc22.yellow(" (unsupported; need 0.2.x)") : ""}`
5278
+ `version: ${report.version ?? pc24.yellow("not found")}${report.version && !report.supported ? pc24.yellow(" (unsupported; need 0.2.x)") : ""}`
4469
5279
  );
4470
5280
  console.log(`index ready: ${mark(report.indexReady)}`);
4471
5281
  console.log(
4472
- `zg server: ${report.server} ${pc22.dim("(running = faster queries; optional)")}`
5282
+ `zg server: ${report.server} ${pc24.dim("(running = faster queries; optional)")}`
4473
5283
  );
4474
5284
  console.log(`status: ${report.reason}`);
4475
- if (!report.version) console.log(pc22.dim("install: npm install -g @zvec/zvec-grep"));
5285
+ if (!report.version) console.log(pc24.dim("install: npm install -g @zvec/zvec-grep"));
4476
5286
  else if (report.supported && !report.indexReady)
4477
- console.log(pc22.dim("next: codebuddy zvec index"));
5287
+ console.log(pc24.dim("next: codebuddy zvec index"));
4478
5288
  else if (report.indexReady && !report.enabled)
4479
- console.log(pc22.dim("next: codebuddy zvec enable"));
5289
+ console.log(pc24.dim("next: codebuddy zvec enable"));
4480
5290
  });
4481
5291
  });
4482
5292
  zvec.command("index").option("--model <model>", "Embedding model for a new index.", DEFAULT_ZVEC_MODEL).option("--rebuild", "Rebuild the index from scratch (e.g. after changing model).").description("Build or refresh the zg index for this repository (explicit; never automatic).").action(async (opts) => {
@@ -4486,21 +5296,21 @@ function registerZvecCommands(program, runSafely2) {
4486
5296
  ...opts.rebuild ? { rebuild: true } : {}
4487
5297
  });
4488
5298
  if (code !== 0) throw new Error(`zg index exited with code ${code}`);
4489
- console.log(pc22.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
5299
+ console.log(pc24.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
4490
5300
  });
4491
5301
  });
4492
5302
  zvec.command("enable").description("Use zvec-grep candidates in context_pack / context_before_edit.").action(async () => {
4493
5303
  await runSafely2(async () => {
4494
5304
  await setZvecEnabled(true);
4495
5305
  console.log(
4496
- `${pc22.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
5306
+ `${pc24.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
4497
5307
  );
4498
5308
  });
4499
5309
  });
4500
5310
  zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
4501
5311
  await runSafely2(async () => {
4502
5312
  await setZvecEnabled(false);
4503
- console.log(`${pc22.yellow("disabled")} zvec-grep retrieval`);
5313
+ console.log(`${pc24.yellow("disabled")} zvec-grep retrieval`);
4504
5314
  });
4505
5315
  });
4506
5316
  }
@@ -4773,6 +5583,36 @@ function createCli() {
4773
5583
  console.log(JSON.stringify(await runtime.repository.getStats(), null, 2));
4774
5584
  });
4775
5585
  });
5586
+ program.command("storage").option("--json", "Print machine-readable JSON.").description(
5587
+ "Memory storage health: Markdown/database drift, backup coverage, embedding backlog and spaces, capture lag."
5588
+ ).action(async (opts) => {
5589
+ await runSafely(async () => {
5590
+ const root = process.cwd();
5591
+ const capture2 = {
5592
+ stagedNotCaptured: await new HookStagingStore(root).pending(),
5593
+ journalBehind: (await journalLag(root)).filter((entry) => entry.behindBytes !== 0)
5594
+ };
5595
+ let database;
5596
+ try {
5597
+ const runtime = await createRuntime(await loadRuntimeConfig());
5598
+ try {
5599
+ database = await runtime.memory.storageReport();
5600
+ } finally {
5601
+ await runtime.close();
5602
+ }
5603
+ } catch (error) {
5604
+ database = { unavailable: error.message };
5605
+ }
5606
+ if (opts.json) {
5607
+ console.log(JSON.stringify({ database, capture: capture2 }, null, 2));
5608
+ return;
5609
+ }
5610
+ printStorage(database, capture2);
5611
+ if ("unavailable" in database || database.markdownOnly.length + database.dbOnly.length > 0) {
5612
+ process.exitCode = 1;
5613
+ }
5614
+ });
5615
+ });
4776
5616
  program.command("doctor").description("Check CodeBuddy environment health.").option("--skip-model-check", "Do not call Hugging Face models.").action(async (opts) => {
4777
5617
  await runSafely(async () => {
4778
5618
  const report = await runDoctor({ skipModelCheck: opts.skipModelCheck ?? true });
@@ -4799,6 +5639,8 @@ function createCli() {
4799
5639
  registerLearnCommand(program, runSafely);
4800
5640
  registerMemoryCommands(program, runSafely);
4801
5641
  registerWorklogCommands(program, runSafely);
5642
+ registerJournalCommands(program, runSafely);
5643
+ registerOutputCommands(program, runSafely);
4802
5644
  registerZvecCommands(program, runSafely);
4803
5645
  registerLlmCommands(program, runSafely);
4804
5646
  registerSuggestCommand(program, runSafely);
@@ -4810,7 +5652,12 @@ function createCli() {
4810
5652
  registerGitHubCommands(program, runSafely);
4811
5653
  registerSymbolsCommand(program, runSafely);
4812
5654
  registerTelemetryCommands(program, runSafely);
4813
- program.hook("preAction", () => showTelemetryNoticeOnce());
5655
+ let started = Date.now();
5656
+ program.hook("preAction", () => {
5657
+ started = Date.now();
5658
+ return showTelemetryNoticeOnce();
5659
+ });
5660
+ program.hook("postAction", (_program, command) => traceCliCommand(command, started));
4814
5661
  program.action(async () => {
4815
5662
  await runSafely(() => runUseCommand());
4816
5663
  });
@@ -4821,7 +5668,20 @@ var HELP_GROUPS = [
4821
5668
  ["Get started:", ["use", "init", "doctor", "hooks", "clients", "models", "dashboard"]],
4822
5669
  [
4823
5670
  "Daily work:",
4824
- ["context", "plan", "change", "risk", "suggest", "memory", "worklog", "map", "symbols"]
5671
+ [
5672
+ "context",
5673
+ "plan",
5674
+ "change",
5675
+ "risk",
5676
+ "suggest",
5677
+ "memory",
5678
+ "worklog",
5679
+ "journal",
5680
+ "run",
5681
+ "output",
5682
+ "map",
5683
+ "symbols"
5684
+ ]
4825
5685
  ],
4826
5686
  ["Indexing and search:", ["index", "watch", "reindex", "zvec", "graphify", "llm"]],
4827
5687
  [
@@ -4831,6 +5691,7 @@ var HELP_GROUPS = [
4831
5691
  "db",
4832
5692
  "migrate",
4833
5693
  "stats",
5694
+ "storage",
4834
5695
  "inspect",
4835
5696
  "namespaces",
4836
5697
  "list-facts",
@@ -4881,7 +5742,7 @@ async function runSafely(fn) {
4881
5742
  try {
4882
5743
  await fn();
4883
5744
  } catch (error) {
4884
- console.error(pc23.red(String(redactSecrets(error.message))));
5745
+ console.error(pc25.red(String(redactSecrets(error.message))));
4885
5746
  process.exitCode = 1;
4886
5747
  }
4887
5748
  }
@@ -4895,9 +5756,9 @@ function parseDuration(value) {
4895
5756
  return amount * 60 * 1e3;
4896
5757
  }
4897
5758
  function printDoctor(report) {
4898
- const ok2 = pc23.green("ok");
4899
- const bad = pc23.red("fail");
4900
- const warn2 = pc23.yellow("warn");
5759
+ const ok2 = pc25.green("ok");
5760
+ const bad = pc25.red("fail");
5761
+ const warn2 = pc25.yellow("warn");
4901
5762
  console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok2 : bad}`);
4902
5763
  if (report.scaffold.codebuddyDir.warning)
4903
5764
  console.log(` ${report.scaffold.codebuddyDir.warning}`);
@@ -4923,7 +5784,7 @@ function printDoctor(report) {
4923
5784
  );
4924
5785
  console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
4925
5786
  if (report.usage.warning80Percent)
4926
- console.log(pc23.yellow("usage warning: daily cap is over 80% used"));
5787
+ console.log(pc25.yellow("usage warning: daily cap is over 80% used"));
4927
5788
  console.log(
4928
5789
  `config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok2 : warn2}`
4929
5790
  );
@@ -4933,9 +5794,9 @@ function printDoctor(report) {
4933
5794
  }
4934
5795
  }
4935
5796
  function printDbDoctor(report) {
4936
- const ok2 = pc23.green("ok");
4937
- const bad = pc23.red("fail");
4938
- const warn2 = pc23.yellow("warn");
5797
+ const ok2 = pc25.green("ok");
5798
+ const bad = pc25.red("fail");
5799
+ const warn2 = pc25.yellow("warn");
4939
5800
  console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
4940
5801
  if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
4941
5802
  if (report.db.error) console.log(` ${report.db.error}`);
@@ -4948,7 +5809,42 @@ function printDbDoctor(report) {
4948
5809
  }
4949
5810
  if (report.namespace.warning) console.log(` ${report.namespace.warning}`);
4950
5811
  }
5812
+ function printStorage(database, capture2) {
5813
+ if ("unavailable" in database) {
5814
+ console.log(pc25.yellow(`database: unavailable (${database.unavailable})`));
5815
+ } else {
5816
+ const drift = database.markdownOnly.length + database.dbOnly.length;
5817
+ console.log(
5818
+ `facts: ${database.markdownFacts} Markdown, ${database.databaseFacts} database${database.invalidFiles > 0 ? `, ${database.invalidFiles} unreadable file(s)` : ""}`
5819
+ );
5820
+ console.log(
5821
+ drift === 0 ? pc25.green("sync: Markdown and database agree") : pc25.yellow(
5822
+ `sync: ${database.markdownOnly.length} file(s) not in the database, ${database.dbOnly.length} row(s) without a file (run \`codebuddy reindex\`)`
5823
+ )
5824
+ );
5825
+ if (database.unbacked.length > 0) {
5826
+ console.log(
5827
+ pc25.yellow(
5828
+ `backup: ${database.unbacked.length} database-only fact(s) have no Markdown copy`
5829
+ )
5830
+ );
5831
+ }
5832
+ const e = database.embeddings;
5833
+ console.log(
5834
+ `embeddings: ${e.ready} ready, ${e.pending} pending, ${e.processing} processing, ${e.failed} failed${database.oldestPendingAt ? ` (oldest pending ${database.oldestPendingAt})` : ""}`
5835
+ );
5836
+ for (const space of database.spaces) {
5837
+ console.log(` space ${space.model} ${space.space ?? "(legacy)"}: ${space.count}`);
5838
+ }
5839
+ if (database.spaces.length > 1) {
5840
+ console.log(pc25.yellow(" more than one embedding space: run `codebuddy reindex --respace`"));
5841
+ }
5842
+ }
5843
+ console.log(
5844
+ capture2.stagedNotCaptured.length === 0 && capture2.journalBehind.length === 0 ? pc25.green("capture: nothing pending") : `capture: ${capture2.stagedNotCaptured.length} session(s) with uncaptured edits, ${capture2.journalBehind.length} journal(s) behind their transcript`
5845
+ );
5846
+ }
4951
5847
  export {
4952
5848
  createCli
4953
5849
  };
4954
- //# sourceMappingURL=main-QUASSFP7.js.map
5850
+ //# sourceMappingURL=main-SFC5LV4E.js.map