@orquentra/codebuddy 2.7.0 → 2.8.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 (52) hide show
  1. package/README.md +10 -0
  2. package/dist/adapters-S4HHTWJW.js +24 -0
  3. package/dist/{chunk-EEUFU7KV.js → chunk-6DONJW3W.js} +163 -22
  4. package/dist/chunk-6DONJW3W.js.map +1 -0
  5. package/dist/{chunk-JUAFCLRB.js → chunk-6KGYAFG7.js} +2 -2
  6. package/dist/{chunk-JUAFCLRB.js.map → chunk-6KGYAFG7.js.map} +1 -1
  7. package/dist/{chunk-BQM7FJ7D.js → chunk-6NZEZJYD.js} +5 -3
  8. package/dist/{chunk-BQM7FJ7D.js.map → chunk-6NZEZJYD.js.map} +1 -1
  9. package/dist/{chunk-AQHPEJR6.js → chunk-EJQVGFH2.js} +2 -2
  10. package/dist/{chunk-MSUAVR3R.js → chunk-GXOML4UZ.js} +2 -2
  11. package/dist/chunk-ITQO632W.js +673 -0
  12. package/dist/chunk-ITQO632W.js.map +1 -0
  13. package/dist/chunk-J4T6HHI4.js +1444 -0
  14. package/dist/chunk-J4T6HHI4.js.map +1 -0
  15. package/dist/{chunk-AK2UAE32.js → chunk-NY2YXP3R.js} +2 -2
  16. package/dist/{chunk-5EDGAZQN.js → chunk-TLWGZVPC.js} +64 -725
  17. package/dist/chunk-TLWGZVPC.js.map +1 -0
  18. package/dist/{chunk-LCSBL5TT.js → chunk-UK6XTS2Y.js} +5 -3
  19. package/dist/{chunk-LCSBL5TT.js.map → chunk-UK6XTS2Y.js.map} +1 -1
  20. package/dist/chunk-W7GAXL6C.js +104 -0
  21. package/dist/chunk-W7GAXL6C.js.map +1 -0
  22. package/dist/{chunk-TUR6YKEE.js → chunk-XL2HLACE.js} +1 -1
  23. package/dist/chunk-XL2HLACE.js.map +1 -0
  24. package/dist/{chunk-YJIA2MFL.js → chunk-ZNLSCV3J.js} +15 -13
  25. package/dist/{chunk-YJIA2MFL.js.map → chunk-ZNLSCV3J.js.map} +1 -1
  26. package/dist/cli/index.js +4 -4
  27. package/dist/index.js +9 -8
  28. package/dist/install-BXM56QCD.js +19 -0
  29. package/dist/{lean-command-CWJAVQ5G.js → lean-command-7DFW32MA.js} +4 -3
  30. package/dist/{main-AHNGHC4L.js → main-QUASSFP7.js} +902 -1686
  31. package/dist/main-QUASSFP7.js.map +1 -0
  32. package/dist/{operations-7BTAAUHF.js → operations-WJR7AZZF.js} +3 -3
  33. package/dist/{pretool-command-3SXQSMAO.js → pretool-command-JTKX6G3M.js} +3 -3
  34. package/dist/{runtime-E7JQZ2BZ.js → runtime-OQQ22VJF.js} +7 -6
  35. package/dist/{summaries-XV3JGGCC.js → summaries-ML53R7EL.js} +4 -3
  36. package/dist/summaries-ML53R7EL.js.map +1 -0
  37. package/dist/{zvec-KP4KNB7Y.js → zvec-XDZNZ2OL.js} +4 -3
  38. package/dist/zvec-XDZNZ2OL.js.map +1 -0
  39. package/package.json +1 -1
  40. package/dist/chunk-5EDGAZQN.js.map +0 -1
  41. package/dist/chunk-EEUFU7KV.js.map +0 -1
  42. package/dist/chunk-TUR6YKEE.js.map +0 -1
  43. package/dist/main-AHNGHC4L.js.map +0 -1
  44. /package/dist/{lean-command-CWJAVQ5G.js.map → adapters-S4HHTWJW.js.map} +0 -0
  45. /package/dist/{chunk-AQHPEJR6.js.map → chunk-EJQVGFH2.js.map} +0 -0
  46. /package/dist/{chunk-MSUAVR3R.js.map → chunk-GXOML4UZ.js.map} +0 -0
  47. /package/dist/{chunk-AK2UAE32.js.map → chunk-NY2YXP3R.js.map} +0 -0
  48. /package/dist/{operations-7BTAAUHF.js.map → install-BXM56QCD.js.map} +0 -0
  49. /package/dist/{pretool-command-3SXQSMAO.js.map → lean-command-7DFW32MA.js.map} +0 -0
  50. /package/dist/{runtime-E7JQZ2BZ.js.map → operations-WJR7AZZF.js.map} +0 -0
  51. /package/dist/{summaries-XV3JGGCC.js.map → pretool-command-JTKX6G3M.js.map} +0 -0
  52. /package/dist/{zvec-KP4KNB7Y.js.map → runtime-OQQ22VJF.js.map} +0 -0
@@ -1,6 +1,48 @@
1
+ import {
2
+ renderLeanBlock,
3
+ runLeanHookCommand
4
+ } from "./chunk-UK6XTS2Y.js";
1
5
  import {
2
6
  runPreToolHookCommand
3
- } from "./chunk-AQHPEJR6.js";
7
+ } from "./chunk-EJQVGFH2.js";
8
+ import {
9
+ failSoft,
10
+ readHookInput,
11
+ writeStdout
12
+ } from "./chunk-FJOTP632.js";
13
+ import {
14
+ WORKFLOW_CLIENTS,
15
+ clientDisplayName,
16
+ clientHooksInstalled,
17
+ defaultRulesMode,
18
+ detectClient,
19
+ hermesConfigPath,
20
+ hermesPluginInstalled,
21
+ installClaudeEntry,
22
+ installClient,
23
+ installCodexEntry,
24
+ installWorkflowRules,
25
+ listClaudeEntries,
26
+ listCodexEntries,
27
+ readHermesConfig,
28
+ removeClaudeEntry,
29
+ removeCodexEntry,
30
+ renderManagedBlock,
31
+ renderWorkflowTemplate,
32
+ setHooks,
33
+ uninstallClient,
34
+ writeProjectMcpConfig
35
+ } from "./chunk-J4T6HHI4.js";
36
+ import {
37
+ AUTOMATION_MODES,
38
+ CLIENT_ADAPTERS,
39
+ CLIENT_IDS,
40
+ LIFECYCLE_EVENTS,
41
+ MODE_LABELS,
42
+ parseClient,
43
+ parseMode,
44
+ supportedModes
45
+ } from "./chunk-W7GAXL6C.js";
4
46
  import {
5
47
  isDockerDaemonRunning,
6
48
  tryStartDockerDesktop,
@@ -12,26 +54,25 @@ import {
12
54
  postgresUp
13
55
  } from "./chunk-CSRD5OQA.js";
14
56
  import {
15
- renderLeanBlock,
16
- runLeanHookCommand
17
- } from "./chunk-LCSBL5TT.js";
18
- import {
19
- failSoft,
20
- readHookInput,
21
- writeStdout
22
- } from "./chunk-FJOTP632.js";
23
- import {
57
+ TELEMETRY_NOTICE,
58
+ flushTelemetry,
24
59
  generateSuggestions,
25
60
  migrateMemoryToFiles,
61
+ readTelemetryState,
26
62
  registerCodeBuddyTools,
63
+ showTelemetryNoticeOnce,
27
64
  startMcpServer,
28
- watchRepository
29
- } from "./chunk-EEUFU7KV.js";
65
+ telemetryBlocked,
66
+ telemetryStatePath,
67
+ telemetryUrl,
68
+ watchRepository,
69
+ writeTelemetryState
70
+ } from "./chunk-6DONJW3W.js";
30
71
  import {
31
72
  DEFAULT_ZVEC_MODEL,
32
73
  buildZvecIndex,
33
74
  zvecStatus
34
- } from "./chunk-MSUAVR3R.js";
75
+ } from "./chunk-GXOML4UZ.js";
35
76
  import {
36
77
  PlanFileStore,
37
78
  PlanLifecycle,
@@ -49,21 +90,21 @@ import {
49
90
  runResetHook,
50
91
  runStageHook,
51
92
  runSyncHook
52
- } from "./chunk-YJIA2MFL.js";
53
- import {
54
- parseTraceLines,
55
- readTraces,
56
- tracePath
57
- } from "./chunk-TUR6YKEE.js";
58
- import "./chunk-DFV2SK6C.js";
93
+ } from "./chunk-ZNLSCV3J.js";
59
94
  import {
60
95
  SessionCapsuleLedger,
61
96
  detectTestCommand,
62
97
  estimateTokens
63
98
  } from "./chunk-GKNYMNFM.js";
99
+ import {
100
+ parseTraceLines,
101
+ readTraces,
102
+ tracePath
103
+ } from "./chunk-XL2HLACE.js";
64
104
  import {
65
105
  hookRoot
66
106
  } from "./chunk-TGFTKWOR.js";
107
+ import "./chunk-DFV2SK6C.js";
67
108
  import {
68
109
  bootstrapDatabase,
69
110
  buildProviderFromConfig,
@@ -75,11 +116,11 @@ import {
75
116
  pingDatabase,
76
117
  runDoctor,
77
118
  runMigrations
78
- } from "./chunk-AK2UAE32.js";
119
+ } from "./chunk-NY2YXP3R.js";
79
120
  import {
80
121
  MemoryFileStore,
81
122
  VERSION
82
- } from "./chunk-JUAFCLRB.js";
123
+ } from "./chunk-6KGYAFG7.js";
83
124
  import {
84
125
  computeFactHash,
85
126
  generateEntityId
@@ -92,23 +133,25 @@ import {
92
133
  import {
93
134
  loadLlmSummaries,
94
135
  summarizeRepository
95
- } from "./chunk-BQM7FJ7D.js";
136
+ } from "./chunk-6NZEZJYD.js";
96
137
  import {
97
138
  chatJson,
98
139
  coolingDown,
99
140
  usageToday
100
141
  } from "./chunk-ESZXCZIN.js";
101
142
  import {
102
- buildArchitectureMap,
103
143
  generateGraphifyGraph,
104
- indexRepository,
105
- loadFreshIndex,
106
144
  loadGraphifyMap,
107
- neighbours,
108
- queryMap,
109
145
  setupGraphify,
110
146
  syncFileVectors
111
- } from "./chunk-5EDGAZQN.js";
147
+ } from "./chunk-TLWGZVPC.js";
148
+ import {
149
+ buildArchitectureMap,
150
+ indexRepository,
151
+ loadFreshIndex,
152
+ neighbours,
153
+ queryMap
154
+ } from "./chunk-ITQO632W.js";
112
155
  import {
113
156
  extractSymbolTable,
114
157
  supportsSymbols
@@ -143,14 +186,12 @@ import {
143
186
  setZvecEnabled,
144
187
  writeConfigFile
145
188
  } from "./chunk-KNR2P2UY.js";
146
- import {
147
- writeAtomic
148
- } from "./chunk-CG5B3LYG.js";
189
+ import "./chunk-CG5B3LYG.js";
149
190
  import "./chunk-PZ5AY32C.js";
150
191
 
151
192
  // src/cli/main.ts
152
193
  import { Command } from "commander";
153
- import pc22 from "picocolors";
194
+ import pc23 from "picocolors";
154
195
 
155
196
  // src/integrations/github.ts
156
197
  import { constants } from "fs";
@@ -337,7 +378,7 @@ function runTestCommand(command, source, repositoryRoot, timeoutMs) {
337
378
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
338
379
  throw new Error("--timeout must be a positive number of milliseconds.");
339
380
  }
340
- return new Promise((resolve13) => {
381
+ return new Promise((resolve6) => {
341
382
  const startedAt = Date.now();
342
383
  const child = spawn(command, {
343
384
  cwd: repositoryRoot,
@@ -362,7 +403,7 @@ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
362
403
  }, timeoutMs);
363
404
  child.on("close", (exitCode, signal) => {
364
405
  clearTimeout(timer);
365
- resolve13({
406
+ resolve6({
366
407
  status: timedOut ? "timed_out" : exitCode === 0 ? "passed" : "failed",
367
408
  command,
368
409
  source,
@@ -374,7 +415,7 @@ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
374
415
  });
375
416
  child.on("error", (error) => {
376
417
  clearTimeout(timer);
377
- resolve13({
418
+ resolve6({
378
419
  status: "failed",
379
420
  command,
380
421
  source,
@@ -585,1016 +626,17 @@ function testBadge(status) {
585
626
  return pc.yellow("[not run]");
586
627
  }
587
628
 
588
- // src/cli/commands/claude-desktop.ts
589
- import { existsSync } from "fs";
590
- import { mkdir as mkdir2, readFile } from "fs/promises";
591
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
592
- import { fileURLToPath } from "url";
593
- function claudeDesktopConfigPath(home = process.env.HOME ?? "") {
594
- if (process.platform === "darwin") {
595
- return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
596
- }
597
- if (process.platform === "win32") {
598
- return join2(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
599
- }
600
- return join2(home, ".config", "Claude", "claude_desktop_config.json");
601
- }
602
- async function readClaudeConfig(path) {
603
- if (!existsSync(path)) return {};
604
- try {
605
- return JSON.parse(await readFile(path, "utf8"));
606
- } catch (error) {
607
- throw new Error(
608
- `Failed to parse Claude Desktop config at ${path}: ${error.message}`
609
- );
610
- }
611
- }
612
- async function writeClaudeConfig(path, config) {
613
- await mkdir2(dirname2(path), { recursive: true });
614
- await writeAtomic(path, `${JSON.stringify(config, null, 2)}
615
- `, { mode: 384 });
616
- }
617
- function resolveCliEntrypoint() {
618
- const fromArgv = process.argv[1];
619
- if (fromArgv && existsSync(fromArgv)) return fromArgv;
620
- const here = dirname2(fileURLToPath(import.meta.url));
621
- const candidates = [
622
- resolve2(here, "index.js"),
623
- // bundled: here = dist/cli/
624
- resolve2(here, "..", "cli", "index.js"),
625
- // source: here = src/cli/commands/
626
- resolve2(here, "..", "index.js")
627
- ];
628
- for (const candidate of candidates) {
629
- if (existsSync(candidate)) return candidate;
630
- }
631
- return fromArgv ?? candidates[0] ?? "";
632
- }
633
- function buildEntry(options) {
634
- const env = {
635
- CODEBUDDY_NAMESPACE: options.namespace,
636
- CODEBUDDY_PROJECT_ROOT: resolve2(options.projectRoot ?? process.cwd())
637
- };
638
- if (options.hfToken) env.HF_TOKEN = options.hfToken;
639
- if (options.groqApiKey) env.GROQ_API_KEY = options.groqApiKey;
640
- if (options.databaseUrl) env.DATABASE_URL = options.databaseUrl;
641
- return {
642
- // Absolute: Claude Desktop is a GUI app and does not inherit the shell's
643
- // PATH (nvm, Homebrew), so a bare "node" may not resolve or may be another
644
- // version. Re-run `codebuddy claude install` after switching Node.
645
- command: process.execPath,
646
- args: [resolveCliEntrypoint(), "serve"],
647
- env
648
- };
649
- }
650
- async function installClaudeEntry(options) {
651
- const path = claudeDesktopConfigPath();
652
- const config = await readClaudeConfig(path);
653
- config.mcpServers ??= {};
654
- const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
655
- const existed = config.mcpServers[serverKey] !== void 0;
656
- config.mcpServers[serverKey] = buildEntry(options);
657
- await writeClaudeConfig(path, config);
658
- return { path, serverKey, created: !existed };
659
- }
660
- async function removeClaudeEntry(serverKey) {
661
- const path = claudeDesktopConfigPath();
662
- const config = await readClaudeConfig(path);
663
- if (!config.mcpServers || !(serverKey in config.mcpServers)) {
664
- return { removed: false, path };
665
- }
666
- delete config.mcpServers[serverKey];
667
- await writeClaudeConfig(path, config);
668
- return { removed: true, path };
669
- }
670
- async function listClaudeEntries() {
671
- const path = claudeDesktopConfigPath();
672
- const config = await readClaudeConfig(path);
673
- const entries = Object.entries(config.mcpServers ?? {}).filter(([key]) => key === "codebuddy" || key.startsWith("codebuddy-")).map(([key, entry]) => ({
674
- key,
675
- namespace: entry.env?.CODEBUDDY_NAMESPACE ?? null,
676
- databaseUrl: entry.env?.DATABASE_URL ?? null
677
- }));
678
- return { path, entries };
679
- }
680
-
681
629
  // src/cli/commands/clients.ts
682
630
  import { relative } from "path";
683
631
  import pc2 from "picocolors";
684
632
 
685
- // src/clients/adapters.ts
686
- var AUTOMATION_MODES = ["agent", "hooks", "mcp-only", "lean"];
687
- var MODE_LABELS = {
688
- agent: "agent-driven",
689
- hooks: "hook-driven",
690
- "mcp-only": "MCP-only",
691
- lean: "Lean (token reduction)"
692
- };
693
- var LIFECYCLE_EVENTS = ["prompt", "edit", "turn-end", "compaction"];
694
- var CLIENT_IDS = ["claude", "codex", "opencode", "freebuff", "hermes"];
695
- var CLIENT_ADAPTERS = {
696
- claude: {
697
- name: "Claude Code",
698
- host: { bin: "claude", verified: "2.1.287" },
699
- rulesFile: "CLAUDE.md",
700
- mcp: { file: ".mcp.json", format: "mcpServers" },
701
- hookEvents: LIFECYCLE_EVENTS,
702
- hooks: "claude-settings",
703
- framing: "UserPromptSubmit stdout is added to the prompt as hook context (wrapper is host-defined); unchanged lines are not resent; MCP schemas may be deferred by tool search",
704
- defersMcpSchemas: true
705
- },
706
- codex: {
707
- name: "Codex",
708
- host: { bin: "codex", verified: "0.163.0" },
709
- rulesFile: "AGENTS.md",
710
- mcp: { global: "codex", manual: "codebuddy codex install (global ~/.codex/config.toml)" },
711
- hookEvents: [],
712
- gap: "no CodeBuddy hook adapter for Codex yet",
713
- framing: "agent mode only; AGENTS.md is part of the instructions every request"
714
- },
715
- opencode: {
716
- name: "OpenCode",
717
- host: { bin: "opencode", verified: "1.18.32" },
718
- rulesFile: "AGENTS.md",
719
- // opencode.jsonc is used instead when it is the only one present.
720
- mcp: { file: "opencode.json", format: "opencode" },
721
- // Plugin bridge: chat.message (prompt), tool.execute.after (edit),
722
- // session.idle (turn-end), session.compacted.
723
- hookEvents: LIFECYCLE_EVENTS,
724
- hooks: "opencode-plugin",
725
- framing: "block appended verbatim as a synthetic text part that stays in history; unchanged lines are not resent"
726
- },
727
- freebuff: {
728
- name: "Freebuff",
729
- // Not installed; checked against source CodebuffAI/freebuff@f9ae64dd.
730
- host: { bin: "freebuff", verified: "source f9ae64dd" },
731
- rulesFile: "AGENTS.md",
732
- mcp: { file: ".agents/mcp.json", format: "mcpServers" },
733
- hookEvents: [],
734
- gap: "no lifecycle surface at CodebuffAI/freebuff@f9ae64dd: no user hook config, and the SDK run_file_change_hooks tool is a no-op (sdk/src/tools/run-file-change-hooks.ts)",
735
- note: "reads AGENTS.md, and CLAUDE.md only in a directory without one (common/src/constants/knowledge.ts)",
736
- framing: "agent mode only; AGENTS.md is sent as a fenced block under a fixed project-instructions header"
737
- },
738
- hermes: {
739
- name: "Hermes Agent",
740
- host: { bin: "hermes", verified: "0.17.0" },
741
- rulesFile: "AGENTS.md",
742
- mcp: {
743
- global: "hermes",
744
- manual: "add a codebuddy entry under mcp_servers in ~/.hermes/config.yaml"
745
- },
746
- // Project Python plugin: pre_llm_call (prompt), post_tool_call (edit),
747
- // on_session_end (turn-end). Compaction has no plugin hook; the context is
748
- // resent in full every turn and the session ledger reset with it.
749
- hookEvents: LIFECYCLE_EVENTS,
750
- hooks: "hermes-plugin",
751
- ephemeralContext: true,
752
- framing: "block appended to this turn's user message only and never persisted, so it is resent in full every turn",
753
- note: "hooks load only with HERMES_ENABLE_PROJECT_PLUGINS=1 and codebuddy in plugins.enabled of ~/.hermes/config.yaml; .hermes.md takes precedence over AGENTS.md"
754
- }
755
- };
756
- function parseClient(value) {
757
- const client = value ?? "claude";
758
- if (!CLIENT_IDS.includes(client)) {
759
- throw new Error(`Unknown client "${client}". Use one of: ${CLIENT_IDS.join(", ")}.`);
760
- }
761
- return client;
762
- }
763
- function parseMode(value) {
764
- if (!AUTOMATION_MODES.includes(value)) {
765
- throw new Error(`Unknown mode "${value}". Use one of: ${AUTOMATION_MODES.join(", ")}.`);
766
- }
767
- return value;
768
- }
769
- function supportedModes(adapter) {
770
- return adapter.hookEvents.length === LIFECYCLE_EVENTS.length ? [...AUTOMATION_MODES] : ["agent", "mcp-only"];
771
- }
772
- function sharedRulesReaders(client) {
773
- const file = CLIENT_ADAPTERS[client].rulesFile;
774
- return CLIENT_IDS.filter((id) => id !== client && CLIENT_ADAPTERS[id].rulesFile === file);
775
- }
776
-
777
633
  // src/clients/cost.ts
778
- import { mkdtemp, rm as rm3 } from "fs/promises";
634
+ import { mkdtemp, rm } from "fs/promises";
779
635
  import { tmpdir } from "os";
780
- import { join as join7, resolve as resolve7 } from "path";
636
+ import { join as join2, resolve as resolve2 } from "path";
781
637
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
782
638
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
783
639
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
784
-
785
- // src/templates/install.ts
786
- import { readFile as readFile6, rm as rm2 } from "fs/promises";
787
- import { join as join6, resolve as resolve6 } from "path";
788
-
789
- // src/hooks/hermes-plugin.ts
790
- import { readFile as readFile3 } from "fs/promises";
791
- import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
792
-
793
- // src/hooks/managed-plugin.ts
794
- import { mkdir as mkdir3, readFile as readFile2, rm, rmdir } from "fs/promises";
795
- import { dirname as dirname3 } from "path";
796
- async function read(path) {
797
- try {
798
- return await readFile2(path, "utf8");
799
- } catch (error) {
800
- if (error.code === "ENOENT") return null;
801
- throw error;
802
- }
803
- }
804
- async function managedPluginInstalled(plugin3) {
805
- for (const file of plugin3.files) {
806
- if (!(await read(file.path))?.startsWith(plugin3.marker)) return false;
807
- }
808
- return true;
809
- }
810
- async function setManagedPlugin(plugin3, enabled) {
811
- const current = await Promise.all(plugin3.files.map((file) => read(file.path)));
812
- const foreign = plugin3.files.find((_, i) => {
813
- const text3 = current[i];
814
- return text3 != null && !text3.startsWith(plugin3.marker);
815
- });
816
- if (foreign) {
817
- if (!enabled) return false;
818
- throw new Error(
819
- `${foreign.path} exists and is not managed by CodeBuddy; move it aside and re-run.`
820
- );
821
- }
822
- let changed = false;
823
- for (const [i, file] of plugin3.files.entries()) {
824
- if (enabled && current[i] !== file.content) {
825
- await mkdir3(dirname3(file.path), { recursive: true });
826
- await writeAtomic(file.path, file.content, { mode: 420 });
827
- changed = true;
828
- } else if (!enabled && current[i] != null) {
829
- await rm(file.path, { force: true });
830
- changed = true;
831
- }
832
- }
833
- if (!enabled && changed) {
834
- for (const dir of plugin3.dirs) await rmdir(dir).catch(() => {
835
- });
836
- }
837
- return changed;
838
- }
839
-
840
- // src/hooks/hermes-plugin.ts
841
- var HERMES_PLUGIN_MARKER = "# codebuddy-managed: hermes-plugin";
842
- var HERMES_PLUGIN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1
843
- name: codebuddy
844
- version: 1.0.0
845
- kind: standalone
846
- description: CodeBuddy recall and capture bridge (codebuddy clients install hermes --mode hooks).
847
- provides_hooks:
848
- - pre_llm_call
849
- - post_tool_call
850
- - post_llm_call
851
- - on_session_end
852
- `;
853
- var HERMES_LEAN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1 lean
854
- name: codebuddy
855
- version: 1.0.0
856
- kind: standalone
857
- description: CodeBuddy lean bridge (codebuddy clients install hermes --mode lean).
858
- provides_hooks:
859
- - pre_llm_call
860
- `;
861
- var HERMES_HOOK_FN = `def _hook(command, payload):
862
- """Run one fail-soft CodeBuddy hook; its stdout, or "" on any failure."""
863
- try:
864
- done = subprocess.run(
865
- ["codebuddy", "hooks", command],
866
- input=json.dumps({"cwd": ROOT, **payload}),
867
- capture_output=True,
868
- text=True,
869
- timeout=TIMEOUT_S[command],
870
- cwd=ROOT,
871
- )
872
- except Exception:
873
- return ""
874
- return done.stdout if done.returncode == 0 else ""
875
-
876
-
877
- `;
878
- function renderHermesPlugin(variant) {
879
- return variant === "lean" ? `${HERMES_PLUGIN_MARKER} v1 lean
880
- # Generated by "codebuddy clients install hermes --mode lean"; removed by
881
- # "codebuddy clients uninstall hermes". Local edits are overwritten.
882
- """CodeBuddy lean bridge: forwards each prompt to "codebuddy hooks lean"."""
883
-
884
- import json
885
- import subprocess
886
- from pathlib import Path
887
-
888
- # <root>/.hermes/plugins/codebuddy/__init__.py
889
- ROOT = str(Path(__file__).absolute().parents[3])
890
- TIMEOUT_S = {"lean": 2}
891
-
892
-
893
- ${HERMES_HOOK_FN}def _pre_llm_call(session_id="", user_message="", **_):
894
- if not isinstance(user_message, str) or not user_message.strip():
895
- return None
896
- payload = {
897
- "session_id": session_id,
898
- "hook_event_name": "pre_llm_call",
899
- "ephemeral_context": True,
900
- "prompt": user_message,
901
- }
902
- block = _hook("lean", payload).strip()
903
- return {"context": block} if block else None
904
-
905
-
906
- def register(ctx):
907
- ctx.register_hook("pre_llm_call", _pre_llm_call)
908
- ` : `${HERMES_PLUGIN_MARKER} v1
909
- # Generated by "codebuddy clients install hermes --mode hooks"; removed by
910
- # "codebuddy clients uninstall hermes". Local edits are overwritten.
911
- """CodeBuddy lifecycle bridge: forwards Hermes hooks to "codebuddy hooks <command>"."""
912
-
913
- import json
914
- import subprocess
915
- import threading
916
- from pathlib import Path
917
-
918
- # <root>/.hermes/plugins/codebuddy/__init__.py
919
- ROOT = str(Path(__file__).absolute().parents[3])
920
- EDIT_TOOLS = {"write_file", "patch"}
921
- TIMEOUT_S = {"context": 20, "stage": 10, "capture": 60}
922
- _lock = threading.Lock()
923
- _turns = {}
924
-
925
-
926
- ${HERMES_HOOK_FN}def _turn(turn_id, session_id):
927
- # Compression can rotate the session id mid-turn; a turn keeps its first id.
928
- with _lock:
929
- return _turns.setdefault(
930
- turn_id or session_id, {"session": session_id, "text": "", "commands": []}
931
- )
932
-
933
-
934
- def _pre_llm_call(session_id="", turn_id="", user_message="", **_):
935
- _turn(turn_id, session_id)
936
- payload = {"session_id": session_id, "hook_event_name": "pre_llm_call", "ephemeral_context": True}
937
- if isinstance(user_message, str) and user_message.strip():
938
- payload["prompt"] = user_message
939
- block = _hook("context", payload).strip()
940
- return {"context": block} if block else None
941
-
942
-
943
- def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id="", **_):
944
- turn = _turn(turn_id, session_id)
945
- args = args if isinstance(args, dict) else {}
946
- if tool_name in EDIT_TOOLS:
947
- # Paths only: write_file content never needs to cross the pipe.
948
- tool_input = {k: args[k] for k in ("path", "mode", "patch") if k in args}
949
- _hook("stage", {
950
- "session_id": turn["session"],
951
- "hook_event_name": "post_tool_call",
952
- "tool_name": tool_name,
953
- "tool_input": tool_input,
954
- })
955
- elif tool_name == "terminal" and isinstance(args.get("command"), str):
956
- exit_code = None
957
- if not args.get("background"): # a background process has not finished
958
- try:
959
- exit_code = json.loads(result).get("exit_code")
960
- except Exception:
961
- pass
962
- turn["commands"].append({
963
- "command": args["command"],
964
- "exit_code": exit_code if type(exit_code) is int else None,
965
- })
966
-
967
-
968
- def _post_llm_call(session_id="", turn_id="", assistant_response="", **_):
969
- if isinstance(assistant_response, str) and assistant_response.strip():
970
- _turn(turn_id, session_id)["text"] = assistant_response
971
-
972
-
973
- def _on_session_end(session_id="", turn_id="", **_):
974
- with _lock:
975
- turn = _turns.pop(turn_id or session_id, None)
976
- turn = turn or {"session": session_id, "text": "", "commands": []}
977
- payload = {"session_id": turn["session"], "hook_event_name": "on_session_end"}
978
- if turn["text"]:
979
- payload["last_assistant_message"] = turn["text"]
980
- if turn["commands"]:
981
- payload["shell_commands"] = turn["commands"][-20:]
982
- _hook("capture", payload)
983
-
984
-
985
- def register(ctx):
986
- ctx.register_hook("pre_llm_call", _pre_llm_call)
987
- ctx.register_hook("post_tool_call", _post_tool_call)
988
- ctx.register_hook("post_llm_call", _post_llm_call)
989
- ctx.register_hook("on_session_end", _on_session_end)
990
- `;
991
- }
992
- var HERMES_PLUGIN_SOURCE = renderHermesPlugin("full");
993
- function hermesPluginDir(repositoryRoot) {
994
- return join3(resolve3(repositoryRoot), ".hermes", "plugins", "codebuddy");
995
- }
996
- function plugin(repositoryRoot, variant = "full") {
997
- const dir = hermesPluginDir(repositoryRoot);
998
- return {
999
- marker: HERMES_PLUGIN_MARKER,
1000
- files: [
1001
- {
1002
- path: join3(dir, "plugin.yaml"),
1003
- content: variant === "lean" ? HERMES_LEAN_MANIFEST : HERMES_PLUGIN_MANIFEST
1004
- },
1005
- { path: join3(dir, "__init__.py"), content: renderHermesPlugin(variant) }
1006
- ],
1007
- dirs: [dir, dirname4(dir), dirname4(dirname4(dir))]
1008
- };
1009
- }
1010
- function hermesPluginInstalled(repositoryRoot) {
1011
- return managedPluginInstalled(plugin(repositoryRoot));
1012
- }
1013
- function setHermesPlugin(repositoryRoot, enabled, variant = "full") {
1014
- return setManagedPlugin(plugin(repositoryRoot, variant), enabled);
1015
- }
1016
- async function hermesPluginVariant(repositoryRoot) {
1017
- if (!await hermesPluginInstalled(repositoryRoot)) return null;
1018
- const source = await readFile3(join3(hermesPluginDir(repositoryRoot), "__init__.py"), "utf8");
1019
- return source.startsWith(`${HERMES_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
1020
- }
1021
-
1022
- // src/hooks/opencode-plugin.ts
1023
- import { readFile as readFile4 } from "fs/promises";
1024
- import { dirname as dirname5, join as join4, resolve as resolve4 } from "path";
1025
- var OPENCODE_PLUGIN_MARKER = "// codebuddy-managed: opencode-plugin";
1026
- var OPENCODE_RUNTIME = `const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1027
- let lastMs = 0;
1028
- let counter = 0;
1029
-
1030
- /** Run one fail-soft CodeBuddy hook; resolves to its stdout, "" on any failure. */
1031
- function hook(command, payload) {
1032
- return new Promise((done) => {
1033
- let out = "";
1034
- let child;
1035
- try {
1036
- child = spawn("codebuddy", ["hooks", command], {
1037
- cwd: payload.cwd,
1038
- stdio: ["pipe", "pipe", "ignore"],
1039
- // npm installs a codebuddy.cmd shim on Windows, which spawn only runs through a shell.
1040
- shell: process.platform === "win32",
1041
- });
1042
- } catch {
1043
- done("");
1044
- return;
1045
- }
1046
- const timer = setTimeout(() => child.kill(), TIMEOUT_MS[command]);
1047
- child.stdout.setEncoding("utf8");
1048
- child.stdout.on("data", (chunk) => {
1049
- out += chunk;
1050
- });
1051
- child.on("error", () => {
1052
- clearTimeout(timer);
1053
- done("");
1054
- });
1055
- child.on("close", (code) => {
1056
- clearTimeout(timer);
1057
- done(code === 0 ? out : "");
1058
- });
1059
- child.stdin.on("error", () => {});
1060
- child.stdin.end(JSON.stringify(payload));
1061
- });
1062
- }
1063
-
1064
- /** An ascending OpenCode part id (prt_ + 48-bit time/counter hex + 14 base62). */
1065
- function partId() {
1066
- const now = Date.now();
1067
- if (now !== lastMs) {
1068
- lastMs = now;
1069
- counter = 0;
1070
- }
1071
- counter += 1;
1072
- const time = (BigInt(now) * 4096n + BigInt(counter)) & 0xffffffffffffn;
1073
- let random = "";
1074
- for (const byte of crypto.getRandomValues(new Uint8Array(14))) random += ID_CHARS[byte % 62];
1075
- return "prt_" + time.toString(16).padStart(12, "0") + random;
1076
- }
1077
-
1078
- `;
1079
- function renderOpencodePlugin(variant) {
1080
- return variant === "lean" ? `${OPENCODE_PLUGIN_MARKER} v1 lean
1081
- // Generated by \`codebuddy clients install opencode --mode lean\`; removed by
1082
- // \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
1083
- import { spawn } from "node:child_process";
1084
-
1085
- const TIMEOUT_MS = { lean: 2000, reset: 10000 };
1086
- ${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
1087
- export const CodeBuddyPlugin = async ({ directory }) => {
1088
- const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
1089
-
1090
- return {
1091
- "chat.message": async (input, output) => {
1092
- const prompt = output.parts
1093
- .flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
1094
- .join("\\n")
1095
- .trim();
1096
- if (!prompt) return;
1097
- const block = (await hook("lean", { ...base(input.sessionID, "chat.message"), prompt })).trim();
1098
- if (!block) return;
1099
- output.parts.push({
1100
- id: partId(),
1101
- sessionID: input.sessionID,
1102
- messageID: output.message.id,
1103
- type: "text",
1104
- text: block,
1105
- synthetic: true,
1106
- });
1107
- },
1108
- event: async ({ event }) => {
1109
- if (event.type === "session.compacted") {
1110
- await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
1111
- }
1112
- },
1113
- };
1114
- };
1115
- ` : `${OPENCODE_PLUGIN_MARKER} v1
1116
- // Generated by \`codebuddy clients install opencode --mode hooks\`; removed by
1117
- // \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
1118
- import { spawn } from "node:child_process";
1119
-
1120
- const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]);
1121
- const TIMEOUT_MS = { context: 20000, stage: 10000, capture: 60000, reset: 10000 };
1122
- ${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
1123
- export const CodeBuddyPlugin = async ({ directory }) => {
1124
- /** @type {Map<string, { text: string, commands: Array<{ command: string, exit_code: number | null }> }>} */
1125
- const turns = new Map();
1126
- const turn = (sessionID) => {
1127
- if (!turns.has(sessionID)) turns.set(sessionID, { text: "", commands: [] });
1128
- return turns.get(sessionID);
1129
- };
1130
- const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
1131
-
1132
- return {
1133
- "chat.message": async (input, output) => {
1134
- turns.delete(input.sessionID);
1135
- const prompt = output.parts
1136
- .flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
1137
- .join("\\n")
1138
- .trim();
1139
- const block = (
1140
- await hook("context", { ...base(input.sessionID, "chat.message"), ...(prompt ? { prompt } : {}) })
1141
- ).trim();
1142
- if (!block) return;
1143
- output.parts.push({
1144
- id: partId(),
1145
- sessionID: input.sessionID,
1146
- messageID: output.message.id,
1147
- type: "text",
1148
- text: block,
1149
- synthetic: true,
1150
- });
1151
- },
1152
- "tool.execute.after": async (input, output) => {
1153
- if (EDIT_TOOLS.has(input.tool)) {
1154
- await hook("stage", {
1155
- ...base(input.sessionID, "tool.execute.after"),
1156
- tool_name: input.tool,
1157
- tool_input: input.args,
1158
- });
1159
- } else if (input.tool === "bash" && typeof input.args?.command === "string") {
1160
- const exit = output.metadata?.exit;
1161
- turn(input.sessionID).commands.push({
1162
- command: input.args.command,
1163
- exit_code: typeof exit === "number" ? exit : null,
1164
- });
1165
- }
1166
- },
1167
- "experimental.text.complete": async (input, output) => {
1168
- if (output.text.trim()) turn(input.sessionID).text = output.text;
1169
- },
1170
- event: async ({ event }) => {
1171
- if (event.type === "session.idle") {
1172
- const sessionID = event.properties.sessionID;
1173
- const current = turns.get(sessionID);
1174
- turns.delete(sessionID);
1175
- await hook("capture", {
1176
- ...base(sessionID, "session.idle"),
1177
- ...(current?.text ? { last_assistant_message: current.text } : {}),
1178
- ...(current?.commands.length ? { shell_commands: current.commands.slice(-20) } : {}),
1179
- });
1180
- } else if (event.type === "session.compacted") {
1181
- await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
1182
- }
1183
- },
1184
- };
1185
- };
1186
- `;
1187
- }
1188
- var OPENCODE_PLUGIN_SOURCE = renderOpencodePlugin("full");
1189
- function opencodePluginPath(repositoryRoot) {
1190
- return join4(resolve4(repositoryRoot), ".opencode", "plugins", "codebuddy.js");
1191
- }
1192
- function plugin2(repositoryRoot, variant = "full") {
1193
- const path = opencodePluginPath(repositoryRoot);
1194
- return {
1195
- marker: OPENCODE_PLUGIN_MARKER,
1196
- files: [{ path, content: renderOpencodePlugin(variant) }],
1197
- dirs: [dirname5(path), dirname5(dirname5(path))]
1198
- };
1199
- }
1200
- function opencodePluginInstalled(repositoryRoot) {
1201
- return managedPluginInstalled(plugin2(repositoryRoot));
1202
- }
1203
- function setOpencodePlugin(repositoryRoot, enabled, variant = "full") {
1204
- return setManagedPlugin(plugin2(repositoryRoot, variant), enabled);
1205
- }
1206
- async function opencodePluginVariant(repositoryRoot) {
1207
- if (!await opencodePluginInstalled(repositoryRoot)) return null;
1208
- const source = await readFile4(opencodePluginPath(repositoryRoot), "utf8");
1209
- return source.startsWith(`${OPENCODE_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
1210
- }
1211
-
1212
- // src/hooks/settings.ts
1213
- import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
1214
- import { dirname as dirname6, join as join5, resolve as resolve5 } from "path";
1215
- var HOOK_COMMAND_PREFIX = "codebuddy hooks";
1216
- var TOOL_GUARD_DEF = {
1217
- event: "PreToolUse",
1218
- matcher: "Read|Grep|Bash",
1219
- command: `${HOOK_COMMAND_PREFIX} pretool`
1220
- };
1221
- var HOOK_DEFS = [
1222
- { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} context` },
1223
- {
1224
- event: "PostToolUse",
1225
- matcher: "Edit|Write|MultiEdit",
1226
- command: `${HOOK_COMMAND_PREFIX} stage`
1227
- },
1228
- { event: "Stop", command: `${HOOK_COMMAND_PREFIX} capture` },
1229
- // After compaction or /clear the model no longer holds earlier context, so
1230
- // capsules the session ledger marked as sent must be sent again.
1231
- { event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
1232
- TOOL_GUARD_DEF
1233
- ];
1234
- var LEAN_HOOK_DEFS = [
1235
- { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} lean` },
1236
- { event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
1237
- TOOL_GUARD_DEF
1238
- ];
1239
- function claudeSettingsPath(repositoryRoot = process.cwd()) {
1240
- return join5(resolve5(repositoryRoot), ".claude", "settings.json");
1241
- }
1242
- function upsertHooks(settings, variant = "full") {
1243
- const defs = variant === "lean" ? LEAN_HOOK_DEFS : HOOK_DEFS;
1244
- const wanted = new Set(defs.map((def) => def.command));
1245
- const next = { ...settings, hooks: { ...settings.hooks ?? {} } };
1246
- const hooks = next.hooks;
1247
- let changed = false;
1248
- for (const event of Object.keys(hooks)) {
1249
- const groups = [];
1250
- for (const group of hooks[event] ?? []) {
1251
- const kept = (group.hooks ?? []).filter(
1252
- (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX) || wanted.has(hook.command)
1253
- );
1254
- if (kept.length !== (group.hooks ?? []).length) changed = true;
1255
- if (kept.length > 0) groups.push({ ...group, hooks: kept });
1256
- }
1257
- if (groups.length > 0) hooks[event] = groups;
1258
- else delete hooks[event];
1259
- }
1260
- for (const def of defs) {
1261
- const groups = [...hooks[def.event] ?? []];
1262
- const existingIndex = groups.findIndex(
1263
- (group) => group.hooks?.some((hook) => hook.command === def.command)
1264
- );
1265
- if (existingIndex === -1) {
1266
- groups.push({
1267
- ...def.matcher ? { matcher: def.matcher } : {},
1268
- hooks: [{ type: "command", command: def.command }]
1269
- });
1270
- changed = true;
1271
- } else {
1272
- const existing = groups[existingIndex];
1273
- if (existing && (def.matcher ?? void 0) !== (existing.matcher ?? void 0)) {
1274
- const { matcher: _oldMatcher, ...rest } = existing;
1275
- groups[existingIndex] = def.matcher ? { ...rest, matcher: def.matcher } : rest;
1276
- changed = true;
1277
- }
1278
- }
1279
- hooks[def.event] = groups;
1280
- }
1281
- return { settings: next, changed };
1282
- }
1283
- function removeHooks(settings) {
1284
- if (!settings.hooks) return { settings, changed: false };
1285
- const next = { ...settings, hooks: { ...settings.hooks } };
1286
- const hooks = next.hooks;
1287
- let changed = false;
1288
- for (const event of Object.keys(hooks)) {
1289
- const keptGroups = [];
1290
- for (const group of hooks[event] ?? []) {
1291
- const keptHooks = (group.hooks ?? []).filter(
1292
- (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX)
1293
- );
1294
- if (keptHooks.length !== (group.hooks ?? []).length) changed = true;
1295
- if (keptHooks.length === 0) continue;
1296
- keptGroups.push({ ...group, hooks: keptHooks });
1297
- }
1298
- if (keptGroups.length === 0) delete hooks[event];
1299
- else hooks[event] = keptGroups;
1300
- }
1301
- return { settings: next, changed };
1302
- }
1303
- function hooksInstalled(settings) {
1304
- const hooks = settings.hooks ?? {};
1305
- return Object.values(hooks).some(
1306
- (groups) => groups.some((group) => group.hooks?.some((h) => h.command?.startsWith(HOOK_COMMAND_PREFIX)))
1307
- );
1308
- }
1309
- function hooksVariant(settings) {
1310
- if (!hooksInstalled(settings)) return null;
1311
- const leanCommand = `${HOOK_COMMAND_PREFIX} lean`;
1312
- const lean = Object.values(settings.hooks ?? {}).some(
1313
- (groups) => groups.some((group) => group.hooks?.some((hook) => hook.command === leanCommand))
1314
- );
1315
- return lean ? "lean" : "full";
1316
- }
1317
- async function readSettings(path) {
1318
- try {
1319
- return JSON.parse(await readFile5(path, "utf8"));
1320
- } catch (error) {
1321
- if (error.code === "ENOENT") return {};
1322
- throw error;
1323
- }
1324
- }
1325
- async function writeSettings(path, settings) {
1326
- await mkdir4(dirname6(path), { recursive: true });
1327
- await writeAtomic(path, `${JSON.stringify(settings, null, 2)}
1328
- `, { mode: 420 });
1329
- }
1330
-
1331
- // src/hooks/install.ts
1332
- var SURFACES = {
1333
- "claude-settings": {
1334
- path: claudeSettingsPath,
1335
- installed: async (root) => hooksInstalled(await readSettings(claudeSettingsPath(root))),
1336
- variant: async (root) => hooksVariant(await readSettings(claudeSettingsPath(root))),
1337
- set: async (root, enabled, variant) => {
1338
- const path = claudeSettingsPath(root);
1339
- const current = await readSettings(path);
1340
- const { settings, changed } = enabled ? upsertHooks(current, variant) : removeHooks(current);
1341
- if (changed) await writeSettings(path, settings);
1342
- return changed;
1343
- }
1344
- },
1345
- "opencode-plugin": {
1346
- path: opencodePluginPath,
1347
- installed: opencodePluginInstalled,
1348
- variant: opencodePluginVariant,
1349
- set: setOpencodePlugin
1350
- },
1351
- "hermes-plugin": {
1352
- path: hermesPluginDir,
1353
- installed: hermesPluginInstalled,
1354
- variant: hermesPluginVariant,
1355
- set: setHermesPlugin
1356
- }
1357
- };
1358
- async function setClientHooks(root, client, enabled, variant = "full") {
1359
- const surface = CLIENT_ADAPTERS[client].hooks;
1360
- if (!surface) return null;
1361
- const { path, set } = SURFACES[surface];
1362
- return { target: path(root), changed: await set(root, enabled, variant) };
1363
- }
1364
- async function clientHooksVariant(root, client) {
1365
- const surface = CLIENT_ADAPTERS[client].hooks;
1366
- if (!surface) return void 0;
1367
- return SURFACES[surface].variant(root);
1368
- }
1369
- async function clientHooksInstalled(root, client) {
1370
- const surface = CLIENT_ADAPTERS[client].hooks;
1371
- return surface ? SURFACES[surface].installed(root) : null;
1372
- }
1373
- async function hookedRulesSiblings(root, client) {
1374
- const file = CLIENT_ADAPTERS[client].rulesFile;
1375
- const hooked = [];
1376
- for (const id of CLIENT_IDS) {
1377
- if (id !== client && CLIENT_ADAPTERS[id].rulesFile === file && await clientHooksInstalled(root, id))
1378
- hooked.push(id);
1379
- }
1380
- return hooked;
1381
- }
1382
- async function rulesFileHooked(root, client) {
1383
- return await clientHooksInstalled(root, client) === true || (await hookedRulesSiblings(root, client)).length > 0;
1384
- }
1385
-
1386
- // src/templates/workflow.ts
1387
- var WORKFLOW_CLIENTS = CLIENT_IDS;
1388
- var INTRO = [
1389
- "You have CodeBuddy available as a local, MCP-backed project-context engine.",
1390
- "Use it to work like a careful senior engineer, not to dump broad context.",
1391
- ""
1392
- ];
1393
- var START_AGENT = [
1394
- "## At the start of a substantive coding turn",
1395
- "",
1396
- "1. When project context is needed, call `context_pack` with the user's task and a token budget (start",
1397
- " with 4000). It automatically finds relevant files and returns bounded symbols,",
1398
- " plan, policy, incidents, risks, architecture neighbours, and team facts.",
1399
- " Reuse evidence already present. For a clearly identified small edit, read the",
1400
- " needed source directly; fetch context for unknown dependencies or project constraints.",
1401
- " Read missing function bodies before editing. If safety evidence was omitted,",
1402
- " narrow the targets or increase the budget before proceeding.",
1403
- "2. Use `context_bootstrap` only when you need broad project orientation beyond",
1404
- " the task pack. Use `context_before_edit` when you already have exact paths",
1405
- " or a plan and need a narrower follow-up.",
1406
- "3. Prior work from earlier chats arrives as `handoffs` with freshness",
1407
- " (`changedSince`, `missing`); re-check those files before relying on it.",
1408
- "4. On repeated context calls in one conversation, pass a stable `sessionId` and",
1409
- " the previous response's `delivery.deliveryId` as `ack`. Unchanged sections",
1410
- " are then omitted and listed in `delivery.unchanged`. If earlier output is no",
1411
- " longer in your context (e.g. after compaction), pass `full: true`.",
1412
- ""
1413
- ];
1414
- var START_HOOKS = [
1415
- "## At the start of a substantive coding turn",
1416
- "",
1417
- "CodeBuddy hooks inject a `CodeBuddy project context (auto-recalled)` block into",
1418
- "each prompt: project, plan, policies, facts, incidents, handoffs, current",
1419
- "changes, and risk. Agents without CodeBuddy hooks (no such block in the prompt)",
1420
- "follow the agent-driven flow: `context_pack` first, `context_after_turn` last.",
1421
- "",
1422
- "1. Start from the injected block; do not call `context_bootstrap` for orientation",
1423
- " it already covers. Its `task files` are CodeBuddy's best match; when it names",
1424
- " none, call `context_pack` with the task before searching the repository yourself.",
1425
- "2. Read ranges, not whole files: `Read` with offset/limit, or `sed -n 'A,Bp'`.",
1426
- " Large files read whole (`Read` without a range, `cat`, `less`) are denied with an",
1427
- " outline of their symbols and line ranges; read the range you need from it.",
1428
- "3. Call `context_pack` (or `context_before_edit` with exact paths) when the",
1429
- " task needs symbols, neighbours, or file-level evidence the block lacks. Pass the",
1430
- " block's `session` value as `sessionId` and the previous `delivery.deliveryId`",
1431
- " as `ack` so unchanged sections are not resent.",
1432
- "4. Prior work from earlier chats arrives as handoffs with freshness; re-check",
1433
- " those files before relying on it.",
1434
- ""
1435
- ];
1436
- var MIDDLE = [
1437
- "## While implementing",
1438
- "",
1439
- "- Prefer the smallest sufficient set of edits; preserve existing behavior.",
1440
- "- Honor risk warnings and policy rules for the files you touch; call out risk",
1441
- " before editing sensitive areas.",
1442
- "- Do not re-request broad context if the task pack already suffices.",
1443
- "",
1444
- "## Before finalizing",
1445
- "",
1446
- "- Consider calling `code_suggestions` (with `paths`, `planId`, or `useGit`) for",
1447
- " read-only, evidence-backed quality findings; resolve high-severity ones.",
1448
- "- Run the smallest relevant verification (tests, lint, typecheck) and fix",
1449
- " failures before presenting completion.",
1450
- ""
1451
- ];
1452
- var AFTER_AGENT = [
1453
- "## After a meaningful turn",
1454
- "",
1455
- "- Call `context_after_turn` with a concise `summary`, the `changedFiles`, and a",
1456
- " `planId` if one is active, plus `verification` (commands run and results)",
1457
- " so the turn's worklog handoff records it. CodeBuddy auto-saves confident facts, decisions,",
1458
- " and incidents, queues uncertain or sensitive items for review, and drops",
1459
- " chatter. Never paste secrets, tokens, or credentials into the summary.",
1460
- ""
1461
- ];
1462
- var AFTER_HOOKS = [
1463
- "## After a meaningful turn",
1464
- "",
1465
- "- Do not call `context_after_turn`: the turn-end hook captures the turn once, from",
1466
- " the real change set and the verification commands you ran. A second capture",
1467
- " would record a divergent worklog entry.",
1468
- "- Never paste secrets, tokens, or credentials into your final message; it is the",
1469
- " capture summary.",
1470
- ""
1471
- ];
1472
- var TAIL = [
1473
- "## If a tool or service is unavailable (degraded mode)",
1474
- "",
1475
- "- A CodeBuddy MCP tool errors or is missing: proceed using the repository",
1476
- " itself as the source of truth; do not block on it.",
1477
- "- Postgres is unreachable: the file-based tools (context, plan, risk, map,",
1478
- " suggest) still work; recall and embeddings may be degraded \u2014 say so instead",
1479
- " of inventing remembered facts.",
1480
- "- Retrieval returns no context: read the relevant files directly rather than",
1481
- " guessing; the working tree is authoritative.",
1482
- "",
1483
- "## Never",
1484
- "",
1485
- "- Invent behavior the codebase does not support.",
1486
- "- Ignore failing tests or lint.",
1487
- "- Make speculative, wide changes without evidence.",
1488
- "- Save low-confidence project memory as a durable fact."
1489
- ];
1490
- function renderBody(mode) {
1491
- return [
1492
- ...INTRO,
1493
- ...mode === "hooks" ? START_HOOKS : START_AGENT,
1494
- ...MIDDLE,
1495
- ...mode === "hooks" ? AFTER_HOOKS : AFTER_AGENT,
1496
- ...TAIL
1497
- ].join("\n");
1498
- }
1499
- function rulesReaders(file) {
1500
- return CLIENT_IDS.filter((id) => CLIENT_ADAPTERS[id].rulesFile === file).map((id) => CLIENT_ADAPTERS[id].name).join(", ");
1501
- }
1502
- function renderWorkflowTemplate(client, mode = "agent") {
1503
- const file = CLIENT_ADAPTERS[client].rulesFile;
1504
- return [
1505
- `# CodeBuddy workflow (${rulesReaders(file)})`,
1506
- "",
1507
- "This section is managed by CodeBuddy (`codebuddy clients install <client> --mode <mode>`)",
1508
- `and lives in \`${file}\`. Edit outside the managed markers to customize.`,
1509
- `Automation mode: \`${mode}\` (${MODE_LABELS[mode]}).`,
1510
- "",
1511
- renderBody(mode)
1512
- ].join("\n");
1513
- }
1514
- function parseWorkflowMode(block) {
1515
- return block.match(/Automation mode: `(hooks|agent)`/)?.[1] === "hooks" ? "hooks" : "agent";
1516
- }
1517
- function defaultRulesFile(client) {
1518
- return CLIENT_ADAPTERS[client].rulesFile;
1519
- }
1520
- function clientDisplayName(client) {
1521
- return CLIENT_ADAPTERS[client].name;
1522
- }
1523
-
1524
- // src/templates/install.ts
1525
- var START_MARKER = "<!-- codebuddy:workflow:start -->";
1526
- var END_MARKER = "<!-- codebuddy:workflow:end -->";
1527
- function renderManagedBlock(client, mode = "agent") {
1528
- return `${START_MARKER}
1529
- ${renderWorkflowTemplate(client, mode)}
1530
- ${END_MARKER}
1531
- `;
1532
- }
1533
- async function installWorkflowRules(options) {
1534
- const projectRoot = resolve6(options.projectRoot ?? process.cwd());
1535
- const path = options.targetPath ?? join6(projectRoot, defaultRulesFile(options.client));
1536
- const existing = await readIfPresent(path);
1537
- const mode = options.mode ?? await defaultRulesMode(options.client, projectRoot);
1538
- if (options.onlyIfManaged && (existing === null || !hasManagedBlock(existing))) {
1539
- return { path, client: options.client, action: "unchanged" };
1540
- }
1541
- if (mode === "mcp-only") {
1542
- const content2 = existing === null ? null : removeBlock(existing);
1543
- if (content2 === null || content2 === existing) {
1544
- return { path, client: options.client, action: "unchanged" };
1545
- }
1546
- if (content2.trim() === "") await rm2(path, { force: true });
1547
- else await writeAtomic(path, content2, { mode: 420 });
1548
- return { path, client: options.client, action: "removed" };
1549
- }
1550
- const { content, action } = upsertBlock(existing, renderManagedBlock(options.client, mode));
1551
- if (action === "unchanged") return { path, client: options.client, action };
1552
- await writeAtomic(path, content, { mode: 420 });
1553
- return { path, client: options.client, action };
1554
- }
1555
- function upsertBlock(existing, block) {
1556
- if (existing === null) return { content: block, action: "created" };
1557
- const start = existing.indexOf(START_MARKER);
1558
- const end = existing.indexOf(END_MARKER);
1559
- if (start !== -1 && end !== -1 && end > start) {
1560
- const before = existing.slice(0, start);
1561
- const after = existing.slice(end + END_MARKER.length).replace(/^\n/, "");
1562
- const replaced = `${before}${block}${after}`;
1563
- return { content: replaced, action: replaced === existing ? "unchanged" : "updated" };
1564
- }
1565
- const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
1566
- return { content: `${existing}${separator}${block}`, action: "added" };
1567
- }
1568
- async function defaultRulesMode(client, projectRoot = process.cwd()) {
1569
- return await rulesFileHooked(projectRoot, client) ? "hooks" : "agent";
1570
- }
1571
- async function readManagedMode(path) {
1572
- const existing = await readIfPresent(path);
1573
- if (existing === null || !hasManagedBlock(existing)) return null;
1574
- return parseWorkflowMode(existing.slice(existing.indexOf(START_MARKER)));
1575
- }
1576
- function hasManagedBlock(content) {
1577
- const start = content.indexOf(START_MARKER);
1578
- return start !== -1 && content.indexOf(END_MARKER) > start;
1579
- }
1580
- function removeBlock(existing) {
1581
- if (!hasManagedBlock(existing)) return existing;
1582
- const start = existing.indexOf(START_MARKER);
1583
- const end = existing.indexOf(END_MARKER) + END_MARKER.length;
1584
- const before = existing.slice(0, start).replace(/\n\n$/, "\n");
1585
- const after = existing.slice(end).replace(/^\n/, "");
1586
- return `${before}${after}`;
1587
- }
1588
- async function readIfPresent(path) {
1589
- try {
1590
- return await readFile6(path, "utf8");
1591
- } catch (error) {
1592
- if (error.code === "ENOENT") return null;
1593
- throw error;
1594
- }
1595
- }
1596
-
1597
- // src/clients/cost.ts
1598
640
  async function mcpSchemaTokens(root, namespace) {
1599
641
  const server = new McpServer({ name: "codebuddy", version: VERSION });
1600
642
  registerCodeBuddyTools(server, {
@@ -1624,7 +666,7 @@ async function hookBlockTokens(root, namespace) {
1624
666
  namespace,
1625
667
  useGit: true
1626
668
  }).catch(() => null);
1627
- const scratch = await mkdtemp(join7(tmpdir(), "codebuddy-cost-"));
669
+ const scratch = await mkdtemp(join2(tmpdir(), "codebuddy-cost-"));
1628
670
  try {
1629
671
  const ledger = new SessionCapsuleLedger(scratch);
1630
672
  const render = () => renderContextBlock(bootstrap, beforeEdit, ledger, "cost", true);
@@ -1636,546 +678,50 @@ async function hookBlockTokens(root, namespace) {
1636
678
  const leanFirst = await renderLean();
1637
679
  await ledger.markSent("lean", leanFirst.emitted);
1638
680
  const leanLater = await renderLean();
1639
- return {
1640
- first: estimateTokens(first.text),
1641
- later: estimateTokens(later.text),
1642
- lean: { first: estimateTokens(leanFirst.text), later: estimateTokens(leanLater.text) }
1643
- };
1644
- } finally {
1645
- await rm3(scratch, { recursive: true, force: true });
1646
- }
1647
- }
1648
- async function measureClientCosts(projectRoot = process.cwd(), namespace) {
1649
- const root = resolve7(projectRoot);
1650
- const ns = namespace ?? await resolveNamespaceFrom(root);
1651
- const mcpSchema = await mcpSchemaTokens(root, ns);
1652
- const block = await hookBlockTokens(root, ns);
1653
- return CLIENT_IDS.flatMap((client) => {
1654
- const adapter = CLIENT_ADAPTERS[client];
1655
- return supportedModes(adapter).flatMap((mode) => {
1656
- if (mode === "mcp-only") return [];
1657
- if (mode === "lean") {
1658
- const later2 = adapter.ephemeralContext ? block.lean.first : block.lean.later;
1659
- return [
1660
- { client, mode, rules: 0, mcpSchema: 0, firstBlock: block.lean.first, laterBlock: later2 }
1661
- ];
1662
- }
1663
- const hooks = mode === "hooks";
1664
- const later = adapter.ephemeralContext ? block.first : block.later;
1665
- return [
1666
- {
1667
- client,
1668
- mode,
1669
- rules: estimateTokens(renderManagedBlock(client, mode)),
1670
- mcpSchema: adapter.defersMcpSchemas ? mcpSchema.listing : mcpSchema.full,
1671
- firstBlock: hooks ? block.first : 0,
1672
- laterBlock: hooks ? later : 0
1673
- }
1674
- ];
1675
- });
1676
- });
1677
- }
1678
-
1679
- // src/clients/install.ts
1680
- import { access as access3 } from "fs/promises";
1681
- import { join as join11, resolve as resolve9 } from "path";
1682
-
1683
- // src/integrations/project-mcp.ts
1684
- import { access as access2, mkdir as mkdir5, readFile as readFile7, rm as rm4 } from "fs/promises";
1685
- import { dirname as dirname7, join as join8, resolve as resolve8 } from "path";
1686
- async function writeProjectMcpConfig(options) {
1687
- const path = join8(options.repositoryRoot, ".mcp.json");
1688
- const current = await readProjectMcpConfig(path);
1689
- current.mcpServers ??= {};
1690
- current.mcpServers[`codebuddy-${options.namespace}`] = codebuddyMcpServer(
1691
- options.repositoryRoot,
1692
- options.namespace
1693
- );
1694
- if (options.graphify) {
1695
- current.mcpServers.graphify = {
1696
- command: "python",
1697
- args: [
1698
- "-m",
1699
- "graphify.serve",
1700
- resolve8(options.repositoryRoot, options.graphPath ?? "graphify-out/graph.json")
1701
- ],
1702
- env: { CODEBUDDY_PROJECT_ROOT: options.repositoryRoot }
1703
- };
1704
- } else if (current.mcpServers.graphify) {
1705
- delete current.mcpServers.graphify;
1706
- }
1707
- await writeAtomic(path, `${JSON.stringify(current, null, 2)}
1708
- `, { mode: 420 });
1709
- return { path, servers: Object.keys(current.mcpServers).sort() };
1710
- }
1711
- var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
1712
- function codebuddyMcpServer(repositoryRoot, namespace) {
1713
- return {
1714
- command: "codebuddy",
1715
- args: ["serve"],
1716
- env: {
1717
- CODEBUDDY_PROJECT_ROOT: repositoryRoot,
1718
- CODEBUDDY_NAMESPACE: namespace,
1719
- CODEBUDDY_TOOL_PROFILE: "core"
1720
- }
1721
- };
1722
- }
1723
- async function projectMcpPath(root, file, format) {
1724
- const path = join8(root, file);
1725
- if (format !== "opencode" || await exists(path)) return path;
1726
- return await exists(`${path}c`) ? `${path}c` : path;
1727
- }
1728
- async function exists(path) {
1729
- return access2(path).then(
1730
- () => true,
1731
- () => false
1732
- );
1733
- }
1734
- function formatEntry(format, server) {
1735
- if (format === "mcpServers") return server;
1736
- return {
1737
- type: "local",
1738
- command: [server.command, ...server.args],
1739
- ...server.env ? { environment: server.env } : {},
1740
- enabled: true
1741
- };
1742
- }
1743
- function serversKey(format) {
1744
- return format === "opencode" ? "mcp" : "mcpServers";
1745
- }
1746
- async function upsertMcpServer(path, format, name, server) {
1747
- const existed = await exists(path);
1748
- const { config, comments } = await readConfig(path);
1749
- const key = serversKey(format);
1750
- const servers2 = { ...config[key] ?? {} };
1751
- const entry = formatEntry(format, server);
1752
- if (JSON.stringify(servers2[name]) === JSON.stringify(entry)) return "unchanged";
1753
- if (comments) return "manual";
1754
- servers2[name] = entry;
1755
- const next = format === "opencode" && !existed ? { $schema: OPENCODE_SCHEMA, ...config, [key]: servers2 } : { ...config, [key]: servers2 };
1756
- await mkdir5(dirname7(path), { recursive: true });
1757
- await writeAtomic(path, `${JSON.stringify(next, null, 2)}
1758
- `, { mode: 420 });
1759
- return existed ? "updated" : "created";
1760
- }
1761
- async function removeMcpServer(path, format, name) {
1762
- const { config, comments } = await readConfig(path);
1763
- const key = serversKey(format);
1764
- const servers2 = { ...config[key] ?? {} };
1765
- if (!(name in servers2)) return "unchanged";
1766
- if (comments) return "manual";
1767
- delete servers2[name];
1768
- const next = { ...config, [key]: servers2 };
1769
- const leftover = Object.keys(next).filter((k) => k !== key && k !== "$schema");
1770
- if (Object.keys(servers2).length === 0 && leftover.length === 0) await rm4(path, { force: true });
1771
- else await writeAtomic(path, `${JSON.stringify(next, null, 2)}
1772
- `, { mode: 420 });
1773
- return "removed";
1774
- }
1775
- async function hasMcpServer(path, format, name) {
1776
- const { config } = await readConfig(path);
1777
- return name in (config[serversKey(format)] ?? {});
1778
- }
1779
- function parseJsonc(raw) {
1780
- let comments = false;
1781
- const json2 = raw.replace(/"(?:[^"\\]|\\.)*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (match) => {
1782
- if (match.startsWith('"')) return match;
1783
- comments = true;
1784
- return " ";
1785
- }).replace(/"(?:[^"\\]|\\.)*"|,(\s*[}\]])/g, (match, tail) => tail ?? match);
1786
- return { value: JSON.parse(json2), comments };
1787
- }
1788
- async function readConfig(path) {
1789
- try {
1790
- const { value, comments } = parseJsonc(await readFile7(path, "utf8"));
1791
- const config = value && typeof value === "object" ? value : {};
1792
- return { config, comments };
1793
- } catch (error) {
1794
- if (error.code === "ENOENT") return { config: {}, comments: false };
1795
- throw new Error(`Could not read ${path}: ${error.message}`);
1796
- }
1797
- }
1798
- async function readProjectMcpConfig(path) {
1799
- return (await readConfig(path)).config;
1800
- }
1801
-
1802
- // src/cli/commands/codex.ts
1803
- import { existsSync as existsSync2 } from "fs";
1804
- import { mkdir as mkdir6, readFile as readFile8 } from "fs/promises";
1805
- import { homedir } from "os";
1806
- import { dirname as dirname8, join as join9 } from "path";
1807
- function codexConfigPath(home = homedir()) {
1808
- return join9(home, ".codex", "config.toml");
1809
- }
1810
- async function readCodexConfig(path) {
1811
- if (!existsSync2(path)) return "";
1812
- return readFile8(path, "utf8");
1813
- }
1814
- async function writeCodexConfig(path, content) {
1815
- await mkdir6(dirname8(path), { recursive: true });
1816
- await writeAtomic(path, content, { mode: 384 });
1817
- }
1818
- async function installCodexEntry(options) {
1819
- const path = codexConfigPath();
1820
- const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
1821
- const current = await readCodexConfig(path);
1822
- const existed = hasServerBlock(current, serverKey);
1823
- const entry = buildEntry(options);
1824
- if (options.toolProfile)
1825
- entry.env = { ...entry.env, CODEBUDDY_TOOL_PROFILE: options.toolProfile };
1826
- const next = upsertServerBlock(current, serverKey, entry);
1827
- if (next !== current) await writeCodexConfig(path, next);
1828
- return { path, serverKey, created: !existed, changed: next !== current };
1829
- }
1830
- async function removeCodexEntry(serverKey) {
1831
- const path = codexConfigPath();
1832
- const current = await readCodexConfig(path);
1833
- const next = removeServerBlock(current, serverKey);
1834
- const removed = next !== current;
1835
- const trimmed = next.trimEnd();
1836
- if (removed) await writeCodexConfig(path, trimmed ? `${trimmed}
1837
- ` : "");
1838
- return { removed, path };
1839
- }
1840
- async function listCodexEntries() {
1841
- const path = codexConfigPath();
1842
- const config = await readCodexConfig(path);
1843
- const entries = Array.from(config.matchAll(/^\[mcp_servers\."([^"]+)"\]\s*$/gm)).flatMap((match) => match[1] ? [match[1]] : []).filter((key) => key === "codebuddy" || key.startsWith("codebuddy-")).map((key) => {
1844
- const block = getServerBlock(config, key);
1845
- return {
1846
- key,
1847
- namespace: readEnvValue(block, "CODEBUDDY_NAMESPACE"),
1848
- databaseUrl: readEnvValue(block, "DATABASE_URL")
1849
- };
1850
- });
1851
- return { path, entries };
1852
- }
1853
- function upsertServerBlock(config, serverKey, entry) {
1854
- const trimmed = removeServerBlock(config, serverKey).trimEnd();
1855
- const block = formatServerBlock(serverKey, entry);
1856
- return `${trimmed}${trimmed ? "\n\n" : ""}${block}
1857
- `;
1858
- }
1859
- function removeServerBlock(config, serverKey) {
1860
- const escaped = escapeRegExp(serverKey);
1861
- const end = String.raw`(?![\s\S])`;
1862
- const sectionPattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
1863
- const envPattern = String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
1864
- return config.replace(new RegExp(sectionPattern, "gm"), "").replace(new RegExp(envPattern, "gm"), "");
1865
- }
1866
- function getServerBlock(config, serverKey) {
1867
- const escaped = escapeRegExp(serverKey);
1868
- const end = String.raw`(?![\s\S])`;
1869
- const pattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})|^\[[^\]]+\]\s*$|${end})`;
1870
- const server = config.match(new RegExp(pattern, "m"))?.[0] ?? "";
1871
- const env = config.match(
1872
- new RegExp(
1873
- String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`,
1874
- "m"
1875
- )
1876
- )?.[0] ?? "";
1877
- return `${server}
1878
- ${env}`;
1879
- }
1880
- function hasServerBlock(config, serverKey) {
1881
- return new RegExp(String.raw`^\[mcp_servers\."${escapeRegExp(serverKey)}"\]\s*$`, "m").test(
1882
- config
1883
- );
1884
- }
1885
- function formatServerBlock(serverKey, entry) {
1886
- const lines = [
1887
- `[mcp_servers.${tomlKey(serverKey)}]`,
1888
- `command = ${tomlString(entry.command)}`,
1889
- `args = ${tomlArray(entry.args)}`
1890
- ];
1891
- if (entry.env && Object.keys(entry.env).length > 0) {
1892
- lines.push("", `[mcp_servers.${tomlKey(serverKey)}.env]`);
1893
- for (const [key, value] of Object.entries(entry.env).sort(
1894
- ([left], [right]) => left.localeCompare(right)
1895
- )) {
1896
- lines.push(`${key} = ${tomlString(value)}`);
1897
- }
1898
- }
1899
- return lines.join("\n");
1900
- }
1901
- function readEnvValue(block, key) {
1902
- return block.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
1903
- }
1904
- function tomlKey(key) {
1905
- return `"${key.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
1906
- }
1907
- function tomlString(value) {
1908
- return JSON.stringify(value);
1909
- }
1910
- function tomlArray(values) {
1911
- return `[${values.map(tomlString).join(", ")}]`;
1912
- }
1913
- function escapeRegExp(value) {
1914
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1915
- }
1916
-
1917
- // src/integrations/hermes-config.ts
1918
- import { mkdir as mkdir7, readFile as readFile9 } from "fs/promises";
1919
- import { homedir as homedir2 } from "os";
1920
- import { dirname as dirname9, join as join10 } from "path";
1921
- import { isDeepStrictEqual } from "util";
1922
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1923
- function hermesConfigPath(home = homedir2()) {
1924
- return join10(process.env.HERMES_HOME ?? join10(home, ".hermes"), "config.yaml");
1925
- }
1926
- async function readRaw(path) {
1927
- try {
1928
- return await readFile9(path, "utf8");
1929
- } catch (error) {
1930
- if (error.code === "ENOENT") return "";
1931
- throw error;
1932
- }
1933
- }
1934
- function parseConfig(raw) {
1935
- try {
1936
- const value = parseYaml(raw) ?? {};
1937
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1938
- } catch {
1939
- return null;
1940
- }
1941
- }
1942
- function servers(config) {
1943
- const value = config.mcp_servers;
1944
- return value && typeof value === "object" ? value : {};
1945
- }
1946
- var isItem = (line) => line.trim() !== "" && !line.trim().startsWith("#");
1947
- var indentOf = (line) => line.length - line.trimStart().length;
1948
- function editMcpServers(raw, name, entry) {
1949
- const lines = raw.split("\n");
1950
- const header = lines.findIndex((line) => /^mcp_servers:\s*(#.*)?$/.test(line));
1951
- const render = (indent2) => stringifyYaml({ [name]: entry }).trimEnd().split("\n").map((line) => `${" ".repeat(indent2)}${line}`);
1952
- if (header === -1) {
1953
- if (entry === null) return raw;
1954
- const body = raw === "" || raw.endsWith("\n") ? raw : `${raw}
1955
- `;
1956
- return `${body}mcp_servers:
1957
- ${render(2).join("\n")}
1958
- `;
1959
- }
1960
- let end = header + 1;
1961
- while (end < lines.length && !/^\S/.test(lines[end] ?? "")) end++;
1962
- const block = Array.from({ length: end - header - 1 }, (_, i) => header + 1 + i);
1963
- const items = block.filter((i) => isItem(lines[i] ?? ""));
1964
- const indent = items.length > 0 ? indentOf(lines[items[0]] ?? "") : 2;
1965
- const quoted = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1966
- const keyLine = new RegExp(`^ {${indent}}(?:${quoted}|"${quoted}"|'${quoted}'):\\s*(#.*)?$`);
1967
- const start = items.find((i) => keyLine.test(lines[i] ?? ""));
1968
- if (start === void 0) {
1969
- if (entry === null) return raw;
1970
- const at = (items.at(-1) ?? header) + 1;
1971
- lines.splice(at, 0, ...render(indent));
1972
- return lines.join("\n");
1973
- }
1974
- const next = block.find(
1975
- (i) => i > start && (lines[i] ?? "").trim() !== "" && indentOf(lines[i] ?? "") <= indent
1976
- );
1977
- let stop = next ?? end;
1978
- while (stop > start + 1 && (lines[stop - 1] ?? "").trim() === "") stop--;
1979
- lines.splice(start, stop - start, ...entry === null ? [] : render(indent));
1980
- if (entry === null && items.length > 0 && items.every((i) => i >= start && i < stop)) {
1981
- lines.splice(header, 1);
1982
- }
1983
- return lines.join("\n");
1984
- }
1985
- async function setHermesMcpServer(path, name, entry) {
1986
- const raw = await readRaw(path);
1987
- const before = parseConfig(raw);
1988
- if (!before) return "manual";
1989
- const current = servers(before)[name];
1990
- if (entry === null ? current === void 0 : isDeepStrictEqual(current, entry)) {
1991
- return "unchanged";
1992
- }
1993
- const nextServers = { ...servers(before) };
1994
- if (entry === null) delete nextServers[name];
1995
- else nextServers[name] = entry;
1996
- const { mcp_servers: _, ...rest } = before;
1997
- const expected = Object.keys(nextServers).length > 0 ? { ...rest, mcp_servers: nextServers } : rest;
1998
- const next = editMcpServers(raw, name, entry);
1999
- if (!isDeepStrictEqual(parseConfig(next), expected)) return "manual";
2000
- await mkdir7(dirname9(path), { recursive: true });
2001
- await writeAtomic(path, next, { mode: 384 });
2002
- return entry === null ? "removed" : current === void 0 ? "created" : "updated";
2003
- }
2004
- async function readHermesConfig(path) {
2005
- return parseConfig(await readRaw(path));
2006
- }
2007
- async function hasHermesMcpServer(path, name) {
2008
- const config = await readHermesConfig(path);
2009
- return config !== null && name in servers(config);
2010
- }
2011
-
2012
- // src/clients/global-mcp.ts
2013
- function globalMcpPath(target) {
2014
- return target === "codex" ? codexConfigPath() : hermesConfigPath();
2015
- }
2016
- async function setGlobalMcp(target, options) {
2017
- const { name, root, namespace, enabled } = options;
2018
- if (target === "hermes") {
2019
- return setHermesMcpServer(
2020
- hermesConfigPath(),
2021
- name,
2022
- enabled ? codebuddyMcpServer(root, namespace) : null
2023
- );
2024
- }
2025
- if (!enabled) return (await removeCodexEntry(name)).removed ? "removed" : "unchanged";
2026
- const result = await installCodexEntry({
2027
- namespace,
2028
- projectRoot: root,
2029
- serverName: name,
2030
- toolProfile: "core"
2031
- });
2032
- return result.created ? "created" : result.changed ? "updated" : "unchanged";
2033
- }
2034
- async function hasGlobalMcp(target, name) {
2035
- if (target === "hermes") return hasHermesMcpServer(hermesConfigPath(), name);
2036
- return (await listCodexEntries()).entries.some((entry) => entry.key === name);
2037
- }
2038
-
2039
- // src/clients/install.ts
2040
- async function installClient(options) {
2041
- const adapter = CLIENT_ADAPTERS[options.client];
2042
- if (!supportedModes(adapter).includes(options.mode)) {
2043
- throw new Error(
2044
- `${adapter.name} does not support ${MODE_LABELS[options.mode]} mode: ${adapter.gap ?? "no lifecycle hooks"}.`
2045
- );
2046
- }
2047
- return apply(options, options.mode, options.mode !== "lean");
2048
- }
2049
- async function ensureIndex(root) {
2050
- const target = join11(root, ".codebuddy", "cache", "index.json");
2051
- const existed = await access3(target).then(
2052
- () => true,
2053
- () => false
2054
- );
2055
- if (!existed) await indexRepository({ repositoryRoot: root });
2056
- return { surface: "index", target, action: existed ? "unchanged" : "built" };
2057
- }
2058
- async function uninstallClient(options) {
2059
- return apply(options, "mcp-only", false);
2060
- }
2061
- async function apply(options, mode, keepMcp) {
2062
- const root = resolve9(options.projectRoot ?? process.cwd());
2063
- const adapter = CLIENT_ADAPTERS[options.client];
2064
- const steps = [];
2065
- const notes = [];
2066
- const namespace = options.namespace ?? await resolveNamespaceFrom(root);
2067
- const name = `codebuddy-${namespace}`;
2068
- const shared = sharedRulesReaders(options.client);
2069
- const liveSiblings = mode === "mcp-only" || mode === "lean" ? await installedWithMcp(root, shared, namespace) : [];
2070
- if (liveSiblings.length > 0) {
2071
- const path = join11(root, adapter.rulesFile);
2072
- steps.push({ surface: "rules", target: path, action: "kept" });
2073
- notes.push(
2074
- `Kept the CodeBuddy block in ${adapter.rulesFile}: still used by ${liveSiblings.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
2075
- );
2076
- } else {
2077
- const hooked = mode === "agent" ? await hookedRulesSiblings(root, options.client) : [];
2078
- const rules = await installWorkflowRules({
2079
- client: options.client,
2080
- projectRoot: root,
2081
- // Lean sends no rules: the block would cost tokens every turn and shadow AGENTS.md.
2082
- mode: mode === "lean" ? "mcp-only" : hooked.length > 0 ? "hooks" : mode
2083
- });
2084
- steps.push({ surface: "rules", target: rules.path, action: rules.action });
2085
- if (hooked.length > 0) {
2086
- notes.push(
2087
- `Kept ${adapter.rulesFile} hook-driven: ${hooked.map((id) => CLIENT_ADAPTERS[id].name).join(", ")} hooks are installed; agents without hooks follow its agent-driven fallback.`
2088
- );
2089
- }
2090
- }
2091
- if (shared.length > 0) {
2092
- notes.push(
2093
- `${adapter.rulesFile} is also read by ${shared.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
2094
- );
2095
- }
2096
- const hooks = await setHooks(
2097
- root,
2098
- mode === "hooks" || mode === "lean",
2099
- options.client,
2100
- mode === "lean" ? "lean" : "full"
2101
- );
2102
- if (hooks) steps.push(hooks);
2103
- if (mode === "lean") steps.push(await ensureIndex(root));
2104
- if ("file" in adapter.mcp) {
2105
- const path = await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format);
2106
- const action = keepMcp ? await upsertMcpServer(path, adapter.mcp.format, name, codebuddyMcpServer(root, namespace)) : await removeMcpServer(path, adapter.mcp.format, name);
2107
- steps.push({ surface: "mcp", target: path, action });
2108
- if (action === "manual") {
2109
- notes.push(
2110
- `${path} has comments CodeBuddy will not rewrite; ${keepMcp ? "add" : "remove"} the "${name}" entry under "mcp" by hand${keepMcp ? `: ${JSON.stringify(formatEntry(adapter.mcp.format, codebuddyMcpServer(root, namespace)))}` : ""}.`
2111
- );
2112
- }
2113
- } else if (options.global) {
2114
- const target = globalMcpPath(adapter.mcp.global);
2115
- const action = await setGlobalMcp(adapter.mcp.global, {
2116
- name,
2117
- root,
2118
- namespace,
2119
- enabled: keepMcp
2120
- });
2121
- steps.push({ surface: "mcp", target, action });
2122
- if (action === "manual") {
2123
- notes.push(
2124
- `${target} is not a YAML map CodeBuddy can edit safely; ${keepMcp ? "add" : "remove"} the "${name}" entry under mcp_servers by hand.`
2125
- );
2126
- }
2127
- } else {
2128
- notes.push(
2129
- `MCP config is global; ${keepMcp ? "add" : "remove"} it with --global, or: ${adapter.mcp.manual}.`
2130
- );
2131
- }
2132
- if (adapter.note && mode !== "mcp-only") notes.push(`${adapter.name}: ${adapter.note}.`);
2133
- return { client: options.client, steps, notes };
2134
- }
2135
- async function installedWithMcp(root, clients, namespace) {
2136
- const live = [];
2137
- for (const id of clients) {
2138
- if ((await detectClient(id, root, namespace)).mcp) live.push(id);
681
+ return {
682
+ first: estimateTokens(first.text),
683
+ later: estimateTokens(later.text),
684
+ lean: { first: estimateTokens(leanFirst.text), later: estimateTokens(leanLater.text) }
685
+ };
686
+ } finally {
687
+ await rm(scratch, { recursive: true, force: true });
2139
688
  }
2140
- return live;
2141
689
  }
2142
- async function setHooks(root, enabled, client = "claude", variant = "full") {
2143
- const result = await setClientHooks(root, client, enabled, variant);
2144
- if (!result) return null;
2145
- const action = result.changed ? enabled ? "installed" : "removed" : "unchanged";
2146
- return { surface: "hooks", target: result.target, action };
2147
- }
2148
- async function detectClient(client, projectRoot = process.cwd(), namespace) {
2149
- const root = resolve9(projectRoot);
2150
- const adapter = CLIENT_ADAPTERS[client];
2151
- const rulesMode = await readManagedMode(join11(root, adapter.rulesFile));
2152
- const hooks = await clientHooksInstalled(root, client);
2153
- const name = `codebuddy-${namespace ?? await resolveNamespaceFrom(root)}`;
2154
- const mcp = "file" in adapter.mcp ? await hasMcpServer(
2155
- await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format),
2156
- adapter.mcp.format,
2157
- name
2158
- ) : await hasGlobalMcp(adapter.mcp.global, name).catch(() => false);
2159
- const warnings = [];
2160
- if (hooks && rulesMode === "agent") {
2161
- warnings.push(
2162
- `hooks are installed but ${adapter.rulesFile} is agent-driven: retrieval and capture run twice. Run \`codebuddy clients install ${client} --mode hooks\`.`
2163
- );
2164
- }
2165
- if (rulesMode === "hooks" && hooks === false) {
2166
- warnings.push(
2167
- `${adapter.rulesFile} is hook-driven but hooks are not installed: turns are not captured.`
2168
- );
2169
- }
2170
- const effectiveRules = hooks === null && rulesMode === "hooks" ? "agent" : rulesMode;
2171
- const mode = hooks ? await clientHooksVariant(root, client) === "lean" ? "lean" : "hooks" : effectiveRules ?? (mcp ? "mcp-only" : "none");
2172
- return { client, mode, rulesMode, hooks, mcp, warnings };
690
+ async function measureClientCosts(projectRoot = process.cwd(), namespace) {
691
+ const root = resolve2(projectRoot);
692
+ const ns = namespace ?? await resolveNamespaceFrom(root);
693
+ const mcpSchema = await mcpSchemaTokens(root, ns);
694
+ const block = await hookBlockTokens(root, ns);
695
+ return CLIENT_IDS.flatMap((client) => {
696
+ const adapter = CLIENT_ADAPTERS[client];
697
+ return supportedModes(adapter).flatMap((mode) => {
698
+ if (mode === "mcp-only") return [];
699
+ if (mode === "lean") {
700
+ const later2 = adapter.ephemeralContext ? block.lean.first : block.lean.later;
701
+ return [
702
+ { client, mode, rules: 0, mcpSchema: 0, firstBlock: block.lean.first, laterBlock: later2 }
703
+ ];
704
+ }
705
+ const hooks = mode === "hooks";
706
+ const later = adapter.ephemeralContext ? block.first : block.later;
707
+ return [
708
+ {
709
+ client,
710
+ mode,
711
+ rules: estimateTokens(renderManagedBlock(client, mode)),
712
+ mcpSchema: adapter.defersMcpSchemas ? mcpSchema.listing : mcpSchema.full,
713
+ firstBlock: hooks ? block.first : 0,
714
+ laterBlock: hooks ? later : 0
715
+ }
716
+ ];
717
+ });
718
+ });
2173
719
  }
2174
720
 
2175
721
  // src/clients/verify.ts
2176
722
  import { execFile } from "child_process";
2177
- import { readFile as readFile10, realpath } from "fs/promises";
2178
- import { join as join12, resolve as resolve10 } from "path";
723
+ import { readFile, realpath } from "fs/promises";
724
+ import { join as join3, resolve as resolve3 } from "path";
2179
725
  import { pathToFileURL } from "url";
2180
726
  var runHost = (bin, args, cwd) => new Promise((done) => {
2181
727
  execFile(
@@ -2185,9 +731,9 @@ var runHost = (bin, args, cwd) => new Promise((done) => {
2185
731
  (error, stdout) => done({ ok: !error, stdout: String(stdout ?? "") })
2186
732
  );
2187
733
  });
2188
- var pass = (name, detail) => ({ name, status: "pass", detail });
2189
- var fail = (name, detail) => ({ name, status: "fail", detail });
2190
- var skip = (name, detail) => ({ name, status: "skip", detail });
734
+ var pass = (name2, detail) => ({ name: name2, status: "pass", detail });
735
+ var fail = (name2, detail) => ({ name: name2, status: "fail", detail });
736
+ var skip = (name2, detail) => ({ name: name2, status: "skip", detail });
2191
737
  function json(text3) {
2192
738
  try {
2193
739
  return JSON.parse(text3);
@@ -2196,48 +742,48 @@ function json(text3) {
2196
742
  }
2197
743
  }
2198
744
  var CONFORMANCE = {
2199
- claude: async ({ root, name, run }) => {
745
+ claude: async ({ root, name: name2, run }) => {
2200
746
  const hooks = await clientHooksInstalled(root, "claude") ? pass("hooks", ".claude/settings.json has the CodeBuddy hooks") : skip("hooks", "not installed in this repo");
2201
- const mcp = await run("claude", ["mcp", "get", name], root);
747
+ const mcp = await run("claude", ["mcp", "get", name2], root);
2202
748
  return [
2203
749
  hooks,
2204
- mcp.ok ? pass("mcp", `claude mcp get ${name}: ${mcp.stdout.match(/Scope: (.+)/)?.[1] ?? "found"}`) : skip("mcp", `claude mcp get ${name}: not configured`)
750
+ mcp.ok ? pass("mcp", `claude mcp get ${name2}: ${mcp.stdout.match(/Scope: (.+)/)?.[1] ?? "found"}`) : skip("mcp", `claude mcp get ${name2}: not configured`)
2205
751
  ];
2206
752
  },
2207
- opencode: async ({ root, name, run }) => {
753
+ opencode: async ({ root, name: name2, run }) => {
2208
754
  const result = await run("opencode", ["debug", "config"], root);
2209
755
  const config = json(result.stdout);
2210
756
  if (!config) return [fail("config", "opencode debug config returned no JSON")];
2211
757
  const installed = await clientHooksInstalled(root, "opencode");
2212
758
  const href = pathToFileURL(
2213
- join12(await realpath(root).catch(() => root), ".opencode", "plugins", "codebuddy.js")
759
+ join3(await realpath(root).catch(() => root), ".opencode", "plugins", "codebuddy.js")
2214
760
  ).href;
2215
761
  const loaded = config.plugin?.includes(href) === true;
2216
762
  return [
2217
763
  installed ? loaded ? pass("plugin", "opencode debug config loads .opencode/plugins/codebuddy.js") : fail("plugin", "plugin file is installed but opencode does not load it") : skip("plugin", "not installed in this repo"),
2218
- config.mcp?.[name] ? pass("mcp", `opencode resolves the ${name} MCP entry`) : skip("mcp", `no ${name} entry in the resolved config`)
764
+ config.mcp?.[name2] ? pass("mcp", `opencode resolves the ${name2} MCP entry`) : skip("mcp", `no ${name2} entry in the resolved config`)
2219
765
  ];
2220
766
  },
2221
- codex: async ({ root, name, run }) => {
767
+ codex: async ({ root, name: name2, run }) => {
2222
768
  const result = await run("codex", ["mcp", "list", "--json"], root);
2223
- const servers2 = json(result.stdout);
2224
- if (!Array.isArray(servers2)) return [fail("mcp", "codex mcp list --json returned no JSON")];
769
+ const servers = json(result.stdout);
770
+ if (!Array.isArray(servers)) return [fail("mcp", "codex mcp list --json returned no JSON")];
2225
771
  return [
2226
- servers2.some((server) => server.name === name) ? pass("mcp", `codex mcp list has ${name}`) : skip("mcp", `no ${name} entry (clients install codex --global adds it)`)
772
+ servers.some((server) => server.name === name2) ? pass("mcp", `codex mcp list has ${name2}`) : skip("mcp", `no ${name2} entry (clients install codex --global adds it)`)
2227
773
  ];
2228
774
  },
2229
- hermes: async ({ root, name, run, env }) => {
775
+ hermes: async ({ root, name: name2, run, env }) => {
2230
776
  const checks = [];
2231
777
  const listed = await run("hermes", ["mcp", "list"], root);
2232
778
  checks.push(
2233
- listed.ok && listed.stdout.includes(name) ? pass("mcp", `hermes mcp list has ${name}`) : skip("mcp", `no ${name} entry (clients install hermes --global adds it)`)
779
+ listed.ok && listed.stdout.includes(name2) ? pass("mcp", `hermes mcp list has ${name2}`) : skip("mcp", `no ${name2} entry (clients install hermes --global adds it)`)
2234
780
  );
2235
781
  if (!await hermesPluginInstalled(root)) {
2236
782
  checks.push(skip("plugin", "not installed in this repo"));
2237
783
  return checks;
2238
784
  }
2239
785
  const projectPlugins = /^(1|true|yes|on)$/i.test(env.HERMES_ENABLE_PROJECT_PLUGINS ?? "") || /^\s*HERMES_ENABLE_PROJECT_PLUGINS\s*=\s*["']?(1|true|yes|on)/im.test(
2240
- await readFile10(join12(hermesConfigPath(), "..", ".env"), "utf8").catch(() => "")
786
+ await readFile(join3(hermesConfigPath(), "..", ".env"), "utf8").catch(() => "")
2241
787
  );
2242
788
  const plugins = (await readHermesConfig(hermesConfigPath()))?.plugins;
2243
789
  const enabled = Array.isArray(plugins?.enabled) && plugins.enabled.includes("codebuddy");
@@ -2249,7 +795,7 @@ var CONFORMANCE = {
2249
795
  }
2250
796
  };
2251
797
  async function verifyClient(client, options = {}) {
2252
- const root = resolve10(options.root ?? process.cwd());
798
+ const root = resolve3(options.root ?? process.cwd());
2253
799
  const run = options.run ?? runHost;
2254
800
  const adapter = CLIENT_ADAPTERS[client];
2255
801
  const version = await run(adapter.host.bin, ["--version"], root);
@@ -2268,9 +814,9 @@ async function verifyClient(client, options = {}) {
2268
814
  checks: [skip("host", `${adapter.host.bin} not found or printed no version`)]
2269
815
  };
2270
816
  }
2271
- const name = `codebuddy-${options.namespace ?? await resolveNamespaceFrom(root)}`;
817
+ const name2 = `codebuddy-${options.namespace ?? await resolveNamespaceFrom(root)}`;
2272
818
  const conformance = CONFORMANCE[client];
2273
- const checks = conformance ? await conformance({ root, name, run, env: options.env ?? process.env }).catch(
819
+ const checks = conformance ? await conformance({ root, name: name2, run, env: options.env ?? process.env }).catch(
2274
820
  (error) => [fail("conformance", error.message)]
2275
821
  ) : [];
2276
822
  return {
@@ -3008,12 +1554,12 @@ data: ${data}
3008
1554
  heartbeat.unref();
3009
1555
  return {
3010
1556
  url: `http://127.0.0.1:${port}/`,
3011
- close: () => new Promise((resolve13) => {
1557
+ close: () => new Promise((resolve6) => {
3012
1558
  clearInterval(heartbeat);
3013
1559
  unwatchFile(path);
3014
1560
  for (const client of clients) client.end();
3015
1561
  server.closeAllConnections();
3016
- server.close(() => resolve13());
1562
+ server.close(() => resolve6());
3017
1563
  })
3018
1564
  };
3019
1565
  }
@@ -3021,11 +1567,11 @@ async function listen(server, port) {
3021
1567
  for (let attempt = 0; attempt < 10; attempt += 1) {
3022
1568
  const candidate = port === 0 ? 0 : port + attempt;
3023
1569
  try {
3024
- await new Promise((resolve13, reject) => {
1570
+ await new Promise((resolve6, reject) => {
3025
1571
  server.once("error", reject);
3026
1572
  server.listen(candidate, "127.0.0.1", () => {
3027
1573
  server.off("error", reject);
3028
- resolve13();
1574
+ resolve6();
3029
1575
  });
3030
1576
  });
3031
1577
  return server.address().port;
@@ -3063,8 +1609,8 @@ function registerDashboardCommand(program, runSafely2) {
3063
1609
  );
3064
1610
  console.log(pc4.dim(" Ctrl+C to stop."));
3065
1611
  if (opts.open) openBrowser(dashboard.url);
3066
- await new Promise((resolve13) => {
3067
- const stop = () => void dashboard.close().then(resolve13);
1612
+ await new Promise((resolve6) => {
1613
+ const stop = () => void dashboard.close().then(resolve6);
3068
1614
  process.once("SIGINT", stop);
3069
1615
  process.once("SIGTERM", stop);
3070
1616
  });
@@ -3076,20 +1622,20 @@ function registerDashboardCommand(program, runSafely2) {
3076
1622
  import pc5 from "picocolors";
3077
1623
 
3078
1624
  // src/eval/runner.ts
3079
- import { readFile as readFile11 } from "fs/promises";
3080
- import { dirname as dirname10, isAbsolute, join as join13, resolve as resolve11 } from "path";
1625
+ import { readFile as readFile2 } from "fs/promises";
1626
+ import { dirname as dirname2, isAbsolute, join as join4, resolve as resolve4 } from "path";
3081
1627
  async function runEvaluation(input = {}) {
3082
- const datasetPath = resolve11(
3083
- input.datasetPath ?? join13(process.cwd(), "evals", "change-readiness.json")
1628
+ const datasetPath = resolve4(
1629
+ input.datasetPath ?? join4(process.cwd(), "evals", "change-readiness.json")
3084
1630
  );
3085
- const dataset = JSON.parse(await readFile11(datasetPath, "utf8"));
1631
+ const dataset = JSON.parse(await readFile2(datasetPath, "utf8"));
3086
1632
  if (dataset.version !== 1 || !Array.isArray(dataset.cases)) {
3087
1633
  throw new Error(`Unsupported evaluation dataset: ${datasetPath}`);
3088
1634
  }
3089
- const datasetDirectory = dirname10(datasetPath);
1635
+ const datasetDirectory = dirname2(datasetPath);
3090
1636
  const results = await Promise.all(
3091
1637
  dataset.cases.map(async (evaluationCase) => {
3092
- const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture : resolve11(input.repositoryRoot ?? datasetDirectory, evaluationCase.fixture);
1638
+ const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture : resolve4(input.repositoryRoot ?? datasetDirectory, evaluationCase.fixture);
3093
1639
  const report = await buildChangeReport({
3094
1640
  repositoryRoot: fixtureRoot,
3095
1641
  namespace: `eval:${evaluationCase.id}`,
@@ -3253,6 +1799,7 @@ function registerHooksCommands(program, runSafely2) {
3253
1799
  );
3254
1800
  if (result.saved.length > 0) syncInBackground(hookRoot(input.cwd));
3255
1801
  }
1802
+ await flushTelemetry(hookRoot(input.cwd));
3256
1803
  });
3257
1804
  });
3258
1805
  hooks.command("sync").description(
@@ -3373,7 +1920,7 @@ async function llmStatusReport(root, env = process.env) {
3373
1920
  };
3374
1921
  }
3375
1922
  function formatLlmStatus(report) {
3376
- const features = report.features ? Object.entries(report.features).map(([name, on]) => `${name} ${on ? "on" : "off"}`).join(", ") : "-";
1923
+ const features = report.features ? Object.entries(report.features).map(([name2, on]) => `${name2} ${on ? "on" : "off"}`).join(", ") : "-";
3377
1924
  return [
3378
1925
  `status: ${report.enabled ? "on" : `off (${report.reason})`}`,
3379
1926
  `provider: ${report.provider}${report.model ? ` ${report.model}` : ""}`,
@@ -3579,7 +2126,7 @@ async function runInitWizard() {
3579
2126
  if (result.code === 0) {
3580
2127
  dockerSpinner.stop("Postgres container started.");
3581
2128
  for (let attempt = 0; attempt < 10 && !connected; attempt++) {
3582
- await new Promise((resolve13) => setTimeout(resolve13, 500));
2129
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
3583
2130
  connected = await pingOnce(databaseUrl);
3584
2131
  }
3585
2132
  if (!connected) {
@@ -3871,8 +2418,8 @@ function openEditor(path) {
3871
2418
  }
3872
2419
 
3873
2420
  // src/cli/commands/models.ts
3874
- import { existsSync as existsSync3 } from "fs";
3875
- import { join as join14 } from "path";
2421
+ import { existsSync } from "fs";
2422
+ import { join as join5 } from "path";
3876
2423
  import { spinner as spinner2 } from "@clack/prompts";
3877
2424
  import pc13 from "picocolors";
3878
2425
 
@@ -4157,8 +2704,8 @@ function keyPresent(provider, apiKey) {
4157
2704
  return Boolean(process.env[KEY_ENV2[provider]] || provider === "huggingface" && apiKey);
4158
2705
  }
4159
2706
  function localModelDownloaded(id, revision) {
4160
- const dir = join14(modelCacheDir(), id, ...revision ? [revision] : []);
4161
- return existsSync3(join14(dir, "onnx", "model_quantized.onnx"));
2707
+ const dir = join5(modelCacheDir(), id, ...revision ? [revision] : []);
2708
+ return existsSync(join5(dir, "onnx", "model_quantized.onnx"));
4162
2709
  }
4163
2710
  async function vectorCounts(postgresUrl, namespace, space) {
4164
2711
  const client = createDatabaseClient({ postgresUrl, maxConnections: 1, connectTimeoutSeconds: 3 });
@@ -4501,14 +3048,14 @@ function renderPlan(plan) {
4501
3048
  }
4502
3049
  function openEditor2(path) {
4503
3050
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
4504
- return new Promise((resolve13, reject) => {
3051
+ return new Promise((resolve6, reject) => {
4505
3052
  const child = spawn5(editor, [path], { stdio: "inherit" });
4506
3053
  child.on("error", reject);
4507
- child.on("close", () => resolve13());
3054
+ child.on("close", () => resolve6());
4508
3055
  });
4509
3056
  }
4510
3057
  function runGit(args) {
4511
- return new Promise((resolve13, reject) => {
3058
+ return new Promise((resolve6, reject) => {
4512
3059
  const child = spawn5("git", args, { stdio: ["ignore", "pipe", "pipe"] });
4513
3060
  let stdout = "";
4514
3061
  let stderr = "";
@@ -4519,7 +3066,7 @@ function runGit(args) {
4519
3066
  stderr += String(c);
4520
3067
  });
4521
3068
  child.on("error", reject);
4522
- child.on("close", (code) => resolve13({ code: code ?? 0, stdout, stderr }));
3069
+ child.on("close", (code) => resolve6({ code: code ?? 0, stdout, stderr }));
4523
3070
  });
4524
3071
  }
4525
3072
  function registerPlanCommands(program, runSafely2) {
@@ -4833,8 +3380,8 @@ function severityBadge2(severity) {
4833
3380
  }
4834
3381
 
4835
3382
  // src/cli/commands/symbols.ts
4836
- import { readFile as readFile12 } from "fs/promises";
4837
- import { resolve as resolve12 } from "path";
3383
+ import { readFile as readFile3 } from "fs/promises";
3384
+ import { resolve as resolve5 } from "path";
4838
3385
  import pc18 from "picocolors";
4839
3386
  function registerSymbolsCommand(program, runSafely2) {
4840
3387
  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) => {
@@ -4843,7 +3390,7 @@ function registerSymbolsCommand(program, runSafely2) {
4843
3390
  console.log(pc18.dim(`no symbol extraction for ${path} (unsupported language)`));
4844
3391
  return;
4845
3392
  }
4846
- const content = await readFile12(resolve12(path), "utf8");
3393
+ const content = await readFile3(resolve5(path), "utf8");
4847
3394
  const table = extractSymbolTable(content, path);
4848
3395
  if (opts.json) {
4849
3396
  console.log(JSON.stringify({ path, symbols: table }, null, 2));
@@ -4865,48 +3412,712 @@ ${table.length} symbol(s)`));
4865
3412
  });
4866
3413
  }
4867
3414
 
3415
+ // src/cli/commands/telemetry.ts
3416
+ import pc19 from "picocolors";
3417
+
3418
+ // src/telemetry/collector.ts
3419
+ import { createHash, timingSafeEqual } from "crypto";
3420
+ import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4 } from "fs/promises";
3421
+ import { createServer as createServer2 } from "http";
3422
+ import { join as join6 } from "path";
3423
+ import { z as z2 } from "zod";
3424
+
3425
+ // src/telemetry/admin-page.ts
3426
+ var ADMIN_HTML = `<!doctype html>
3427
+ <html lang="en">
3428
+ <head>
3429
+ <meta charset="utf-8">
3430
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3431
+ <title>CodeBuddy Usage</title>
3432
+ <style>
3433
+ :root {
3434
+ --bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --line: #e2e1dc;
3435
+ --bar: #2a78d6; --bar-soft: #e3edfa; --bad: #b42318;
3436
+ --sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
3437
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
3438
+ }
3439
+ @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 } }
3442
+ * { box-sizing: border-box; }
3443
+ 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; }
3445
+ .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; }
3450
+ .spacer { flex: 1; }
3451
+ .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; }
3453
+ .seg button + button { border-left: 1px solid var(--line); }
3454
+ .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; }
3458
+ .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; }
3472
+ .row .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
3473
+ .scroll { overflow-x: auto; }
3474
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
3475
+ 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; }
3477
+ tr:last-child td { border-bottom: 0; }
3478
+ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
3479
+ .bad { color: var(--bad); }
3480
+ .muted { color: var(--muted); }
3481
+ .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); }
3487
+ </style>
3488
+ </head>
3489
+ <body>
3490
+ <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>
3499
+ </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>
3515
+ </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>
3518
+ </div>
3519
+ <div class="tip" id="tip"></div>
3520
+ <script>
3521
+ (function () {
3522
+ 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;
3527
+ var tip = document.getElementById("tip");
3528
+
3529
+ function esc(value) {
3530
+ return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
3531
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
3532
+ });
3533
+ }
3534
+ function num(n) { return Math.round(n).toLocaleString(); }
3535
+ 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"; }
3538
+
3539
+ 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>";
3542
+ }
3543
+
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;
3548
+ var max = Math.max.apply(null, points.map(function (p) { return p.value; }).concat([1]));
3549
+ var step = (w - left) / points.length;
3550
+ var bw = Math.max(1, step - 2);
3551
+ var out = '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" role="img" aria-label="' + esc(label) + '">';
3552
+ [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>";
3556
+ });
3557
+ 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 + '"/>';
3563
+ });
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>";
3566
+ return out + "</svg>";
3567
+ }
3568
+
3569
+ function rows(items, total) {
3570
+ if (!items.length) return '<div class="muted">No data yet.</div>';
3571
+ return items.slice(0, 8).map(function (item) {
3572
+ 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>';
3575
+ }).join("");
3576
+ }
3577
+
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 }; });
3581
+ }
3582
+ function npmTotal(n) {
3583
+ if (!npm || !npm.downloads) return null;
3584
+ return npm.downloads.slice(-n).reduce(function (sum, d) { return sum + d.downloads; }, 0);
3585
+ }
3586
+
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>";
3620
+ }).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>";
3628
+ }).join("") + "</tbody></table>"
3629
+ : '<div class="empty">No events yet.</div>';
3630
+ }
3631
+
3632
+ function load() {
3633
+ fetch(base + "api/summary?days=" + days).then(function (r) { return r.json(); }).then(function (s) { summary = s; render(); });
3634
+ }
3635
+ fetch(base + "api/npm").then(function (r) { return r.json(); }).then(function (n) { npm = n; render(); });
3636
+
3637
+ document.getElementById("range").addEventListener("click", function (event) {
3638
+ var button = event.target.closest("button");
3639
+ if (!button) return;
3640
+ days = Number(button.dataset.days);
3641
+ document.querySelectorAll("#range button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
3642
+ load();
3643
+ });
3644
+ document.getElementById("csv").addEventListener("click", function () {
3645
+ 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) {
3648
+ return cols.map(function (c) { return '"' + String(u[c]).replace(/"/g, '""') + '"'; }).join(",");
3649
+ }));
3650
+ var link = document.createElement("a");
3651
+ link.href = URL.createObjectURL(new Blob([lines.join("\\n")], { type: "text/csv" }));
3652
+ link.download = "codebuddy-users-" + new Date().toISOString().slice(0, 10) + ".csv";
3653
+ link.click();
3654
+ });
3655
+ document.addEventListener("mousemove", function (event) {
3656
+ var text = event.target.getAttribute && event.target.getAttribute("data-tip");
3657
+ if (!text) { tip.style.display = "none"; return; }
3658
+ tip.textContent = text;
3659
+ 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";
3663
+ });
3664
+ load();
3665
+ setInterval(load, 60000);
3666
+ })();
3667
+ </script>
3668
+ </body>
3669
+ </html>
3670
+ `;
3671
+
3672
+ // src/telemetry/collector.ts
3673
+ var MAX_BODY_BYTES = 256 * 1024;
3674
+ var INSTALL_BATCHES_PER_HOUR = 30;
3675
+ var GLOBAL_BATCHES_PER_MINUTE = 600;
3676
+ var NPM_PACKAGE = "@orquentra/codebuddy";
3677
+ var name = z2.string().regex(/^[A-Za-z][A-Za-z0-9_:.-]{0,63}$/);
3678
+ var short = z2.string().max(64);
3679
+ var batchSchema = z2.object({
3680
+ installId: z2.string().uuid(),
3681
+ project: z2.string().regex(/^[0-9a-f]{16}$/),
3682
+ version: short,
3683
+ platform: short,
3684
+ arch: short,
3685
+ node: short,
3686
+ clients: z2.array(name).max(10),
3687
+ events: z2.array(
3688
+ z2.object({
3689
+ ts: z2.string().datetime(),
3690
+ kind: name,
3691
+ name,
3692
+ ms: z2.number().int().min(0).max(864e5),
3693
+ ok: z2.boolean(),
3694
+ tokensIn: z2.number().int().min(0).max(1e7).optional(),
3695
+ tokensOut: z2.number().int().min(0).max(1e7).optional(),
3696
+ session: z2.string().regex(/^[0-9a-f]{16}$/).optional()
3697
+ })
3698
+ ).min(1).max(500)
3699
+ });
3700
+ async function startCollector(options) {
3701
+ const { dataDir } = options;
3702
+ await mkdir2(dataDir, { recursive: true });
3703
+ let summaryCache;
3704
+ let npmCache;
3705
+ const perInstall = /* @__PURE__ */ new Map();
3706
+ let globalCount = 0;
3707
+ const resetInstalls = setInterval(() => perInstall.clear(), 36e5);
3708
+ const resetGlobal = setInterval(() => {
3709
+ globalCount = 0;
3710
+ }, 6e4);
3711
+ resetInstalls.unref();
3712
+ resetGlobal.unref();
3713
+ const ingest = createServer2(async (request, response) => {
3714
+ const url = new URL(request.url ?? "/", "http://localhost");
3715
+ if (url.pathname !== "/v1/events") {
3716
+ response.writeHead(404).end();
3717
+ return;
3718
+ }
3719
+ if (request.method !== "POST") {
3720
+ response.writeHead(405).end();
3721
+ return;
3722
+ }
3723
+ if (++globalCount > GLOBAL_BATCHES_PER_MINUTE) {
3724
+ response.writeHead(429).end();
3725
+ return;
3726
+ }
3727
+ const body = await readBody(request);
3728
+ if (body === null) {
3729
+ response.writeHead(413, { connection: "close" }).end();
3730
+ return;
3731
+ }
3732
+ let batch;
3733
+ try {
3734
+ batch = batchSchema.parse(JSON.parse(body));
3735
+ } catch {
3736
+ response.writeHead(400).end();
3737
+ return;
3738
+ }
3739
+ const seen = (perInstall.get(batch.installId) ?? 0) + 1;
3740
+ perInstall.set(batch.installId, seen);
3741
+ if (seen > INSTALL_BATCHES_PER_HOUR) {
3742
+ response.writeHead(429).end();
3743
+ return;
3744
+ }
3745
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
3746
+ const { events, ...meta } = batch;
3747
+ const lines = events.map((event) => JSON.stringify({ ...meta, ...event, receivedAt }));
3748
+ await appendFile(
3749
+ join6(dataDir, `events-${receivedAt.slice(0, 10)}.jsonl`),
3750
+ `${lines.join("\n")}
3751
+ `
3752
+ );
3753
+ response.writeHead(204).end();
3754
+ summaryCache = void 0;
3755
+ });
3756
+ const admin = createServer2(async (request, response) => {
3757
+ const host = (request.headers.host ?? "").replace(/:\d+$/, "");
3758
+ if (host !== "127.0.0.1" && host !== "localhost" && !host.endsWith(".ts.net")) {
3759
+ response.writeHead(403, { "content-type": "text/plain" }).end("Forbidden host");
3760
+ return;
3761
+ }
3762
+ if (options.adminPassword && !passwordMatches(request, options.adminPassword)) {
3763
+ response.writeHead(401, {
3764
+ "www-authenticate": 'Basic realm="CodeBuddy admin", charset="UTF-8"',
3765
+ "content-type": "text/plain"
3766
+ }).end("Password required");
3767
+ return;
3768
+ }
3769
+ if (request.method !== "GET") {
3770
+ response.writeHead(405).end();
3771
+ return;
3772
+ }
3773
+ const url = new URL(request.url ?? "/", "http://localhost");
3774
+ url.pathname = url.pathname.replace(/^\/admin(?=\/|$)/, "") || "/";
3775
+ const json2 = (value) => {
3776
+ response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
3777
+ response.end(JSON.stringify(value));
3778
+ };
3779
+ if (url.pathname === "/") {
3780
+ response.writeHead(200, {
3781
+ "content-type": "text/html; charset=utf-8",
3782
+ "cache-control": "no-store"
3783
+ });
3784
+ response.end(ADMIN_HTML);
3785
+ return;
3786
+ }
3787
+ if (url.pathname === "/api/summary") {
3788
+ const days = Math.min(365, Math.max(1, Number(url.searchParams.get("days")) || 30));
3789
+ if (!summaryCache || summaryCache.days !== days || Date.now() - summaryCache.at > 3e4) {
3790
+ summaryCache = {
3791
+ at: Date.now(),
3792
+ days,
3793
+ value: summarize(await readEvents(dataDir, days), days)
3794
+ };
3795
+ }
3796
+ json2(summaryCache.value);
3797
+ return;
3798
+ }
3799
+ if (url.pathname === "/api/npm") {
3800
+ if (!npmCache || Date.now() - npmCache.at > 36e5) {
3801
+ npmCache = { at: Date.now(), value: await npmDownloads(options.fetchImpl ?? fetch) };
3802
+ }
3803
+ json2(npmCache.value);
3804
+ return;
3805
+ }
3806
+ response.writeHead(404, { "content-type": "text/plain" }).end("Not found");
3807
+ });
3808
+ const ingestPort = await listen2(ingest, options.ingestPort ?? 8787);
3809
+ const adminPort = await listen2(admin, options.adminPort ?? 8788);
3810
+ const close = (server) => new Promise((resolve6) => {
3811
+ server.closeAllConnections();
3812
+ server.close(() => resolve6());
3813
+ });
3814
+ return {
3815
+ ingestUrl: `http://127.0.0.1:${ingestPort}/v1/events`,
3816
+ adminUrl: `http://127.0.0.1:${adminPort}/`,
3817
+ close: async () => {
3818
+ clearInterval(resetInstalls);
3819
+ clearInterval(resetGlobal);
3820
+ await Promise.all([close(ingest), close(admin)]);
3821
+ }
3822
+ };
3823
+ }
3824
+ function passwordMatches(request, password3) {
3825
+ const header = request.headers.authorization ?? "";
3826
+ if (!header.startsWith("Basic ")) return false;
3827
+ const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
3828
+ const given = decoded.slice(decoded.indexOf(":") + 1);
3829
+ const digest = (value) => createHash("sha256").update(value).digest();
3830
+ return timingSafeEqual(digest(given), digest(password3));
3831
+ }
3832
+ function readBody(request) {
3833
+ return new Promise((resolve6) => {
3834
+ const chunks = [];
3835
+ let size = 0;
3836
+ request.on("data", (chunk) => {
3837
+ size += chunk.length;
3838
+ if (size > MAX_BODY_BYTES) resolve6(null);
3839
+ else chunks.push(chunk);
3840
+ });
3841
+ request.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
3842
+ request.on("error", () => resolve6(null));
3843
+ });
3844
+ }
3845
+ function listen2(server, port) {
3846
+ return new Promise((resolve6, reject) => {
3847
+ server.once("error", reject);
3848
+ server.listen(port, "127.0.0.1", () => resolve6(server.address().port));
3849
+ });
3850
+ }
3851
+ async function readEvents(dataDir, days, now = Date.now()) {
3852
+ 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();
3854
+ const events = [];
3855
+ for (const file of files) {
3856
+ for (const line of (await readFile4(join6(dataDir, file), "utf8")).split("\n")) {
3857
+ if (!line) continue;
3858
+ try {
3859
+ events.push(JSON.parse(line));
3860
+ } catch {
3861
+ }
3862
+ }
3863
+ }
3864
+ return events;
3865
+ }
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;
3872
+ };
3873
+ 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();
3883
+ const latest = /* @__PURE__ */ new Map();
3884
+ const users = /* @__PURE__ */ new Map();
3885
+ let errors = 0;
3886
+ let tokensOut = 0;
3887
+ for (const event of events) {
3888
+ const bucket = daily.get(day(event.receivedAt));
3889
+ if (bucket) {
3890
+ bucket.installs.add(event.installId);
3891
+ bucket.events += 1;
3892
+ }
3893
+ const seen = firstSeen.get(event.installId);
3894
+ if (!seen || event.receivedAt < seen) firstSeen.set(event.installId, event.receivedAt);
3895
+ latest.set(event.installId, event);
3896
+ const user = users.get(event.installId) ?? {
3897
+ lastSeen: event.receivedAt,
3898
+ events: 0,
3899
+ sessions: /* @__PURE__ */ new Set(),
3900
+ projects: /* @__PURE__ */ new Set(),
3901
+ features: /* @__PURE__ */ new Map()
3902
+ };
3903
+ if (event.receivedAt > user.lastSeen) user.lastSeen = event.receivedAt;
3904
+ user.events += 1;
3905
+ if (event.session) user.sessions.add(event.session);
3906
+ user.projects.add(event.project);
3907
+ const feature = event.kind === "tool" ? event.name : event.kind;
3908
+ user.features.set(feature, (user.features.get(feature) ?? 0) + 1);
3909
+ users.set(event.installId, user);
3910
+ kinds.set(event.kind, (kinds.get(event.kind) ?? 0) + 1);
3911
+ if (!event.ok) errors += 1;
3912
+ 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);
3920
+ }
3921
+ const newPerDay = /* @__PURE__ */ new Map();
3922
+ for (const seen of firstSeen.values())
3923
+ newPerDay.set(day(seen), (newPerDay.get(day(seen)) ?? 0) + 1);
3924
+ const share = (pick) => {
3925
+ const counts = /* @__PURE__ */ new Map();
3926
+ for (const event of latest.values()) {
3927
+ for (const value of pick(event)) counts.set(value, (counts.get(value) ?? 0) + 1);
3928
+ }
3929
+ return [...counts].map(([label, installs]) => ({ label, installs })).sort((a, b) => b.installs - a.installs);
3930
+ };
3931
+ const median = (values) => {
3932
+ const sorted = [...values].sort((a, b) => a - b);
3933
+ return sorted[Math.floor(sorted.length / 2)] ?? 0;
3934
+ };
3935
+ return {
3936
+ generatedAt: new Date(now).toISOString(),
3937
+ days,
3938
+ 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,
3944
+ tokensOut,
3945
+ active1d: activeWithin(1),
3946
+ active7d: activeWithin(7),
3947
+ active30d: activeWithin(30),
3948
+ newToday: newPerDay.get(today) ?? 0
3949
+ },
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]) => ({
3957
+ 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),
3964
+ clients: share((e) => e.clients.length ? e.clients : ["unknown"]),
3965
+ versions: share((e) => [e.version]),
3966
+ platforms: share((e) => [`${e.platform}/${e.arch}`]),
3967
+ nodes: share((e) => [`node ${e.node}`]),
3968
+ /** One row per install, most recently active first. */
3969
+ users: [...users].map(([installId, user]) => {
3970
+ const last = latest.get(installId);
3971
+ return {
3972
+ install: installId.slice(0, 8),
3973
+ firstSeen: firstSeen.get(installId) ?? user.lastSeen,
3974
+ lastSeen: user.lastSeen,
3975
+ events: user.events,
3976
+ sessions: user.sessions.size,
3977
+ projects: user.projects.size,
3978
+ client: last?.clients.join(",") || "unknown",
3979
+ version: last?.version ?? "",
3980
+ platform: last ? `${last.platform}/${last.arch}` : "",
3981
+ topFeature: [...user.features].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""
3982
+ };
3983
+ }).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)).slice(0, 1e3),
3984
+ recent: events.slice(-50).reverse().map((e) => ({
3985
+ receivedAt: e.receivedAt,
3986
+ install: e.installId.slice(0, 8),
3987
+ kind: e.kind,
3988
+ name: e.name,
3989
+ ms: e.ms,
3990
+ ok: e.ok,
3991
+ version: e.version,
3992
+ client: e.clients.join(",") || "unknown"
3993
+ }))
3994
+ };
3995
+ }
3996
+ async function npmDownloads(fetchImpl) {
3997
+ const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3998
+ const from = new Date(Date.now() - 364 * 864e5).toISOString().slice(0, 10);
3999
+ try {
4000
+ const response = await fetchImpl(
4001
+ `https://api.npmjs.org/downloads/range/${from}:${to}/${NPM_PACKAGE}`,
4002
+ { signal: AbortSignal.timeout(5e3) }
4003
+ );
4004
+ if (!response.ok) return { error: `npm API ${response.status}` };
4005
+ const body = await response.json();
4006
+ return { package: NPM_PACKAGE, downloads: body.downloads ?? [] };
4007
+ } catch (error) {
4008
+ return { error: error.message };
4009
+ }
4010
+ }
4011
+
4012
+ // src/cli/commands/telemetry.ts
4013
+ function registerTelemetryCommands(program, runSafely2) {
4014
+ const telemetry = program.command("telemetry").description("Show or change anonymous usage telemetry (no prompts, code or paths).");
4015
+ telemetry.command("status").description("Show whether this machine sends anonymous usage data.").action(async () => {
4016
+ await runSafely2(async () => {
4017
+ const state = await readTelemetryState();
4018
+ const blocked = telemetryBlocked(state);
4019
+ console.log(
4020
+ blocked ? `${pc19.yellow("\u25CB")} telemetry off (${blocked})` : `${pc19.green("\u25CF")} telemetry on`
4021
+ );
4022
+ console.log(pc19.dim(` install id ${state.installId} (${telemetryStatePath()})`));
4023
+ if (telemetryUrl()) console.log(pc19.dim(` collector ${telemetryUrl()}`));
4024
+ });
4025
+ });
4026
+ telemetry.command("enable").description("Allow anonymous usage data from this machine.").action(async () => {
4027
+ await runSafely2(async () => {
4028
+ const state = await readTelemetryState();
4029
+ await writeTelemetryState({
4030
+ installId: state.installId,
4031
+ noticeAt: state.noticeAt ?? (/* @__PURE__ */ new Date()).toISOString()
4032
+ });
4033
+ console.log(TELEMETRY_NOTICE);
4034
+ console.log(pc19.green("Telemetry enabled."));
4035
+ });
4036
+ });
4037
+ telemetry.command("disable").description("Stop sending anonymous usage data from this machine.").action(async () => {
4038
+ await runSafely2(async () => {
4039
+ const state = await readTelemetryState();
4040
+ await writeTelemetryState({ ...state, disabled: true });
4041
+ console.log("Telemetry disabled. Nothing more is sent from this machine.");
4042
+ });
4043
+ });
4044
+ program.command("collector").description("[admin] Receive anonymous telemetry and serve the usage admin panel.").option("--data <dir>", "Where events are stored (one JSONL file per day).", "./telemetry-data").option(
4045
+ "--ingest-port <port>",
4046
+ "127.0.0.1 port for POST /v1/events (expose publicly).",
4047
+ Number,
4048
+ 8787
4049
+ ).option(
4050
+ "--admin-port <port>",
4051
+ "127.0.0.1 port for the admin panel (keep private).",
4052
+ Number,
4053
+ 8788
4054
+ ).action(async (opts) => {
4055
+ await runSafely2(async () => {
4056
+ const collector = await startCollector({
4057
+ dataDir: opts.data,
4058
+ ingestPort: opts.ingestPort,
4059
+ adminPort: opts.adminPort,
4060
+ ...process.env.CODEBUDDY_ADMIN_PASSWORD ? { adminPassword: process.env.CODEBUDDY_ADMIN_PASSWORD } : {}
4061
+ });
4062
+ console.log(`${pc19.green("\u25CF")} ingest ${pc19.bold(collector.ingestUrl)}`);
4063
+ console.log(`${pc19.green("\u25CF")} admin ${pc19.bold(collector.adminUrl)}`);
4064
+ console.log(
4065
+ pc19.dim(
4066
+ 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
+ )
4068
+ );
4069
+ console.log(pc19.dim(` events in ${opts.data}. Ctrl+C to stop.`));
4070
+ await new Promise((resolve6) => {
4071
+ const stop = () => void collector.close().then(resolve6);
4072
+ process.once("SIGINT", stop);
4073
+ process.once("SIGTERM", stop);
4074
+ });
4075
+ });
4076
+ });
4077
+ }
4078
+
4868
4079
  // src/cli/commands/use-command.ts
4869
4080
  import { basename as basename2 } from "path";
4870
4081
  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";
4871
- import pc19 from "picocolors";
4082
+ import pc20 from "picocolors";
4872
4083
 
4873
4084
  // src/core/global-config.ts
4874
- import { chmod, mkdir as mkdir8, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
4875
- import { homedir as homedir3 } from "os";
4876
- import { dirname as dirname11, join as join15 } from "path";
4877
- import { z as z2 } from "zod";
4085
+ import { chmod, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
4086
+ import { homedir } from "os";
4087
+ import { dirname as dirname3, join as join7 } from "path";
4088
+ import { z as z3 } from "zod";
4878
4089
  var GLOBAL_DIR = ".codebuddy";
4879
4090
  var GLOBAL_FILE = "global.json";
4880
- var globalConfigSchema = z2.object({
4881
- hfToken: z2.string().min(1).optional(),
4882
- databaseUrl: z2.string().min(1).optional(),
4091
+ var globalConfigSchema = z3.object({
4092
+ hfToken: z3.string().min(1).optional(),
4093
+ databaseUrl: z3.string().min(1).optional(),
4883
4094
  // Where the codebuddy install lives on this machine. Cached so future
4884
4095
  // `codebuddy use` calls in unrelated projects can find the CLI entry
4885
4096
  // without re-resolving via process.argv every time.
4886
- installPath: z2.string().optional()
4097
+ installPath: z3.string().optional()
4887
4098
  });
4888
- function globalConfigPath(home = homedir3()) {
4889
- return join15(home, GLOBAL_DIR, GLOBAL_FILE);
4099
+ function globalConfigPath(home = homedir()) {
4100
+ return join7(home, GLOBAL_DIR, GLOBAL_FILE);
4890
4101
  }
4891
- async function readGlobalConfig(home = homedir3()) {
4102
+ async function readGlobalConfig(home = homedir()) {
4892
4103
  const path = globalConfigPath(home);
4893
4104
  try {
4894
- const raw = await readFile13(path, "utf8");
4105
+ const raw = await readFile5(path, "utf8");
4895
4106
  return globalConfigSchema.parse(JSON.parse(raw));
4896
4107
  } catch (error) {
4897
4108
  if (error.code === "ENOENT") return {};
4898
4109
  throw error;
4899
4110
  }
4900
4111
  }
4901
- async function writeGlobalConfig(next, home = homedir3()) {
4112
+ async function writeGlobalConfig(next, home = homedir()) {
4902
4113
  const path = globalConfigPath(home);
4903
- await mkdir8(dirname11(path), { recursive: true, mode: 448 });
4114
+ await mkdir3(dirname3(path), { recursive: true, mode: 448 });
4904
4115
  await writeFile3(path, `${JSON.stringify(next, null, 2)}
4905
4116
  `, { mode: 384 });
4906
4117
  await chmod(path, 384);
4907
4118
  return { path };
4908
4119
  }
4909
- async function mergeGlobalConfig(patch, home = homedir3()) {
4120
+ async function mergeGlobalConfig(patch, home = homedir()) {
4910
4121
  const current = await readGlobalConfig(home);
4911
4122
  const merged = { ...current, ...patch };
4912
4123
  const { path } = await writeGlobalConfig(merged, home);
@@ -4952,15 +4163,15 @@ async function runUseCommand(options = {}) {
4952
4163
  })
4953
4164
  ));
4954
4165
  note2(
4955
- `Project folder: ${pc19.bold(process.cwd())}
4956
- Namespace: ${pc19.bold(namespace)}`,
4166
+ `Project folder: ${pc20.bold(process.cwd())}
4167
+ Namespace: ${pc20.bold(namespace)}`,
4957
4168
  "codebuddy use"
4958
4169
  );
4959
4170
  const stored = await readGlobalConfig();
4960
4171
  const projectConfig = await readConfigFile();
4961
4172
  let embeddings = projectConfig.provider?.type;
4962
4173
  if (embeddings) {
4963
- log2.info(`Embeddings: ${embeddings} (change with ${pc19.cyan("codebuddy models use")}).`);
4174
+ log2.info(`Embeddings: ${embeddings} (change with ${pc20.cyan("codebuddy models use")}).`);
4964
4175
  } else if (!interactive) {
4965
4176
  embeddings = "local";
4966
4177
  } else {
@@ -5001,7 +4212,7 @@ Namespace: ${pc19.bold(namespace)}`,
5001
4212
  log2.info("Using HF_TOKEN from environment.");
5002
4213
  } else if (stored.hfToken) {
5003
4214
  hfToken = stored.hfToken;
5004
- log2.info(`Reusing saved Hugging Face token (${pc19.dim("~/.codebuddy/global.json")}).`);
4215
+ log2.info(`Reusing saved Hugging Face token (${pc20.dim("~/.codebuddy/global.json")}).`);
5005
4216
  } else if (!interactive) {
5006
4217
  throw new Error(
5007
4218
  "Hugging Face embeddings need a token: set HF_TOKEN (no terminal to prompt for it)."
@@ -5054,7 +4265,7 @@ Namespace: ${pc19.bold(namespace)}`,
5054
4265
  if (result.code === 0) {
5055
4266
  composeSpinner.stop("Postgres container started.");
5056
4267
  for (let attempt = 0; attempt < 12 && !connected; attempt++) {
5057
- await new Promise((resolve13) => setTimeout(resolve13, 500));
4268
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
5058
4269
  connected = await pingOnce2(databaseUrl);
5059
4270
  }
5060
4271
  if (!connected) {
@@ -5135,7 +4346,7 @@ Namespace: ${pc19.bold(namespace)}`,
5135
4346
  databaseUrl
5136
4347
  });
5137
4348
  log2.success(
5138
- `Registered ${pc19.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
4349
+ `Registered ${pc20.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
5139
4350
  );
5140
4351
  } catch (error) {
5141
4352
  log2.warn(`Claude Desktop registration skipped: ${error.message}`);
@@ -5150,7 +4361,7 @@ Namespace: ${pc19.bold(namespace)}`,
5150
4361
  databaseUrl
5151
4362
  });
5152
4363
  log2.success(
5153
- `Registered ${pc19.bold(install.serverKey)} in Codex. Restart Codex to activate.`
4364
+ `Registered ${pc20.bold(install.serverKey)} in Codex. Restart Codex to activate.`
5154
4365
  );
5155
4366
  } catch (error) {
5156
4367
  log2.warn(`Codex registration skipped: ${error.message}`);
@@ -5158,24 +4369,24 @@ Namespace: ${pc19.bold(namespace)}`,
5158
4369
  }
5159
4370
  note2(
5160
4371
  [
5161
- `${pc19.green("\u2713")} CodeBuddy is ready for ${pc19.bold(folderName)}.`,
5162
- `${pc19.green("\u2713")} Scaffolded ${pc19.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
5163
- `${pc19.green("\u2713")} ${graphifyMessage}`,
5164
- `${pc19.green("\u2713")} Added agent workflow instructions to ${pc19.cyan("CLAUDE.md")} and ${pc19.cyan("AGENTS.md")}.`,
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")}.`,
5165
4376
  ``,
5166
4377
  `Next:`,
5167
4378
  ` 1. Restart Claude Desktop and Codex.`,
5168
- ` 2. Ask Claude or Codex to use the ${pc19.cyan("context_pack")} tool for each coding task.`,
4379
+ ` 2. Ask Claude or Codex to use the ${pc20.cyan("context_pack")} tool for each coding task.`,
5169
4380
  ` 3. Indexing, Graphify, Markdown memory import, and embeddings now update automatically while the MCP server is running.`,
5170
- ` 4. Run ${pc19.cyan("codebuddy doctor")} or ${pc19.cyan("codebuddy db doctor")} if you want a full health check.`,
4381
+ ` 4. Run ${pc20.cyan("codebuddy doctor")} or ${pc20.cyan("codebuddy db doctor")} if you want a full health check.`,
5171
4382
  ``,
5172
4383
  `Manage:`,
5173
- ` codebuddy claude list ${pc19.dim("# see every project wired up")}`,
5174
- ` codebuddy codex list ${pc19.dim("# see Codex entries")}`,
5175
- ` codebuddy db test ${pc19.dim("# quick Postgres connectivity check")}`,
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")}`,
5176
4387
  ` codebuddy claude remove codebuddy-${namespace}`,
5177
4388
  ` codebuddy codex remove codebuddy-${namespace}`,
5178
- ` codebuddy postgres down ${pc19.dim("# stop the DB (data preserved)")}`
4389
+ ` codebuddy postgres down ${pc20.dim("# stop the DB (data preserved)")}`
5179
4390
  ].join("\n"),
5180
4391
  "Done"
5181
4392
  );
@@ -5186,7 +4397,7 @@ Namespace: ${pc19.bold(namespace)}`,
5186
4397
 
5187
4398
  // src/cli/commands/worklog.ts
5188
4399
  import { relative as relative6 } from "path";
5189
- import pc20 from "picocolors";
4400
+ import pc21 from "picocolors";
5190
4401
  function registerWorklogCommands(program, runSafely2) {
5191
4402
  const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
5192
4403
  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) => {
@@ -5197,7 +4408,7 @@ function registerWorklogCommands(program, runSafely2) {
5197
4408
  return;
5198
4409
  }
5199
4410
  if (entries.length === 0) {
5200
- console.log(pc20.dim("no worklog entries yet"));
4411
+ console.log(pc21.dim("no worklog entries yet"));
5201
4412
  return;
5202
4413
  }
5203
4414
  for (const entry of entries) console.log(renderRow2(entry));
@@ -5211,36 +4422,36 @@ function registerWorklogCommands(program, runSafely2) {
5211
4422
  worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
5212
4423
  await runSafely2(async () => {
5213
4424
  const [entry] = await new WorklogStore().list();
5214
- console.log(entry ? renderEntry(entry) : pc20.dim("no worklog entries yet"));
4425
+ console.log(entry ? renderEntry(entry) : pc21.dim("no worklog entries yet"));
5215
4426
  });
5216
4427
  });
5217
4428
  }
5218
4429
  function renderRow2(entry) {
5219
4430
  const summary = entry.summary.replace(/\s+/g, " ");
5220
- const short = summary.length <= 70 ? summary : `${summary.slice(0, 69)}\u2026`;
5221
- return `${pc20.dim(entry.createdAt.slice(0, 16))} ${pc20.dim(entry.id)} ${short} ${pc20.dim(`(${entry.files.length} file(s))`)}`;
4431
+ 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))`)}`;
5222
4433
  }
5223
4434
  function renderEntry(entry) {
5224
- const lines = [`${pc20.bold(entry.id)} ${pc20.dim(entry.createdAt)} ${pc20.dim(entry.namespace)}`];
5225
- if (entry.sourceSessionId) lines.push(`${pc20.bold("session:")} ${entry.sourceSessionId}`);
5226
- if (entry.sourcePlanId) lines.push(`${pc20.bold("plan:")} ${entry.sourcePlanId}`);
5227
- if (entry.commitSha) lines.push(`${pc20.bold("commit:")} ${entry.commitSha}`);
5228
- if (entry.files.length > 0) lines.push(`${pc20.bold("files:")} ${entry.files.join(", ")}`);
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(", ")}`);
5229
4440
  if (entry.verification.length > 0) {
5230
- lines.push(`${pc20.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
4441
+ lines.push(`${pc21.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
5231
4442
  }
5232
4443
  if (entry.savedMemory.length > 0) {
5233
- lines.push(`${pc20.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
4444
+ lines.push(`${pc21.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
5234
4445
  }
5235
4446
  if (entry.queuedForReview.length > 0) {
5236
- lines.push(`${pc20.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
4447
+ lines.push(`${pc21.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
5237
4448
  }
5238
- lines.push("", entry.summary, "", pc20.dim(relative6(process.cwd(), entry.path)));
4449
+ lines.push("", entry.summary, "", pc21.dim(relative6(process.cwd(), entry.path)));
5239
4450
  return lines.join("\n");
5240
4451
  }
5241
4452
 
5242
4453
  // src/cli/commands/zvec.ts
5243
- import pc21 from "picocolors";
4454
+ import pc22 from "picocolors";
5244
4455
  function registerZvecCommands(program, runSafely2) {
5245
4456
  const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
5246
4457
  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) => {
@@ -5250,22 +4461,22 @@ function registerZvecCommands(program, runSafely2) {
5250
4461
  console.log(JSON.stringify(report, null, 2));
5251
4462
  return;
5252
4463
  }
5253
- const mark = (ok2) => ok2 ? pc21.green("yes") : pc21.yellow("no");
4464
+ const mark = (ok2) => ok2 ? pc22.green("yes") : pc22.yellow("no");
5254
4465
  console.log(`enabled: ${mark(report.enabled)}`);
5255
4466
  console.log(`binary: ${report.binary}`);
5256
4467
  console.log(
5257
- `version: ${report.version ?? pc21.yellow("not found")}${report.version && !report.supported ? pc21.yellow(" (unsupported; need 0.2.x)") : ""}`
4468
+ `version: ${report.version ?? pc22.yellow("not found")}${report.version && !report.supported ? pc22.yellow(" (unsupported; need 0.2.x)") : ""}`
5258
4469
  );
5259
4470
  console.log(`index ready: ${mark(report.indexReady)}`);
5260
4471
  console.log(
5261
- `zg server: ${report.server} ${pc21.dim("(running = faster queries; optional)")}`
4472
+ `zg server: ${report.server} ${pc22.dim("(running = faster queries; optional)")}`
5262
4473
  );
5263
4474
  console.log(`status: ${report.reason}`);
5264
- if (!report.version) console.log(pc21.dim("install: npm install -g @zvec/zvec-grep"));
4475
+ if (!report.version) console.log(pc22.dim("install: npm install -g @zvec/zvec-grep"));
5265
4476
  else if (report.supported && !report.indexReady)
5266
- console.log(pc21.dim("next: codebuddy zvec index"));
4477
+ console.log(pc22.dim("next: codebuddy zvec index"));
5267
4478
  else if (report.indexReady && !report.enabled)
5268
- console.log(pc21.dim("next: codebuddy zvec enable"));
4479
+ console.log(pc22.dim("next: codebuddy zvec enable"));
5269
4480
  });
5270
4481
  });
5271
4482
  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) => {
@@ -5275,21 +4486,21 @@ function registerZvecCommands(program, runSafely2) {
5275
4486
  ...opts.rebuild ? { rebuild: true } : {}
5276
4487
  });
5277
4488
  if (code !== 0) throw new Error(`zg index exited with code ${code}`);
5278
- console.log(pc21.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
4489
+ console.log(pc22.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
5279
4490
  });
5280
4491
  });
5281
4492
  zvec.command("enable").description("Use zvec-grep candidates in context_pack / context_before_edit.").action(async () => {
5282
4493
  await runSafely2(async () => {
5283
4494
  await setZvecEnabled(true);
5284
4495
  console.log(
5285
- `${pc21.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
4496
+ `${pc22.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
5286
4497
  );
5287
4498
  });
5288
4499
  });
5289
4500
  zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
5290
4501
  await runSafely2(async () => {
5291
4502
  await setZvecEnabled(false);
5292
- console.log(`${pc21.yellow("disabled")} zvec-grep retrieval`);
4503
+ console.log(`${pc22.yellow("disabled")} zvec-grep retrieval`);
5293
4504
  });
5294
4505
  });
5295
4506
  }
@@ -5598,6 +4809,8 @@ function createCli() {
5598
4809
  registerClientsCommands(program, runSafely);
5599
4810
  registerGitHubCommands(program, runSafely);
5600
4811
  registerSymbolsCommand(program, runSafely);
4812
+ registerTelemetryCommands(program, runSafely);
4813
+ program.hook("preAction", () => showTelemetryNoticeOnce());
5601
4814
  program.action(async () => {
5602
4815
  await runSafely(() => runUseCommand());
5603
4816
  });
@@ -5625,17 +4838,20 @@ var HELP_GROUPS = [
5625
4838
  "prune"
5626
4839
  ]
5627
4840
  ],
5628
- ["Integrations:", ["serve", "claude", "codex", "github", "rules", "learn", "eval"]]
4841
+ [
4842
+ "Integrations:",
4843
+ ["serve", "claude", "codex", "github", "rules", "learn", "eval", "telemetry", "collector"]
4844
+ ]
5629
4845
  ];
5630
4846
  function groupHelp(program) {
5631
4847
  const groupOf = new Map(
5632
- HELP_GROUPS.flatMap(([group, names]) => names.map((name) => [name, group]))
4848
+ HELP_GROUPS.flatMap(([group, names]) => names.map((name2) => [name2, group]))
5633
4849
  );
5634
4850
  for (const command of program.commands) {
5635
4851
  command.helpGroup(groupOf.get(command.name()) ?? "Other:");
5636
4852
  }
5637
4853
  const position = new Map(
5638
- HELP_GROUPS.flatMap(([, names]) => names).map((name, index) => [name, index])
4854
+ HELP_GROUPS.flatMap(([, names]) => names).map((name2, index) => [name2, index])
5639
4855
  );
5640
4856
  program.commands.sort(
5641
4857
  (a, b) => (position.get(a.name()) ?? 999) - (position.get(b.name()) ?? 999)
@@ -5665,7 +4881,7 @@ async function runSafely(fn) {
5665
4881
  try {
5666
4882
  await fn();
5667
4883
  } catch (error) {
5668
- console.error(pc22.red(String(redactSecrets(error.message))));
4884
+ console.error(pc23.red(String(redactSecrets(error.message))));
5669
4885
  process.exitCode = 1;
5670
4886
  }
5671
4887
  }
@@ -5679,9 +4895,9 @@ function parseDuration(value) {
5679
4895
  return amount * 60 * 1e3;
5680
4896
  }
5681
4897
  function printDoctor(report) {
5682
- const ok2 = pc22.green("ok");
5683
- const bad = pc22.red("fail");
5684
- const warn2 = pc22.yellow("warn");
4898
+ const ok2 = pc23.green("ok");
4899
+ const bad = pc23.red("fail");
4900
+ const warn2 = pc23.yellow("warn");
5685
4901
  console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok2 : bad}`);
5686
4902
  if (report.scaffold.codebuddyDir.warning)
5687
4903
  console.log(` ${report.scaffold.codebuddyDir.warning}`);
@@ -5707,7 +4923,7 @@ function printDoctor(report) {
5707
4923
  );
5708
4924
  console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
5709
4925
  if (report.usage.warning80Percent)
5710
- console.log(pc22.yellow("usage warning: daily cap is over 80% used"));
4926
+ console.log(pc23.yellow("usage warning: daily cap is over 80% used"));
5711
4927
  console.log(
5712
4928
  `config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok2 : warn2}`
5713
4929
  );
@@ -5717,9 +4933,9 @@ function printDoctor(report) {
5717
4933
  }
5718
4934
  }
5719
4935
  function printDbDoctor(report) {
5720
- const ok2 = pc22.green("ok");
5721
- const bad = pc22.red("fail");
5722
- const warn2 = pc22.yellow("warn");
4936
+ const ok2 = pc23.green("ok");
4937
+ const bad = pc23.red("fail");
4938
+ const warn2 = pc23.yellow("warn");
5723
4939
  console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
5724
4940
  if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
5725
4941
  if (report.db.error) console.log(` ${report.db.error}`);
@@ -5735,4 +4951,4 @@ function printDbDoctor(report) {
5735
4951
  export {
5736
4952
  createCli
5737
4953
  };
5738
- //# sourceMappingURL=main-AHNGHC4L.js.map
4954
+ //# sourceMappingURL=main-QUASSFP7.js.map