@orquentra/codebuddy 0.0.0-stage → 2.4.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 (82) hide show
  1. package/ARCHITECTURE.md +269 -0
  2. package/CLAUDE_CODEX.md +433 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1118 -2
  5. package/WORKFLOW.md +152 -0
  6. package/dist/chunk-3ZELDCR3.js +204 -0
  7. package/dist/chunk-3ZELDCR3.js.map +1 -0
  8. package/dist/chunk-5OWZVZSO.js +476 -0
  9. package/dist/chunk-5OWZVZSO.js.map +1 -0
  10. package/dist/chunk-DSN5CEHO.js +175 -0
  11. package/dist/chunk-DSN5CEHO.js.map +1 -0
  12. package/dist/chunk-GDJH7XJC.js +119 -0
  13. package/dist/chunk-GDJH7XJC.js.map +1 -0
  14. package/dist/chunk-IBMJYIEP.js +1578 -0
  15. package/dist/chunk-IBMJYIEP.js.map +1 -0
  16. package/dist/chunk-IE2OUO54.js +199 -0
  17. package/dist/chunk-IE2OUO54.js.map +1 -0
  18. package/dist/chunk-IHQM2ZDV.js +19 -0
  19. package/dist/chunk-IHQM2ZDV.js.map +1 -0
  20. package/dist/chunk-ITGRANYW.js +63 -0
  21. package/dist/chunk-ITGRANYW.js.map +1 -0
  22. package/dist/chunk-K63EOA4B.js +416 -0
  23. package/dist/chunk-K63EOA4B.js.map +1 -0
  24. package/dist/chunk-PQK3GKZH.js +98 -0
  25. package/dist/chunk-PQK3GKZH.js.map +1 -0
  26. package/dist/chunk-PYLH4MH5.js +34 -0
  27. package/dist/chunk-PYLH4MH5.js.map +1 -0
  28. package/dist/chunk-PZ5AY32C.js +10 -0
  29. package/dist/chunk-PZ5AY32C.js.map +1 -0
  30. package/dist/chunk-SYHB3AMM.js +8580 -0
  31. package/dist/chunk-SYHB3AMM.js.map +1 -0
  32. package/dist/chunk-TI5TQD6D.js +324 -0
  33. package/dist/chunk-TI5TQD6D.js.map +1 -0
  34. package/dist/chunk-WHZSVY2L.js +144 -0
  35. package/dist/chunk-WHZSVY2L.js.map +1 -0
  36. package/dist/cli/index.d.ts +1 -0
  37. package/dist/cli/index.js +15 -0
  38. package/dist/cli/index.js.map +1 -0
  39. package/dist/codebuddy-D2xJ6hFk.d.ts +841 -0
  40. package/dist/config-S2TULOMK.js +11 -0
  41. package/dist/config-S2TULOMK.js.map +1 -0
  42. package/dist/index.d.ts +2527 -0
  43. package/dist/index.js +116 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/langgraph/index.d.ts +53 -0
  46. package/dist/langgraph/index.js +143 -0
  47. package/dist/langgraph/index.js.map +1 -0
  48. package/dist/lean-command-D6TZJEFX.js +13 -0
  49. package/dist/lean-command-D6TZJEFX.js.map +1 -0
  50. package/dist/main-ZZ3EQLYA.js +4575 -0
  51. package/dist/main-ZZ3EQLYA.js.map +1 -0
  52. package/dist/memory-NIBFAMY4.js +57 -0
  53. package/dist/memory-NIBFAMY4.js.map +1 -0
  54. package/dist/pretool-command-MQQ2UNQM.js +10 -0
  55. package/dist/pretool-command-MQQ2UNQM.js.map +1 -0
  56. package/dist/rerank-Q7MIFNKA.js +60 -0
  57. package/dist/rerank-Q7MIFNKA.js.map +1 -0
  58. package/dist/summaries-TSE44KUQ.js +18 -0
  59. package/dist/summaries-TSE44KUQ.js.map +1 -0
  60. package/dist/zvec-F27AR357.js +23 -0
  61. package/dist/zvec-F27AR357.js.map +1 -0
  62. package/docker-compose.yml +20 -0
  63. package/examples/claude-code/README.md +67 -0
  64. package/examples/claude-desktop/README.md +54 -0
  65. package/examples/claude-desktop/claude_desktop_config.json +13 -0
  66. package/examples/langgraph-agent/README.md +32 -0
  67. package/examples/langgraph-agent/index.ts +71 -0
  68. package/examples/multi-agent-handoff/README.md +35 -0
  69. package/examples/multi-agent-handoff/index.ts +68 -0
  70. package/package.json +108 -4
  71. package/src/db/migrations/0000_cultured_satana.sql +144 -0
  72. package/src/db/migrations/0001_embedding_lease.sql +3 -0
  73. package/src/db/migrations/0002_embedding_lease_token.sql +1 -0
  74. package/src/db/migrations/0003_embedding_space.sql +9 -0
  75. package/src/db/migrations/0004_markdown_reconcile.sql +5 -0
  76. package/src/db/migrations/0005_fact_propositions.sql +1 -0
  77. package/src/db/migrations/meta/0000_snapshot.json +1169 -0
  78. package/src/db/migrations/meta/0002_snapshot.json +1181 -0
  79. package/src/db/migrations/meta/0003_snapshot.json +1214 -0
  80. package/src/db/migrations/meta/0004_snapshot.json +1225 -0
  81. package/src/db/migrations/meta/0005_snapshot.json +1231 -0
  82. package/src/db/migrations/meta/_journal.json +48 -0
@@ -0,0 +1,4575 @@
1
+ import {
2
+ renderLeanBlock,
3
+ runLeanHookCommand
4
+ } from "./chunk-DSN5CEHO.js";
5
+ import {
6
+ runPreToolHookCommand
7
+ } from "./chunk-WHZSVY2L.js";
8
+ import {
9
+ failSoft,
10
+ readHookInput,
11
+ writeStdout
12
+ } from "./chunk-PYLH4MH5.js";
13
+ import {
14
+ MemoryFileStore,
15
+ PlanFileStore,
16
+ PlanLifecycle,
17
+ ReviewStore,
18
+ VERSION,
19
+ WorklogStore,
20
+ approveReviewItem,
21
+ assessRisk,
22
+ bootstrapDatabase,
23
+ buildBeforeEditContext,
24
+ buildBootstrapContext,
25
+ createDatabaseClient,
26
+ createRuntime,
27
+ generateSuggestions,
28
+ inspectNamespace,
29
+ listFactsPage,
30
+ migrateMemoryToFiles,
31
+ pingDatabase,
32
+ registerCodeBuddyTools,
33
+ renderContextBlock,
34
+ resolveRiskPaths,
35
+ runCaptureHook,
36
+ runContextHook,
37
+ runDoctor,
38
+ runMigrations,
39
+ runResetHook,
40
+ runStageHook,
41
+ startMcpServer,
42
+ watchRepository
43
+ } from "./chunk-SYHB3AMM.js";
44
+ import {
45
+ SessionCapsuleLedger,
46
+ computeFactHash,
47
+ estimateTokens,
48
+ generateEntityId
49
+ } from "./chunk-5OWZVZSO.js";
50
+ import {
51
+ DEFAULT_ZVEC_MODEL,
52
+ buildZvecIndex,
53
+ zvecStatus
54
+ } from "./chunk-TI5TQD6D.js";
55
+ import {
56
+ loadLlmSummaries,
57
+ summarizeRepository
58
+ } from "./chunk-3ZELDCR3.js";
59
+ import {
60
+ buildArchitectureMap,
61
+ generateGraphifyGraph,
62
+ indexRepository,
63
+ loadFreshIndex,
64
+ loadGraphifyMap,
65
+ neighbours,
66
+ queryMap,
67
+ setupGraphify
68
+ } from "./chunk-IBMJYIEP.js";
69
+ import {
70
+ extractSymbolTable,
71
+ supportsSymbols
72
+ } from "./chunk-PQK3GKZH.js";
73
+ import "./chunk-IHQM2ZDV.js";
74
+ import {
75
+ chatJson,
76
+ coolingDown,
77
+ usageToday
78
+ } from "./chunk-IE2OUO54.js";
79
+ import {
80
+ redactPersisted
81
+ } from "./chunk-GDJH7XJC.js";
82
+ import {
83
+ resolveLlmConfig
84
+ } from "./chunk-ITGRANYW.js";
85
+ import {
86
+ PLAN_POLICIES,
87
+ configPath,
88
+ initConfigFile,
89
+ initProjectScaffold,
90
+ loadPlanPolicy,
91
+ loadRuntimeConfig,
92
+ readConfigFile,
93
+ redactSecrets,
94
+ resolveNamespaceFrom,
95
+ setLlmEnabled,
96
+ setPlanPolicy,
97
+ setZvecEnabled,
98
+ writeAtomic,
99
+ writeConfigFile
100
+ } from "./chunk-K63EOA4B.js";
101
+ import "./chunk-PZ5AY32C.js";
102
+
103
+ // src/cli/main.ts
104
+ import { Command } from "commander";
105
+ import pc20 from "picocolors";
106
+
107
+ // src/integrations/github.ts
108
+ import { constants } from "fs";
109
+ import { access, mkdir, writeFile } from "fs/promises";
110
+ import { dirname, join, resolve } from "path";
111
+ var CODEBUDDY_WORKFLOW_PATH = ".github/workflows/codebuddy.yml";
112
+ function renderGitHubWorkflow(version = VERSION) {
113
+ return `name: CodeBuddy Change Safety
114
+
115
+ on:
116
+ pull_request:
117
+ types: [opened, synchronize, reopened]
118
+
119
+ permissions:
120
+ contents: read
121
+ pull-requests: write
122
+
123
+ jobs:
124
+ codebuddy:
125
+ runs-on: ubuntu-latest
126
+ steps:
127
+ - name: Check out pull request
128
+ uses: actions/checkout@v4
129
+
130
+ - name: Set up Node
131
+ uses: actions/setup-node@v4
132
+ with:
133
+ node-version: 20
134
+
135
+ - name: Install CodeBuddy
136
+ run: npm install --global --no-audit --no-fund @orquentra/codebuddy@${version}
137
+
138
+ - name: Generate change report
139
+ run: codebuddy change --json > codebuddy-report.json
140
+
141
+ - name: Publish step summary
142
+ env:
143
+ REPORT_PATH: codebuddy-report.json
144
+ SUMMARY_PATH: $GITHUB_STEP_SUMMARY
145
+ run: |
146
+ node <<'NODE'
147
+ const fs = require("fs");
148
+ const report = JSON.parse(fs.readFileSync(process.env.REPORT_PATH, "utf8"));
149
+ const status = report.status.toUpperCase();
150
+ const findings = (report.suggestions || []).slice(0, 8).map((item) =>
151
+ "- **" + item.severity + "** " + item.title + " \u2014 " + item.detail,
152
+ );
153
+ const body = [
154
+ "<!-- codebuddy-report -->",
155
+ "## CodeBuddy change report",
156
+ "**Status:** " + status,
157
+ "**Highest risk:** " + report.risk.highestScore + "/100",
158
+ "**Changed paths:** " + report.paths.length,
159
+ "**Direct dependents:** " + report.architecture.totalDependents,
160
+ findings.length ? "\\n### Findings\\n" + findings.join("\\n") : "\\nNo findings.",
161
+ ].join("\\n");
162
+ fs.writeFileSync("codebuddy-report.md", body + "\\n");
163
+ fs.appendFileSync(process.env.SUMMARY_PATH, body + "\\n");
164
+ NODE
165
+
166
+ - name: Comment on pull request
167
+ uses: actions/github-script@v7
168
+ with:
169
+ github-token: \${{ secrets.GITHUB_TOKEN }}
170
+ script: |
171
+ const fs = require("fs");
172
+ const body = fs.readFileSync("codebuddy-report.md", "utf8");
173
+ const { owner, repo } = context.repo;
174
+ const issue_number = context.issue.number;
175
+ const comments = await github.paginate(github.rest.issues.listComments, {
176
+ owner, repo, issue_number, per_page: 100,
177
+ });
178
+ const existing = comments.find((comment) => comment.body?.includes("<!-- codebuddy-report -->"));
179
+ if (existing) {
180
+ await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
181
+ } else {
182
+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
183
+ }
184
+ `;
185
+ }
186
+ async function installGitHubWorkflow(repositoryRoot = process.cwd(), options = {}) {
187
+ const root = resolve(repositoryRoot);
188
+ const path = join(root, CODEBUDDY_WORKFLOW_PATH);
189
+ if (!options.force) {
190
+ try {
191
+ await access(path, constants.F_OK);
192
+ return { path, created: false };
193
+ } catch {
194
+ }
195
+ }
196
+ await mkdir(dirname(path), { recursive: true });
197
+ await writeFile(path, renderGitHubWorkflow(), "utf8");
198
+ return { path, created: true };
199
+ }
200
+
201
+ // src/cli/commands/change.ts
202
+ import pc from "picocolors";
203
+
204
+ // src/change/engine.ts
205
+ import { spawn } from "child_process";
206
+ import { constants as constants2 } from "fs";
207
+ import { access as access2, readFile } from "fs/promises";
208
+ import { extname, join as join2 } from "path";
209
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
210
+ var DEFAULT_TEST_TIMEOUT_MS = 12e4;
211
+ var MAX_TEST_OUTPUT_CHARS = 12e3;
212
+ async function buildChangeReport(input) {
213
+ const repositoryRoot = input.repositoryRoot ?? process.cwd();
214
+ const hasExplicitSelection = (input.paths?.length ?? 0) > 0 || input.planId !== void 0;
215
+ const paths = input.useGit === false && !hasExplicitSelection ? [] : await resolveRiskPaths({
216
+ repositoryRoot,
217
+ ...input.paths !== void 0 ? { paths: input.paths } : {},
218
+ ...input.planId !== void 0 ? { planId: input.planId } : {},
219
+ ...input.useGit !== void 0 ? { useGit: input.useGit } : { useGit: true }
220
+ });
221
+ const planStore = new PlanFileStore(repositoryRoot);
222
+ const [risk, suggestions, plan, architecture] = await Promise.all([
223
+ paths.length === 0 ? Promise.resolve(emptyAssessment()) : assessRisk({
224
+ repositoryRoot,
225
+ namespace: input.namespace,
226
+ paths,
227
+ ...input.useGit !== void 0 ? { useGit: input.useGit } : {},
228
+ ...input.planId !== void 0 ? { planId: input.planId } : {}
229
+ }),
230
+ paths.length === 0 ? Promise.resolve(emptySuggestions()) : generateSuggestions({ repositoryRoot, namespace: input.namespace, paths }),
231
+ resolveRelevantPlan(planStore, input.namespace, input.planId),
232
+ input.graphifyPath ? loadGraphifyMapOrFallback(input.graphifyPath, repositoryRoot) : buildArchitectureMap(repositoryRoot).then((map2) => ({ map: map2, warning: null }))
233
+ ]);
234
+ const planSummary = summarizePlan(plan, paths);
235
+ const verification = buildVerification(paths, planSummary, suggestions);
236
+ const { map, warning: graphifyWarning } = architecture;
237
+ const dependentsByPath = countDependents(map.edges, paths);
238
+ const highestScore = risk.items.reduce((highest, item) => Math.max(highest, item.score), 0);
239
+ const status = getStatus(paths, highestScore, suggestions, verification);
240
+ return {
241
+ status,
242
+ paths,
243
+ plan: planSummary,
244
+ risk: { highestScore, items: risk.items },
245
+ architecture: {
246
+ dependentsByPath,
247
+ totalDependents: Object.values(dependentsByPath).reduce((sum, count) => sum + count, 0),
248
+ source: input.graphifyPath && !graphifyWarning ? "graphify" : "imports",
249
+ ...graphifyWarning ? { warning: graphifyWarning } : {}
250
+ },
251
+ suggestions: suggestions.suggestions,
252
+ verification
253
+ };
254
+ }
255
+ async function verifyChange(input) {
256
+ const report = await buildChangeReport(input);
257
+ const detected = input.command ? { command: input.command, source: "explicit" } : await detectTestCommand(input.repositoryRoot ?? process.cwd());
258
+ if (!detected) {
259
+ return {
260
+ report,
261
+ test: {
262
+ status: "not_run",
263
+ command: null,
264
+ source: null,
265
+ exitCode: null,
266
+ signal: null,
267
+ durationMs: 0,
268
+ output: "No test command found. Pass --command or add a test script to package.json."
269
+ }
270
+ };
271
+ }
272
+ return {
273
+ report,
274
+ test: await runTestCommand(
275
+ detected.command,
276
+ detected.source,
277
+ input.repositoryRoot ?? process.cwd(),
278
+ input.timeoutMs ?? DEFAULT_TEST_TIMEOUT_MS
279
+ )
280
+ };
281
+ }
282
+ async function detectTestCommand(repositoryRoot) {
283
+ const packagePath = join2(repositoryRoot, "package.json");
284
+ try {
285
+ const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
286
+ if (typeof packageJson.scripts?.test === "string") {
287
+ return {
288
+ command: `${packageManagerName(packageJson.packageManager)} test`,
289
+ source: "package_script"
290
+ };
291
+ }
292
+ } catch (error) {
293
+ if (error.code !== "ENOENT") throw error;
294
+ }
295
+ for (const candidate of [
296
+ ["pnpm-lock.yaml", "pnpm test"],
297
+ ["yarn.lock", "yarn test"],
298
+ ["bun.lockb", "bun test"],
299
+ ["bun.lock", "bun test"],
300
+ ["package-lock.json", "npm test"]
301
+ ]) {
302
+ try {
303
+ await access2(join2(repositoryRoot, candidate[0]), constants2.F_OK);
304
+ return { command: candidate[1], source: "package_manager" };
305
+ } catch {
306
+ }
307
+ }
308
+ return null;
309
+ }
310
+ function packageManagerName(value) {
311
+ if (typeof value !== "string") return "npm";
312
+ const name = value.split(/[\s@]/, 1)[0] ?? "npm";
313
+ return ["npm", "pnpm", "yarn", "bun"].includes(name) ? name : "npm";
314
+ }
315
+ function runTestCommand(command, source, repositoryRoot, timeoutMs) {
316
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
317
+ throw new Error("--timeout must be a positive number of milliseconds.");
318
+ }
319
+ return new Promise((resolve14) => {
320
+ const startedAt = Date.now();
321
+ const child = spawn(command, {
322
+ cwd: repositoryRoot,
323
+ env: process.env,
324
+ shell: true,
325
+ stdio: ["ignore", "pipe", "pipe"]
326
+ });
327
+ let output = "";
328
+ let timedOut = false;
329
+ const append = (chunk) => {
330
+ output += chunk.toString();
331
+ if (output.length > MAX_TEST_OUTPUT_CHARS) {
332
+ output = `[output truncated; showing last ${MAX_TEST_OUTPUT_CHARS} characters]
333
+ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
334
+ }
335
+ };
336
+ child.stdout.on("data", append);
337
+ child.stderr.on("data", append);
338
+ const timer = setTimeout(() => {
339
+ timedOut = true;
340
+ child.kill("SIGTERM");
341
+ }, timeoutMs);
342
+ child.on("close", (exitCode, signal) => {
343
+ clearTimeout(timer);
344
+ resolve14({
345
+ status: timedOut ? "timed_out" : exitCode === 0 ? "passed" : "failed",
346
+ command,
347
+ source,
348
+ exitCode,
349
+ signal,
350
+ durationMs: Date.now() - startedAt,
351
+ output: output.trim()
352
+ });
353
+ });
354
+ child.on("error", (error) => {
355
+ clearTimeout(timer);
356
+ resolve14({
357
+ status: "failed",
358
+ command,
359
+ source,
360
+ exitCode: null,
361
+ signal: null,
362
+ durationMs: Date.now() - startedAt,
363
+ output: `${output.trim()}
364
+ ${error.message}`.trim()
365
+ });
366
+ });
367
+ });
368
+ }
369
+ async function loadGraphifyMapOrFallback(graphifyPath, repositoryRoot) {
370
+ try {
371
+ return { map: await loadGraphifyMap(graphifyPath, repositoryRoot), warning: null };
372
+ } catch (error) {
373
+ return {
374
+ map: await buildArchitectureMap(repositoryRoot),
375
+ warning: `Graphify graph unusable, fell back to the import scan: ${error.message}`
376
+ };
377
+ }
378
+ }
379
+ function emptyAssessment() {
380
+ return {
381
+ items: [],
382
+ stats: { pathsConsidered: 0, itemsProduced: 0, signalsRun: 0, perSignal: [] }
383
+ };
384
+ }
385
+ function emptySuggestions() {
386
+ return {
387
+ suggestions: [],
388
+ stats: { pathsConsidered: 0, produced: 0, byCategory: {}, bySeverity: {} }
389
+ };
390
+ }
391
+ async function resolveRelevantPlan(store, namespace, planId) {
392
+ if (planId) {
393
+ try {
394
+ return await store.readPlan(planId);
395
+ } catch (error) {
396
+ if (error.code === "ENOENT") return null;
397
+ throw error;
398
+ }
399
+ }
400
+ return new PlanLifecycle(store).current(namespace);
401
+ }
402
+ function summarizePlan(plan, paths) {
403
+ if (!plan) return null;
404
+ const plannedPaths = plan.filesToTouch.map((file) => file.path).sort();
405
+ const plannedSet = new Set(plannedPaths);
406
+ const unexpectedPaths = paths.filter((path) => !plannedSet.has(path));
407
+ const plannedTests = plan.testsToAdd.map((test) => test.path).sort();
408
+ const changed = new Set(paths);
409
+ const missingPlannedTests = plannedTests.filter((path) => !changed.has(path));
410
+ return {
411
+ id: plan.id,
412
+ title: plan.title,
413
+ status: plan.status,
414
+ plannedPaths,
415
+ unexpectedPaths,
416
+ plannedTests,
417
+ missingPlannedTests
418
+ };
419
+ }
420
+ function buildVerification(paths, plan, suggestions) {
421
+ const changedTestFiles = paths.filter(isTestFile);
422
+ const changedCodeFiles = paths.filter((path) => isCodeFile(path) && !isTestFile(path));
423
+ const uncovered = new Set(
424
+ suggestions.suggestions.filter((suggestion) => suggestion.category === "missing_test" && suggestion.path).map((suggestion) => suggestion.path)
425
+ );
426
+ const plannedTests = plan?.plannedTests ?? [];
427
+ const missingPlannedTests = plan?.missingPlannedTests ?? [];
428
+ return {
429
+ changedCodeFiles,
430
+ changedTestFiles,
431
+ uncoveredCodeFiles: changedCodeFiles.filter((path) => uncovered.has(path)),
432
+ plannedTests,
433
+ missingPlannedTests,
434
+ readyToVerify: changedTestFiles.length > 0 || changedCodeFiles.length === 0
435
+ };
436
+ }
437
+ function countDependents(edges, paths) {
438
+ const targets = new Set(paths);
439
+ const counts = {};
440
+ for (const path of paths) counts[path] = 0;
441
+ for (const edge of edges) {
442
+ if (targets.has(edge.to)) counts[edge.to] = (counts[edge.to] ?? 0) + 1;
443
+ }
444
+ return counts;
445
+ }
446
+ function getStatus(paths, highestRisk, suggestions, verification) {
447
+ if (paths.length === 0) return "no_changes";
448
+ if (highestRisk >= 75 || suggestions.suggestions.some((item) => item.severity === "high")) {
449
+ return "blocked";
450
+ }
451
+ if (highestRisk >= 50 || suggestions.suggestions.some((item) => item.severity === "medium") || verification.uncoveredCodeFiles.length > 0 || verification.missingPlannedTests.length > 0) {
452
+ return "review";
453
+ }
454
+ return "ready";
455
+ }
456
+ function isCodeFile(path) {
457
+ return CODE_EXTENSIONS.has(extname(path).toLowerCase());
458
+ }
459
+ function isTestFile(path) {
460
+ return /(^|\/)(__tests__\/|.*\.(test|spec)\.)/.test(path);
461
+ }
462
+
463
+ // src/cli/commands/change.ts
464
+ function registerChangeCommand(program, runSafely2) {
465
+ const change = program.command("change").description("Assess and verify the current change.");
466
+ change.command("report", { isDefault: true }).description("Report whether the current change is ready, needs review, or is blocked.").option("--paths <paths>", "Comma-separated repo-relative paths to review.").option("--plan <id>", "Review the files listed in a plan.").option("--no-git", "Do not include the current Git change set.").option("--graphify <path>", "Use a Graphify graph.json for deeper architecture edges.").option("--json", "Print machine-readable JSON.").action(async (opts) => {
467
+ await runSafely2(async () => {
468
+ const report = await buildChangeReport(toInput(opts, await resolveNamespaceFrom()));
469
+ if (opts.json) {
470
+ console.log(JSON.stringify(report, null, 2));
471
+ return;
472
+ }
473
+ printReport(report);
474
+ });
475
+ });
476
+ change.command("verify").description("Run the detected test command and report whether the change is verified.").option("--paths <paths>", "Comma-separated repo-relative paths to review.").option("--plan <id>", "Review the files listed in a plan.").option("--no-git", "Do not include the current Git change set.").option("--graphify <path>", "Use a Graphify graph.json for deeper architecture edges.").option("--command <command>", "Override the detected test command.").option("--timeout <ms>", "Stop the test command after this many milliseconds.", Number).option("--json", "Print machine-readable JSON.").action(async (opts) => {
477
+ await runSafely2(async () => {
478
+ const result = await verifyChange({
479
+ ...toInput(opts, await resolveNamespaceFrom()),
480
+ ...opts.command ? { command: opts.command } : {},
481
+ ...opts.timeout !== void 0 ? { timeoutMs: opts.timeout } : {}
482
+ });
483
+ if (opts.json) {
484
+ console.log(JSON.stringify(result, null, 2));
485
+ } else {
486
+ printVerification(result);
487
+ }
488
+ if (result.test.status !== "passed") process.exitCode = 1;
489
+ });
490
+ });
491
+ }
492
+ function toInput(opts, namespace) {
493
+ return {
494
+ namespace,
495
+ ...opts.paths ? { paths: parsePaths(opts.paths) } : {},
496
+ ...opts.plan ? { planId: opts.plan } : {},
497
+ ...opts.git !== void 0 ? { useGit: opts.git } : {},
498
+ ...opts.graphify ? { graphifyPath: opts.graphify } : {}
499
+ };
500
+ }
501
+ function parsePaths(value) {
502
+ return value.split(",").map((path) => path.trim()).filter(Boolean);
503
+ }
504
+ function printReport(report) {
505
+ console.log(`${statusBadge(report.status)} ${pc.bold("change report")}`);
506
+ if (report.paths.length === 0) {
507
+ console.log(pc.dim(" no changed paths detected"));
508
+ return;
509
+ }
510
+ console.log(` ${report.paths.length} path(s): ${report.paths.join(", ")}`);
511
+ console.log(` highest risk: ${report.risk.highestScore}`);
512
+ console.log(
513
+ ` architecture: ${report.architecture.totalDependents} direct dependent edge(s) across the change set (${report.architecture.source})`
514
+ );
515
+ if (report.architecture.warning) console.log(pc.yellow(` ${report.architecture.warning}`));
516
+ if (report.plan) {
517
+ console.log(` plan: ${report.plan.id} ${report.plan.title} (${report.plan.status})`);
518
+ if (report.plan.unexpectedPaths.length > 0)
519
+ console.log(` plan drift: ${report.plan.unexpectedPaths.join(", ")}`);
520
+ if (report.plan.missingPlannedTests.length > 0)
521
+ console.log(` planned tests still missing: ${report.plan.missingPlannedTests.join(", ")}`);
522
+ } else {
523
+ console.log(pc.dim(" plan: no active plan"));
524
+ }
525
+ console.log(
526
+ ` verification: ${report.verification.changedTestFiles.length} test file(s) changed; ${report.verification.uncoveredCodeFiles.length} code file(s) without a discovered test`
527
+ );
528
+ if (report.suggestions.length > 0) {
529
+ console.log("\n findings:");
530
+ for (const suggestion of report.suggestions.slice(0, 8)) {
531
+ console.log(` ${severityBadge(suggestion.severity)} ${suggestion.title}`);
532
+ console.log(pc.dim(` ${suggestion.detail}`));
533
+ }
534
+ } else {
535
+ console.log(pc.dim("\n findings: none"));
536
+ }
537
+ }
538
+ function printVerification(result) {
539
+ printReport(result.report);
540
+ console.log(`
541
+ ${testBadge(result.test.status)} verification`);
542
+ console.log(` command: ${result.test.command ?? "none detected"}`);
543
+ if (result.test.durationMs > 0) console.log(` duration: ${result.test.durationMs}ms`);
544
+ if (result.test.output) {
545
+ console.log(" output:");
546
+ for (const line of result.test.output.split("\n")) console.log(` ${line}`);
547
+ }
548
+ }
549
+ function statusBadge(status) {
550
+ if (status === "blocked") return pc.red("[blocked]");
551
+ if (status === "review") return pc.yellow("[review ]");
552
+ if (status === "ready") return pc.green("[ready ]");
553
+ return pc.dim("[no diff ]");
554
+ }
555
+ function severityBadge(severity) {
556
+ if (severity === "high") return pc.red("[high]");
557
+ if (severity === "medium") return pc.yellow("[med ]");
558
+ return pc.dim(`[${severity.padEnd(5)}]`);
559
+ }
560
+ function testBadge(status) {
561
+ if (status === "passed") return pc.green("[passed ]");
562
+ if (status === "timed_out") return pc.red("[timeout]");
563
+ if (status === "failed") return pc.red("[failed ]");
564
+ return pc.yellow("[not run]");
565
+ }
566
+
567
+ // src/cli/commands/claude-desktop.ts
568
+ import { existsSync } from "fs";
569
+ import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
570
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
571
+ import { fileURLToPath } from "url";
572
+ function claudeDesktopConfigPath(home = process.env.HOME ?? "") {
573
+ if (process.platform === "darwin") {
574
+ return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
575
+ }
576
+ if (process.platform === "win32") {
577
+ return join3(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
578
+ }
579
+ return join3(home, ".config", "Claude", "claude_desktop_config.json");
580
+ }
581
+ async function readClaudeConfig(path) {
582
+ if (!existsSync(path)) return {};
583
+ try {
584
+ return JSON.parse(await readFile2(path, "utf8"));
585
+ } catch (error) {
586
+ throw new Error(
587
+ `Failed to parse Claude Desktop config at ${path}: ${error.message}`
588
+ );
589
+ }
590
+ }
591
+ async function writeClaudeConfig(path, config) {
592
+ await mkdir2(dirname2(path), { recursive: true });
593
+ await writeAtomic(path, `${JSON.stringify(config, null, 2)}
594
+ `, { mode: 384 });
595
+ }
596
+ function resolveCliEntrypoint() {
597
+ const fromArgv = process.argv[1];
598
+ if (fromArgv && existsSync(fromArgv)) return fromArgv;
599
+ const here = dirname2(fileURLToPath(import.meta.url));
600
+ const candidates = [
601
+ resolve2(here, "index.js"),
602
+ // bundled: here = dist/cli/
603
+ resolve2(here, "..", "cli", "index.js"),
604
+ // source: here = src/cli/commands/
605
+ resolve2(here, "..", "index.js")
606
+ ];
607
+ for (const candidate of candidates) {
608
+ if (existsSync(candidate)) return candidate;
609
+ }
610
+ return fromArgv ?? candidates[0] ?? "";
611
+ }
612
+ function buildEntry(options) {
613
+ const env = {
614
+ CODEBUDDY_NAMESPACE: options.namespace
615
+ };
616
+ if (options.projectRoot) env.CODEBUDDY_PROJECT_ROOT = options.projectRoot;
617
+ if (options.hfToken) env.HF_TOKEN = options.hfToken;
618
+ if (options.groqApiKey) env.GROQ_API_KEY = options.groqApiKey;
619
+ if (options.databaseUrl) env.DATABASE_URL = options.databaseUrl;
620
+ return {
621
+ command: "node",
622
+ args: [resolveCliEntrypoint(), "serve"],
623
+ env
624
+ };
625
+ }
626
+ async function installClaudeEntry(options) {
627
+ const path = claudeDesktopConfigPath();
628
+ const config = await readClaudeConfig(path);
629
+ config.mcpServers ??= {};
630
+ const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
631
+ const existed = config.mcpServers[serverKey] !== void 0;
632
+ config.mcpServers[serverKey] = buildEntry(options);
633
+ await writeClaudeConfig(path, config);
634
+ return { path, serverKey, created: !existed };
635
+ }
636
+ async function removeClaudeEntry(serverKey) {
637
+ const path = claudeDesktopConfigPath();
638
+ const config = await readClaudeConfig(path);
639
+ if (!config.mcpServers || !(serverKey in config.mcpServers)) {
640
+ return { removed: false, path };
641
+ }
642
+ delete config.mcpServers[serverKey];
643
+ await writeClaudeConfig(path, config);
644
+ return { removed: true, path };
645
+ }
646
+ async function listClaudeEntries() {
647
+ const path = claudeDesktopConfigPath();
648
+ const config = await readClaudeConfig(path);
649
+ const entries = Object.entries(config.mcpServers ?? {}).filter(([key]) => key === "codebuddy" || key.startsWith("codebuddy-")).map(([key, entry]) => ({
650
+ key,
651
+ namespace: entry.env?.CODEBUDDY_NAMESPACE ?? null,
652
+ databaseUrl: entry.env?.DATABASE_URL ?? null
653
+ }));
654
+ return { path, entries };
655
+ }
656
+
657
+ // src/cli/commands/clients.ts
658
+ import { relative } from "path";
659
+ import pc2 from "picocolors";
660
+
661
+ // src/clients/adapters.ts
662
+ var AUTOMATION_MODES = ["agent", "hooks", "mcp-only", "lean"];
663
+ var MODE_LABELS = {
664
+ agent: "agent-driven",
665
+ hooks: "hook-driven",
666
+ "mcp-only": "MCP-only",
667
+ lean: "Lean (token reduction)"
668
+ };
669
+ var LIFECYCLE_EVENTS = ["prompt", "edit", "turn-end", "compaction"];
670
+ var CLIENT_IDS = ["claude", "codex", "opencode", "freebuff", "hermes"];
671
+ var CLIENT_ADAPTERS = {
672
+ claude: {
673
+ name: "Claude Code",
674
+ host: { bin: "claude", verified: "2.1.287" },
675
+ rulesFile: "CLAUDE.md",
676
+ mcp: { file: ".mcp.json", format: "mcpServers" },
677
+ hookEvents: LIFECYCLE_EVENTS,
678
+ hooks: "claude-settings",
679
+ 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",
680
+ defersMcpSchemas: true
681
+ },
682
+ codex: {
683
+ name: "Codex",
684
+ host: { bin: "codex", verified: "0.163.0" },
685
+ rulesFile: "AGENTS.md",
686
+ mcp: { global: "codex", manual: "codebuddy codex install (global ~/.codex/config.toml)" },
687
+ hookEvents: [],
688
+ gap: "no CodeBuddy hook adapter for Codex yet",
689
+ framing: "agent mode only; AGENTS.md is part of the instructions every request"
690
+ },
691
+ opencode: {
692
+ name: "OpenCode",
693
+ host: { bin: "opencode", verified: "1.18.32" },
694
+ rulesFile: "AGENTS.md",
695
+ // opencode.jsonc is used instead when it is the only one present.
696
+ mcp: { file: "opencode.json", format: "opencode" },
697
+ // Plugin bridge: chat.message (prompt), tool.execute.after (edit),
698
+ // session.idle (turn-end), session.compacted.
699
+ hookEvents: LIFECYCLE_EVENTS,
700
+ hooks: "opencode-plugin",
701
+ framing: "block appended verbatim as a synthetic text part that stays in history; unchanged lines are not resent"
702
+ },
703
+ freebuff: {
704
+ name: "Freebuff",
705
+ // Not installed; checked against source CodebuffAI/freebuff@f9ae64dd.
706
+ host: { bin: "freebuff", verified: "source f9ae64dd" },
707
+ rulesFile: "AGENTS.md",
708
+ mcp: { file: ".agents/mcp.json", format: "mcpServers" },
709
+ hookEvents: [],
710
+ 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)",
711
+ note: "reads AGENTS.md, and CLAUDE.md only in a directory without one (common/src/constants/knowledge.ts)",
712
+ framing: "agent mode only; AGENTS.md is sent as a fenced block under a fixed project-instructions header"
713
+ },
714
+ hermes: {
715
+ name: "Hermes Agent",
716
+ host: { bin: "hermes", verified: "0.17.0" },
717
+ rulesFile: "AGENTS.md",
718
+ mcp: {
719
+ global: "hermes",
720
+ manual: "add a codebuddy entry under mcp_servers in ~/.hermes/config.yaml"
721
+ },
722
+ // Project Python plugin: pre_llm_call (prompt), post_tool_call (edit),
723
+ // on_session_end (turn-end). Compaction has no plugin hook; the context is
724
+ // resent in full every turn and the session ledger reset with it.
725
+ hookEvents: LIFECYCLE_EVENTS,
726
+ hooks: "hermes-plugin",
727
+ ephemeralContext: true,
728
+ framing: "block appended to this turn's user message only and never persisted, so it is resent in full every turn",
729
+ 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"
730
+ }
731
+ };
732
+ function parseClient(value) {
733
+ const client = value ?? "claude";
734
+ if (!CLIENT_IDS.includes(client)) {
735
+ throw new Error(`Unknown client "${client}". Use one of: ${CLIENT_IDS.join(", ")}.`);
736
+ }
737
+ return client;
738
+ }
739
+ function parseMode(value) {
740
+ if (!AUTOMATION_MODES.includes(value)) {
741
+ throw new Error(`Unknown mode "${value}". Use one of: ${AUTOMATION_MODES.join(", ")}.`);
742
+ }
743
+ return value;
744
+ }
745
+ function supportedModes(adapter) {
746
+ return adapter.hookEvents.length === LIFECYCLE_EVENTS.length ? [...AUTOMATION_MODES] : ["agent", "mcp-only"];
747
+ }
748
+ function sharedRulesReaders(client) {
749
+ const file = CLIENT_ADAPTERS[client].rulesFile;
750
+ return CLIENT_IDS.filter((id) => id !== client && CLIENT_ADAPTERS[id].rulesFile === file);
751
+ }
752
+
753
+ // src/clients/cost.ts
754
+ import { mkdtemp, rm as rm3 } from "fs/promises";
755
+ import { tmpdir } from "os";
756
+ import { join as join8, resolve as resolve7 } from "path";
757
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
758
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
759
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
760
+
761
+ // src/templates/install.ts
762
+ import { readFile as readFile7, rm as rm2 } from "fs/promises";
763
+ import { join as join7, resolve as resolve6 } from "path";
764
+
765
+ // src/hooks/hermes-plugin.ts
766
+ import { readFile as readFile4 } from "fs/promises";
767
+ import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
768
+
769
+ // src/hooks/managed-plugin.ts
770
+ import { mkdir as mkdir3, readFile as readFile3, rm, rmdir } from "fs/promises";
771
+ import { dirname as dirname3 } from "path";
772
+ async function read(path) {
773
+ try {
774
+ return await readFile3(path, "utf8");
775
+ } catch (error) {
776
+ if (error.code === "ENOENT") return null;
777
+ throw error;
778
+ }
779
+ }
780
+ async function managedPluginInstalled(plugin3) {
781
+ for (const file of plugin3.files) {
782
+ if (!(await read(file.path))?.startsWith(plugin3.marker)) return false;
783
+ }
784
+ return true;
785
+ }
786
+ async function setManagedPlugin(plugin3, enabled) {
787
+ const current = await Promise.all(plugin3.files.map((file) => read(file.path)));
788
+ const foreign = plugin3.files.find((_, i) => {
789
+ const text3 = current[i];
790
+ return text3 != null && !text3.startsWith(plugin3.marker);
791
+ });
792
+ if (foreign) {
793
+ if (!enabled) return false;
794
+ throw new Error(
795
+ `${foreign.path} exists and is not managed by CodeBuddy; move it aside and re-run.`
796
+ );
797
+ }
798
+ let changed = false;
799
+ for (const [i, file] of plugin3.files.entries()) {
800
+ if (enabled && current[i] !== file.content) {
801
+ await mkdir3(dirname3(file.path), { recursive: true });
802
+ await writeAtomic(file.path, file.content, { mode: 420 });
803
+ changed = true;
804
+ } else if (!enabled && current[i] != null) {
805
+ await rm(file.path, { force: true });
806
+ changed = true;
807
+ }
808
+ }
809
+ if (!enabled && changed) {
810
+ for (const dir of plugin3.dirs) await rmdir(dir).catch(() => {
811
+ });
812
+ }
813
+ return changed;
814
+ }
815
+
816
+ // src/hooks/hermes-plugin.ts
817
+ var HERMES_PLUGIN_MARKER = "# codebuddy-managed: hermes-plugin";
818
+ var HERMES_PLUGIN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1
819
+ name: codebuddy
820
+ version: 1.0.0
821
+ kind: standalone
822
+ description: CodeBuddy recall and capture bridge (codebuddy clients install hermes --mode hooks).
823
+ provides_hooks:
824
+ - pre_llm_call
825
+ - post_tool_call
826
+ - post_llm_call
827
+ - on_session_end
828
+ `;
829
+ var HERMES_LEAN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1 lean
830
+ name: codebuddy
831
+ version: 1.0.0
832
+ kind: standalone
833
+ description: CodeBuddy lean bridge (codebuddy clients install hermes --mode lean).
834
+ provides_hooks:
835
+ - pre_llm_call
836
+ `;
837
+ var HERMES_HOOK_FN = `def _hook(command, payload):
838
+ """Run one fail-soft CodeBuddy hook; its stdout, or "" on any failure."""
839
+ try:
840
+ done = subprocess.run(
841
+ ["codebuddy", "hooks", command],
842
+ input=json.dumps({"cwd": ROOT, **payload}),
843
+ capture_output=True,
844
+ text=True,
845
+ timeout=TIMEOUT_S[command],
846
+ cwd=ROOT,
847
+ )
848
+ except Exception:
849
+ return ""
850
+ return done.stdout if done.returncode == 0 else ""
851
+
852
+
853
+ `;
854
+ function renderHermesPlugin(variant) {
855
+ return variant === "lean" ? `${HERMES_PLUGIN_MARKER} v1 lean
856
+ # Generated by "codebuddy clients install hermes --mode lean"; removed by
857
+ # "codebuddy clients uninstall hermes". Local edits are overwritten.
858
+ """CodeBuddy lean bridge: forwards each prompt to "codebuddy hooks lean"."""
859
+
860
+ import json
861
+ import subprocess
862
+ from pathlib import Path
863
+
864
+ # <root>/.hermes/plugins/codebuddy/__init__.py
865
+ ROOT = str(Path(__file__).absolute().parents[3])
866
+ TIMEOUT_S = {"lean": 2}
867
+
868
+
869
+ ${HERMES_HOOK_FN}def _pre_llm_call(session_id="", user_message="", **_):
870
+ if not isinstance(user_message, str) or not user_message.strip():
871
+ return None
872
+ payload = {
873
+ "session_id": session_id,
874
+ "hook_event_name": "pre_llm_call",
875
+ "ephemeral_context": True,
876
+ "prompt": user_message,
877
+ }
878
+ block = _hook("lean", payload).strip()
879
+ return {"context": block} if block else None
880
+
881
+
882
+ def register(ctx):
883
+ ctx.register_hook("pre_llm_call", _pre_llm_call)
884
+ ` : `${HERMES_PLUGIN_MARKER} v1
885
+ # Generated by "codebuddy clients install hermes --mode hooks"; removed by
886
+ # "codebuddy clients uninstall hermes". Local edits are overwritten.
887
+ """CodeBuddy lifecycle bridge: forwards Hermes hooks to "codebuddy hooks <command>"."""
888
+
889
+ import json
890
+ import subprocess
891
+ import threading
892
+ from pathlib import Path
893
+
894
+ # <root>/.hermes/plugins/codebuddy/__init__.py
895
+ ROOT = str(Path(__file__).absolute().parents[3])
896
+ EDIT_TOOLS = {"write_file", "patch"}
897
+ TIMEOUT_S = {"context": 20, "stage": 10, "capture": 60}
898
+ _lock = threading.Lock()
899
+ _turns = {}
900
+
901
+
902
+ ${HERMES_HOOK_FN}def _turn(turn_id, session_id):
903
+ # Compression can rotate the session id mid-turn; a turn keeps its first id.
904
+ with _lock:
905
+ return _turns.setdefault(
906
+ turn_id or session_id, {"session": session_id, "text": "", "commands": []}
907
+ )
908
+
909
+
910
+ def _pre_llm_call(session_id="", turn_id="", user_message="", **_):
911
+ _turn(turn_id, session_id)
912
+ payload = {"session_id": session_id, "hook_event_name": "pre_llm_call", "ephemeral_context": True}
913
+ if isinstance(user_message, str) and user_message.strip():
914
+ payload["prompt"] = user_message
915
+ block = _hook("context", payload).strip()
916
+ return {"context": block} if block else None
917
+
918
+
919
+ def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id="", **_):
920
+ turn = _turn(turn_id, session_id)
921
+ args = args if isinstance(args, dict) else {}
922
+ if tool_name in EDIT_TOOLS:
923
+ # Paths only: write_file content never needs to cross the pipe.
924
+ tool_input = {k: args[k] for k in ("path", "mode", "patch") if k in args}
925
+ _hook("stage", {
926
+ "session_id": turn["session"],
927
+ "hook_event_name": "post_tool_call",
928
+ "tool_name": tool_name,
929
+ "tool_input": tool_input,
930
+ })
931
+ elif tool_name == "terminal" and isinstance(args.get("command"), str):
932
+ exit_code = None
933
+ if not args.get("background"): # a background process has not finished
934
+ try:
935
+ exit_code = json.loads(result).get("exit_code")
936
+ except Exception:
937
+ pass
938
+ turn["commands"].append({
939
+ "command": args["command"],
940
+ "exit_code": exit_code if type(exit_code) is int else None,
941
+ })
942
+
943
+
944
+ def _post_llm_call(session_id="", turn_id="", assistant_response="", **_):
945
+ if isinstance(assistant_response, str) and assistant_response.strip():
946
+ _turn(turn_id, session_id)["text"] = assistant_response
947
+
948
+
949
+ def _on_session_end(session_id="", turn_id="", **_):
950
+ with _lock:
951
+ turn = _turns.pop(turn_id or session_id, None)
952
+ turn = turn or {"session": session_id, "text": "", "commands": []}
953
+ payload = {"session_id": turn["session"], "hook_event_name": "on_session_end"}
954
+ if turn["text"]:
955
+ payload["last_assistant_message"] = turn["text"]
956
+ if turn["commands"]:
957
+ payload["shell_commands"] = turn["commands"][-20:]
958
+ _hook("capture", payload)
959
+
960
+
961
+ def register(ctx):
962
+ ctx.register_hook("pre_llm_call", _pre_llm_call)
963
+ ctx.register_hook("post_tool_call", _post_tool_call)
964
+ ctx.register_hook("post_llm_call", _post_llm_call)
965
+ ctx.register_hook("on_session_end", _on_session_end)
966
+ `;
967
+ }
968
+ var HERMES_PLUGIN_SOURCE = renderHermesPlugin("full");
969
+ function hermesPluginDir(repositoryRoot) {
970
+ return join4(resolve3(repositoryRoot), ".hermes", "plugins", "codebuddy");
971
+ }
972
+ function plugin(repositoryRoot, variant = "full") {
973
+ const dir = hermesPluginDir(repositoryRoot);
974
+ return {
975
+ marker: HERMES_PLUGIN_MARKER,
976
+ files: [
977
+ {
978
+ path: join4(dir, "plugin.yaml"),
979
+ content: variant === "lean" ? HERMES_LEAN_MANIFEST : HERMES_PLUGIN_MANIFEST
980
+ },
981
+ { path: join4(dir, "__init__.py"), content: renderHermesPlugin(variant) }
982
+ ],
983
+ dirs: [dir, dirname4(dir), dirname4(dirname4(dir))]
984
+ };
985
+ }
986
+ function hermesPluginInstalled(repositoryRoot) {
987
+ return managedPluginInstalled(plugin(repositoryRoot));
988
+ }
989
+ function setHermesPlugin(repositoryRoot, enabled, variant = "full") {
990
+ return setManagedPlugin(plugin(repositoryRoot, variant), enabled);
991
+ }
992
+ async function hermesPluginVariant(repositoryRoot) {
993
+ if (!await hermesPluginInstalled(repositoryRoot)) return null;
994
+ const source = await readFile4(join4(hermesPluginDir(repositoryRoot), "__init__.py"), "utf8");
995
+ return source.startsWith(`${HERMES_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
996
+ }
997
+
998
+ // src/hooks/opencode-plugin.ts
999
+ import { readFile as readFile5 } from "fs/promises";
1000
+ import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
1001
+ var OPENCODE_PLUGIN_MARKER = "// codebuddy-managed: opencode-plugin";
1002
+ var OPENCODE_RUNTIME = `const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1003
+ let lastMs = 0;
1004
+ let counter = 0;
1005
+
1006
+ /** Run one fail-soft CodeBuddy hook; resolves to its stdout, "" on any failure. */
1007
+ function hook(command, payload) {
1008
+ return new Promise((done) => {
1009
+ let out = "";
1010
+ let child;
1011
+ try {
1012
+ child = spawn("codebuddy", ["hooks", command], {
1013
+ cwd: payload.cwd,
1014
+ stdio: ["pipe", "pipe", "ignore"],
1015
+ });
1016
+ } catch {
1017
+ done("");
1018
+ return;
1019
+ }
1020
+ const timer = setTimeout(() => child.kill(), TIMEOUT_MS[command]);
1021
+ child.stdout.setEncoding("utf8");
1022
+ child.stdout.on("data", (chunk) => {
1023
+ out += chunk;
1024
+ });
1025
+ child.on("error", () => {
1026
+ clearTimeout(timer);
1027
+ done("");
1028
+ });
1029
+ child.on("close", (code) => {
1030
+ clearTimeout(timer);
1031
+ done(code === 0 ? out : "");
1032
+ });
1033
+ child.stdin.on("error", () => {});
1034
+ child.stdin.end(JSON.stringify(payload));
1035
+ });
1036
+ }
1037
+
1038
+ /** An ascending OpenCode part id (prt_ + 48-bit time/counter hex + 14 base62). */
1039
+ function partId() {
1040
+ const now = Date.now();
1041
+ if (now !== lastMs) {
1042
+ lastMs = now;
1043
+ counter = 0;
1044
+ }
1045
+ counter += 1;
1046
+ const time = (BigInt(now) * 4096n + BigInt(counter)) & 0xffffffffffffn;
1047
+ let random = "";
1048
+ for (const byte of crypto.getRandomValues(new Uint8Array(14))) random += ID_CHARS[byte % 62];
1049
+ return "prt_" + time.toString(16).padStart(12, "0") + random;
1050
+ }
1051
+
1052
+ `;
1053
+ function renderOpencodePlugin(variant) {
1054
+ return variant === "lean" ? `${OPENCODE_PLUGIN_MARKER} v1 lean
1055
+ // Generated by \`codebuddy clients install opencode --mode lean\`; removed by
1056
+ // \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
1057
+ import { spawn } from "node:child_process";
1058
+
1059
+ const TIMEOUT_MS = { lean: 2000, reset: 10000 };
1060
+ ${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
1061
+ export const CodeBuddyPlugin = async ({ directory }) => {
1062
+ const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
1063
+
1064
+ return {
1065
+ "chat.message": async (input, output) => {
1066
+ const prompt = output.parts
1067
+ .flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
1068
+ .join("\\n")
1069
+ .trim();
1070
+ if (!prompt) return;
1071
+ const block = (await hook("lean", { ...base(input.sessionID, "chat.message"), prompt })).trim();
1072
+ if (!block) return;
1073
+ output.parts.push({
1074
+ id: partId(),
1075
+ sessionID: input.sessionID,
1076
+ messageID: output.message.id,
1077
+ type: "text",
1078
+ text: block,
1079
+ synthetic: true,
1080
+ });
1081
+ },
1082
+ event: async ({ event }) => {
1083
+ if (event.type === "session.compacted") {
1084
+ await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
1085
+ }
1086
+ },
1087
+ };
1088
+ };
1089
+ ` : `${OPENCODE_PLUGIN_MARKER} v1
1090
+ // Generated by \`codebuddy clients install opencode --mode hooks\`; removed by
1091
+ // \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
1092
+ import { spawn } from "node:child_process";
1093
+
1094
+ const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]);
1095
+ const TIMEOUT_MS = { context: 20000, stage: 10000, capture: 60000, reset: 10000 };
1096
+ ${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
1097
+ export const CodeBuddyPlugin = async ({ directory }) => {
1098
+ /** @type {Map<string, { text: string, commands: Array<{ command: string, exit_code: number | null }> }>} */
1099
+ const turns = new Map();
1100
+ const turn = (sessionID) => {
1101
+ if (!turns.has(sessionID)) turns.set(sessionID, { text: "", commands: [] });
1102
+ return turns.get(sessionID);
1103
+ };
1104
+ const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
1105
+
1106
+ return {
1107
+ "chat.message": async (input, output) => {
1108
+ turns.delete(input.sessionID);
1109
+ const prompt = output.parts
1110
+ .flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
1111
+ .join("\\n")
1112
+ .trim();
1113
+ const block = (
1114
+ await hook("context", { ...base(input.sessionID, "chat.message"), ...(prompt ? { prompt } : {}) })
1115
+ ).trim();
1116
+ if (!block) return;
1117
+ output.parts.push({
1118
+ id: partId(),
1119
+ sessionID: input.sessionID,
1120
+ messageID: output.message.id,
1121
+ type: "text",
1122
+ text: block,
1123
+ synthetic: true,
1124
+ });
1125
+ },
1126
+ "tool.execute.after": async (input, output) => {
1127
+ if (EDIT_TOOLS.has(input.tool)) {
1128
+ await hook("stage", {
1129
+ ...base(input.sessionID, "tool.execute.after"),
1130
+ tool_name: input.tool,
1131
+ tool_input: input.args,
1132
+ });
1133
+ } else if (input.tool === "bash" && typeof input.args?.command === "string") {
1134
+ const exit = output.metadata?.exit;
1135
+ turn(input.sessionID).commands.push({
1136
+ command: input.args.command,
1137
+ exit_code: typeof exit === "number" ? exit : null,
1138
+ });
1139
+ }
1140
+ },
1141
+ "experimental.text.complete": async (input, output) => {
1142
+ if (output.text.trim()) turn(input.sessionID).text = output.text;
1143
+ },
1144
+ event: async ({ event }) => {
1145
+ if (event.type === "session.idle") {
1146
+ const sessionID = event.properties.sessionID;
1147
+ const current = turns.get(sessionID);
1148
+ turns.delete(sessionID);
1149
+ await hook("capture", {
1150
+ ...base(sessionID, "session.idle"),
1151
+ ...(current?.text ? { last_assistant_message: current.text } : {}),
1152
+ ...(current?.commands.length ? { shell_commands: current.commands.slice(-20) } : {}),
1153
+ });
1154
+ } else if (event.type === "session.compacted") {
1155
+ await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
1156
+ }
1157
+ },
1158
+ };
1159
+ };
1160
+ `;
1161
+ }
1162
+ var OPENCODE_PLUGIN_SOURCE = renderOpencodePlugin("full");
1163
+ function opencodePluginPath(repositoryRoot) {
1164
+ return join5(resolve4(repositoryRoot), ".opencode", "plugins", "codebuddy.js");
1165
+ }
1166
+ function plugin2(repositoryRoot, variant = "full") {
1167
+ const path = opencodePluginPath(repositoryRoot);
1168
+ return {
1169
+ marker: OPENCODE_PLUGIN_MARKER,
1170
+ files: [{ path, content: renderOpencodePlugin(variant) }],
1171
+ dirs: [dirname5(path), dirname5(dirname5(path))]
1172
+ };
1173
+ }
1174
+ function opencodePluginInstalled(repositoryRoot) {
1175
+ return managedPluginInstalled(plugin2(repositoryRoot));
1176
+ }
1177
+ function setOpencodePlugin(repositoryRoot, enabled, variant = "full") {
1178
+ return setManagedPlugin(plugin2(repositoryRoot, variant), enabled);
1179
+ }
1180
+ async function opencodePluginVariant(repositoryRoot) {
1181
+ if (!await opencodePluginInstalled(repositoryRoot)) return null;
1182
+ const source = await readFile5(opencodePluginPath(repositoryRoot), "utf8");
1183
+ return source.startsWith(`${OPENCODE_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
1184
+ }
1185
+
1186
+ // src/hooks/settings.ts
1187
+ import { mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
1188
+ import { dirname as dirname6, join as join6, resolve as resolve5 } from "path";
1189
+ var HOOK_COMMAND_PREFIX = "codebuddy hooks";
1190
+ var TOOL_GUARD_DEF = {
1191
+ event: "PreToolUse",
1192
+ matcher: "Read|Grep",
1193
+ command: `${HOOK_COMMAND_PREFIX} pretool`
1194
+ };
1195
+ var HOOK_DEFS = [
1196
+ { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} context` },
1197
+ {
1198
+ event: "PostToolUse",
1199
+ matcher: "Edit|Write|MultiEdit",
1200
+ command: `${HOOK_COMMAND_PREFIX} stage`
1201
+ },
1202
+ { event: "Stop", command: `${HOOK_COMMAND_PREFIX} capture` },
1203
+ // After compaction or /clear the model no longer holds earlier context, so
1204
+ // capsules the session ledger marked as sent must be sent again.
1205
+ { event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
1206
+ TOOL_GUARD_DEF
1207
+ ];
1208
+ var LEAN_HOOK_DEFS = [
1209
+ { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} lean` },
1210
+ { event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
1211
+ TOOL_GUARD_DEF
1212
+ ];
1213
+ function claudeSettingsPath(repositoryRoot = process.cwd()) {
1214
+ return join6(resolve5(repositoryRoot), ".claude", "settings.json");
1215
+ }
1216
+ function upsertHooks(settings, variant = "full") {
1217
+ const defs = variant === "lean" ? LEAN_HOOK_DEFS : HOOK_DEFS;
1218
+ const wanted = new Set(defs.map((def) => def.command));
1219
+ const next = { ...settings, hooks: { ...settings.hooks ?? {} } };
1220
+ const hooks = next.hooks;
1221
+ let changed = false;
1222
+ for (const event of Object.keys(hooks)) {
1223
+ const groups = [];
1224
+ for (const group of hooks[event] ?? []) {
1225
+ const kept = (group.hooks ?? []).filter(
1226
+ (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX) || wanted.has(hook.command)
1227
+ );
1228
+ if (kept.length !== (group.hooks ?? []).length) changed = true;
1229
+ if (kept.length > 0) groups.push({ ...group, hooks: kept });
1230
+ }
1231
+ if (groups.length > 0) hooks[event] = groups;
1232
+ else delete hooks[event];
1233
+ }
1234
+ for (const def of defs) {
1235
+ const groups = [...hooks[def.event] ?? []];
1236
+ const existingIndex = groups.findIndex(
1237
+ (group) => group.hooks?.some((hook) => hook.command === def.command)
1238
+ );
1239
+ if (existingIndex === -1) {
1240
+ groups.push({
1241
+ ...def.matcher ? { matcher: def.matcher } : {},
1242
+ hooks: [{ type: "command", command: def.command }]
1243
+ });
1244
+ changed = true;
1245
+ } else {
1246
+ const existing = groups[existingIndex];
1247
+ if (existing && (def.matcher ?? void 0) !== (existing.matcher ?? void 0)) {
1248
+ const { matcher: _oldMatcher, ...rest } = existing;
1249
+ groups[existingIndex] = def.matcher ? { ...rest, matcher: def.matcher } : rest;
1250
+ changed = true;
1251
+ }
1252
+ }
1253
+ hooks[def.event] = groups;
1254
+ }
1255
+ return { settings: next, changed };
1256
+ }
1257
+ function removeHooks(settings) {
1258
+ if (!settings.hooks) return { settings, changed: false };
1259
+ const next = { ...settings, hooks: { ...settings.hooks } };
1260
+ const hooks = next.hooks;
1261
+ let changed = false;
1262
+ for (const event of Object.keys(hooks)) {
1263
+ const keptGroups = [];
1264
+ for (const group of hooks[event] ?? []) {
1265
+ const keptHooks = (group.hooks ?? []).filter(
1266
+ (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX)
1267
+ );
1268
+ if (keptHooks.length !== (group.hooks ?? []).length) changed = true;
1269
+ if (keptHooks.length === 0) continue;
1270
+ keptGroups.push({ ...group, hooks: keptHooks });
1271
+ }
1272
+ if (keptGroups.length === 0) delete hooks[event];
1273
+ else hooks[event] = keptGroups;
1274
+ }
1275
+ return { settings: next, changed };
1276
+ }
1277
+ function hooksInstalled(settings) {
1278
+ const hooks = settings.hooks ?? {};
1279
+ return Object.values(hooks).some(
1280
+ (groups) => groups.some((group) => group.hooks?.some((h) => h.command?.startsWith(HOOK_COMMAND_PREFIX)))
1281
+ );
1282
+ }
1283
+ function hooksVariant(settings) {
1284
+ if (!hooksInstalled(settings)) return null;
1285
+ const leanCommand = `${HOOK_COMMAND_PREFIX} lean`;
1286
+ const lean = Object.values(settings.hooks ?? {}).some(
1287
+ (groups) => groups.some((group) => group.hooks?.some((hook) => hook.command === leanCommand))
1288
+ );
1289
+ return lean ? "lean" : "full";
1290
+ }
1291
+ async function readSettings(path) {
1292
+ try {
1293
+ return JSON.parse(await readFile6(path, "utf8"));
1294
+ } catch (error) {
1295
+ if (error.code === "ENOENT") return {};
1296
+ throw error;
1297
+ }
1298
+ }
1299
+ async function writeSettings(path, settings) {
1300
+ await mkdir4(dirname6(path), { recursive: true });
1301
+ await writeAtomic(path, `${JSON.stringify(settings, null, 2)}
1302
+ `, { mode: 420 });
1303
+ }
1304
+
1305
+ // src/hooks/install.ts
1306
+ var SURFACES = {
1307
+ "claude-settings": {
1308
+ path: claudeSettingsPath,
1309
+ installed: async (root) => hooksInstalled(await readSettings(claudeSettingsPath(root))),
1310
+ variant: async (root) => hooksVariant(await readSettings(claudeSettingsPath(root))),
1311
+ set: async (root, enabled, variant) => {
1312
+ const path = claudeSettingsPath(root);
1313
+ const current = await readSettings(path);
1314
+ const { settings, changed } = enabled ? upsertHooks(current, variant) : removeHooks(current);
1315
+ if (changed) await writeSettings(path, settings);
1316
+ return changed;
1317
+ }
1318
+ },
1319
+ "opencode-plugin": {
1320
+ path: opencodePluginPath,
1321
+ installed: opencodePluginInstalled,
1322
+ variant: opencodePluginVariant,
1323
+ set: setOpencodePlugin
1324
+ },
1325
+ "hermes-plugin": {
1326
+ path: hermesPluginDir,
1327
+ installed: hermesPluginInstalled,
1328
+ variant: hermesPluginVariant,
1329
+ set: setHermesPlugin
1330
+ }
1331
+ };
1332
+ async function setClientHooks(root, client, enabled, variant = "full") {
1333
+ const surface = CLIENT_ADAPTERS[client].hooks;
1334
+ if (!surface) return null;
1335
+ const { path, set } = SURFACES[surface];
1336
+ return { target: path(root), changed: await set(root, enabled, variant) };
1337
+ }
1338
+ async function clientHooksVariant(root, client) {
1339
+ const surface = CLIENT_ADAPTERS[client].hooks;
1340
+ if (!surface) return void 0;
1341
+ return SURFACES[surface].variant(root);
1342
+ }
1343
+ async function clientHooksInstalled(root, client) {
1344
+ const surface = CLIENT_ADAPTERS[client].hooks;
1345
+ return surface ? SURFACES[surface].installed(root) : null;
1346
+ }
1347
+ async function hookedRulesSiblings(root, client) {
1348
+ const file = CLIENT_ADAPTERS[client].rulesFile;
1349
+ const hooked = [];
1350
+ for (const id of CLIENT_IDS) {
1351
+ if (id !== client && CLIENT_ADAPTERS[id].rulesFile === file && await clientHooksInstalled(root, id))
1352
+ hooked.push(id);
1353
+ }
1354
+ return hooked;
1355
+ }
1356
+ async function rulesFileHooked(root, client) {
1357
+ return await clientHooksInstalled(root, client) === true || (await hookedRulesSiblings(root, client)).length > 0;
1358
+ }
1359
+
1360
+ // src/templates/workflow.ts
1361
+ var WORKFLOW_CLIENTS = CLIENT_IDS;
1362
+ var INTRO = [
1363
+ "You have CodeBuddy available as a local, MCP-backed project-context engine.",
1364
+ "Use it to work like a careful senior engineer, not to dump broad context.",
1365
+ ""
1366
+ ];
1367
+ var START_AGENT = [
1368
+ "## At the start of a substantive coding turn",
1369
+ "",
1370
+ "1. When project context is needed, call `context_pack` with the user's task and a token budget (start",
1371
+ " with 4000). It automatically finds relevant files and returns bounded symbols,",
1372
+ " plan, policy, incidents, risks, architecture neighbours, and team facts.",
1373
+ " Reuse evidence already present. For a clearly identified small edit, read the",
1374
+ " needed source directly; fetch context for unknown dependencies or project constraints.",
1375
+ " Read missing function bodies before editing. If safety evidence was omitted,",
1376
+ " narrow the targets or increase the budget before proceeding.",
1377
+ "2. Use `context_bootstrap` only when you need broad project orientation beyond",
1378
+ " the task pack. Use `context_before_edit` when you already have exact paths",
1379
+ " or a plan and need a narrower follow-up.",
1380
+ "3. Prior work from earlier chats arrives as `handoffs` with freshness",
1381
+ " (`changedSince`, `missing`); re-check those files before relying on it.",
1382
+ "4. On repeated context calls in one conversation, pass a stable `sessionId` and",
1383
+ " the previous response's `delivery.deliveryId` as `ack`. Unchanged sections",
1384
+ " are then omitted and listed in `delivery.unchanged`. If earlier output is no",
1385
+ " longer in your context (e.g. after compaction), pass `full: true`.",
1386
+ ""
1387
+ ];
1388
+ var START_HOOKS = [
1389
+ "## At the start of a substantive coding turn",
1390
+ "",
1391
+ "CodeBuddy hooks inject a `CodeBuddy project context (auto-recalled)` block into",
1392
+ "each prompt: project, plan, policies, facts, incidents, handoffs, current",
1393
+ "changes, and risk. Agents without CodeBuddy hooks (no such block in the prompt)",
1394
+ "follow the agent-driven flow: `context_pack` first, `context_after_turn` last.",
1395
+ "",
1396
+ "1. Start from the injected block; do not call `context_bootstrap` for orientation",
1397
+ " it already covers.",
1398
+ "2. Call `context_pack` (or `context_before_edit` with exact paths) only when the",
1399
+ " task needs symbols, neighbours, or file-level evidence the block lacks. Pass the",
1400
+ " block's `session` value as `sessionId` and the previous `delivery.deliveryId`",
1401
+ " as `ack` so unchanged sections are not resent.",
1402
+ "3. Prior work from earlier chats arrives as handoffs with freshness; re-check",
1403
+ " those files before relying on it.",
1404
+ ""
1405
+ ];
1406
+ var MIDDLE = [
1407
+ "## While implementing",
1408
+ "",
1409
+ "- Prefer the smallest sufficient set of edits; preserve existing behavior.",
1410
+ "- Honor risk warnings and policy rules for the files you touch; call out risk",
1411
+ " before editing sensitive areas.",
1412
+ "- Do not re-request broad context if the task pack already suffices.",
1413
+ "",
1414
+ "## Before finalizing",
1415
+ "",
1416
+ "- Consider calling `code_suggestions` (with `paths`, `planId`, or `useGit`) for",
1417
+ " read-only, evidence-backed quality findings; resolve high-severity ones.",
1418
+ "- Run the smallest relevant verification (tests, lint, typecheck) and fix",
1419
+ " failures before presenting completion.",
1420
+ ""
1421
+ ];
1422
+ var AFTER_AGENT = [
1423
+ "## After a meaningful turn",
1424
+ "",
1425
+ "- Call `context_after_turn` with a concise `summary`, the `changedFiles`, and a",
1426
+ " `planId` if one is active, plus `verification` (commands run and results)",
1427
+ " so the turn's worklog handoff records it. CodeBuddy auto-saves confident facts, decisions,",
1428
+ " and incidents, queues uncertain or sensitive items for review, and drops",
1429
+ " chatter. Never paste secrets, tokens, or credentials into the summary.",
1430
+ ""
1431
+ ];
1432
+ var AFTER_HOOKS = [
1433
+ "## After a meaningful turn",
1434
+ "",
1435
+ "- Do not call `context_after_turn`: the turn-end hook captures the turn once, from",
1436
+ " the real change set and the verification commands you ran. A second capture",
1437
+ " would record a divergent worklog entry.",
1438
+ "- Never paste secrets, tokens, or credentials into your final message; it is the",
1439
+ " capture summary.",
1440
+ ""
1441
+ ];
1442
+ var TAIL = [
1443
+ "## If a tool or service is unavailable (degraded mode)",
1444
+ "",
1445
+ "- A CodeBuddy MCP tool errors or is missing: proceed using the repository",
1446
+ " itself as the source of truth; do not block on it.",
1447
+ "- Postgres is unreachable: the file-based tools (context, plan, risk, map,",
1448
+ " suggest) still work; recall and embeddings may be degraded \u2014 say so instead",
1449
+ " of inventing remembered facts.",
1450
+ "- Retrieval returns no context: read the relevant files directly rather than",
1451
+ " guessing; the working tree is authoritative.",
1452
+ "",
1453
+ "## Never",
1454
+ "",
1455
+ "- Invent behavior the codebase does not support.",
1456
+ "- Ignore failing tests or lint.",
1457
+ "- Make speculative, wide changes without evidence.",
1458
+ "- Save low-confidence project memory as a durable fact."
1459
+ ];
1460
+ function renderBody(mode) {
1461
+ return [
1462
+ ...INTRO,
1463
+ ...mode === "hooks" ? START_HOOKS : START_AGENT,
1464
+ ...MIDDLE,
1465
+ ...mode === "hooks" ? AFTER_HOOKS : AFTER_AGENT,
1466
+ ...TAIL
1467
+ ].join("\n");
1468
+ }
1469
+ function rulesReaders(file) {
1470
+ return CLIENT_IDS.filter((id) => CLIENT_ADAPTERS[id].rulesFile === file).map((id) => CLIENT_ADAPTERS[id].name).join(", ");
1471
+ }
1472
+ function renderWorkflowTemplate(client, mode = "agent") {
1473
+ const file = CLIENT_ADAPTERS[client].rulesFile;
1474
+ return [
1475
+ `# CodeBuddy workflow (${rulesReaders(file)})`,
1476
+ "",
1477
+ "This section is managed by CodeBuddy (`codebuddy clients install <client> --mode <mode>`)",
1478
+ `and lives in \`${file}\`. Edit outside the managed markers to customize.`,
1479
+ `Automation mode: \`${mode}\` (${MODE_LABELS[mode]}).`,
1480
+ "",
1481
+ renderBody(mode)
1482
+ ].join("\n");
1483
+ }
1484
+ function parseWorkflowMode(block) {
1485
+ return block.match(/Automation mode: `(hooks|agent)`/)?.[1] === "hooks" ? "hooks" : "agent";
1486
+ }
1487
+ function defaultRulesFile(client) {
1488
+ return CLIENT_ADAPTERS[client].rulesFile;
1489
+ }
1490
+ function clientDisplayName(client) {
1491
+ return CLIENT_ADAPTERS[client].name;
1492
+ }
1493
+
1494
+ // src/templates/install.ts
1495
+ var START_MARKER = "<!-- codebuddy:workflow:start -->";
1496
+ var END_MARKER = "<!-- codebuddy:workflow:end -->";
1497
+ function renderManagedBlock(client, mode = "agent") {
1498
+ return `${START_MARKER}
1499
+ ${renderWorkflowTemplate(client, mode)}
1500
+ ${END_MARKER}
1501
+ `;
1502
+ }
1503
+ async function installWorkflowRules(options) {
1504
+ const projectRoot = resolve6(options.projectRoot ?? process.cwd());
1505
+ const path = options.targetPath ?? join7(projectRoot, defaultRulesFile(options.client));
1506
+ const existing = await readIfPresent(path);
1507
+ const mode = options.mode ?? await defaultRulesMode(options.client, projectRoot);
1508
+ if (options.onlyIfManaged && (existing === null || !hasManagedBlock(existing))) {
1509
+ return { path, client: options.client, action: "unchanged" };
1510
+ }
1511
+ if (mode === "mcp-only") {
1512
+ const content2 = existing === null ? null : removeBlock(existing);
1513
+ if (content2 === null || content2 === existing) {
1514
+ return { path, client: options.client, action: "unchanged" };
1515
+ }
1516
+ if (content2.trim() === "") await rm2(path, { force: true });
1517
+ else await writeAtomic(path, content2, { mode: 420 });
1518
+ return { path, client: options.client, action: "removed" };
1519
+ }
1520
+ const { content, action } = upsertBlock(existing, renderManagedBlock(options.client, mode));
1521
+ if (action === "unchanged") return { path, client: options.client, action };
1522
+ await writeAtomic(path, content, { mode: 420 });
1523
+ return { path, client: options.client, action };
1524
+ }
1525
+ function upsertBlock(existing, block) {
1526
+ if (existing === null) return { content: block, action: "created" };
1527
+ const start = existing.indexOf(START_MARKER);
1528
+ const end = existing.indexOf(END_MARKER);
1529
+ if (start !== -1 && end !== -1 && end > start) {
1530
+ const before = existing.slice(0, start);
1531
+ const after = existing.slice(end + END_MARKER.length).replace(/^\n/, "");
1532
+ const replaced = `${before}${block}${after}`;
1533
+ return { content: replaced, action: replaced === existing ? "unchanged" : "updated" };
1534
+ }
1535
+ const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
1536
+ return { content: `${existing}${separator}${block}`, action: "added" };
1537
+ }
1538
+ async function defaultRulesMode(client, projectRoot = process.cwd()) {
1539
+ return await rulesFileHooked(projectRoot, client) ? "hooks" : "agent";
1540
+ }
1541
+ async function readManagedMode(path) {
1542
+ const existing = await readIfPresent(path);
1543
+ if (existing === null || !hasManagedBlock(existing)) return null;
1544
+ return parseWorkflowMode(existing.slice(existing.indexOf(START_MARKER)));
1545
+ }
1546
+ function hasManagedBlock(content) {
1547
+ const start = content.indexOf(START_MARKER);
1548
+ return start !== -1 && content.indexOf(END_MARKER) > start;
1549
+ }
1550
+ function removeBlock(existing) {
1551
+ if (!hasManagedBlock(existing)) return existing;
1552
+ const start = existing.indexOf(START_MARKER);
1553
+ const end = existing.indexOf(END_MARKER) + END_MARKER.length;
1554
+ const before = existing.slice(0, start).replace(/\n\n$/, "\n");
1555
+ const after = existing.slice(end).replace(/^\n/, "");
1556
+ return `${before}${after}`;
1557
+ }
1558
+ async function readIfPresent(path) {
1559
+ try {
1560
+ return await readFile7(path, "utf8");
1561
+ } catch (error) {
1562
+ if (error.code === "ENOENT") return null;
1563
+ throw error;
1564
+ }
1565
+ }
1566
+
1567
+ // src/clients/cost.ts
1568
+ async function mcpSchemaTokens(root, namespace) {
1569
+ const server = new McpServer({ name: "codebuddy", version: VERSION });
1570
+ registerCodeBuddyTools(server, {
1571
+ config: { namespace, projectRoot: root },
1572
+ database: {
1573
+ get: () => Promise.reject(new Error("not used")),
1574
+ status: () => ({ available: false, state: "idle" })
1575
+ }
1576
+ });
1577
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1578
+ const client = new Client({ name: "codebuddy-cost", version: VERSION });
1579
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
1580
+ try {
1581
+ const { tools } = await client.listTools();
1582
+ return {
1583
+ full: estimateTokens(tools),
1584
+ listing: estimateTokens(tools.map((tool) => tool.name).join("\n"))
1585
+ };
1586
+ } finally {
1587
+ await client.close();
1588
+ }
1589
+ }
1590
+ async function hookBlockTokens(root, namespace) {
1591
+ const bootstrap = await buildBootstrapContext({ repositoryRoot: root, namespace });
1592
+ const beforeEdit = await buildBeforeEditContext({
1593
+ repositoryRoot: root,
1594
+ namespace,
1595
+ useGit: true
1596
+ }).catch(() => null);
1597
+ const scratch = await mkdtemp(join8(tmpdir(), "codebuddy-cost-"));
1598
+ try {
1599
+ const ledger = new SessionCapsuleLedger(scratch);
1600
+ const render = () => renderContextBlock(bootstrap, beforeEdit, ledger, "cost", true);
1601
+ const first = await render();
1602
+ await ledger.markSent("cost", first.emitted);
1603
+ const later = await render();
1604
+ const pointers = (beforeEdit?.targetPaths ?? []).map((path) => ({ path, symbols: [] }));
1605
+ const renderLean = () => renderLeanBlock({ repositoryRoot: root, prompt: "", pointers, ledger, sessionId: "lean" });
1606
+ const leanFirst = await renderLean();
1607
+ await ledger.markSent("lean", leanFirst.emitted);
1608
+ const leanLater = await renderLean();
1609
+ return {
1610
+ first: estimateTokens(first.text),
1611
+ later: estimateTokens(later.text),
1612
+ lean: { first: estimateTokens(leanFirst.text), later: estimateTokens(leanLater.text) }
1613
+ };
1614
+ } finally {
1615
+ await rm3(scratch, { recursive: true, force: true });
1616
+ }
1617
+ }
1618
+ async function measureClientCosts(projectRoot = process.cwd(), namespace) {
1619
+ const root = resolve7(projectRoot);
1620
+ const ns = namespace ?? await resolveNamespaceFrom(root);
1621
+ const mcpSchema = await mcpSchemaTokens(root, ns);
1622
+ const block = await hookBlockTokens(root, ns);
1623
+ return CLIENT_IDS.flatMap((client) => {
1624
+ const adapter = CLIENT_ADAPTERS[client];
1625
+ return supportedModes(adapter).flatMap((mode) => {
1626
+ if (mode === "mcp-only") return [];
1627
+ if (mode === "lean") {
1628
+ const later2 = adapter.ephemeralContext ? block.lean.first : block.lean.later;
1629
+ return [
1630
+ { client, mode, rules: 0, mcpSchema: 0, firstBlock: block.lean.first, laterBlock: later2 }
1631
+ ];
1632
+ }
1633
+ const hooks = mode === "hooks";
1634
+ const later = adapter.ephemeralContext ? block.first : block.later;
1635
+ return [
1636
+ {
1637
+ client,
1638
+ mode,
1639
+ rules: estimateTokens(renderManagedBlock(client, mode)),
1640
+ mcpSchema: adapter.defersMcpSchemas ? mcpSchema.listing : mcpSchema.full,
1641
+ firstBlock: hooks ? block.first : 0,
1642
+ laterBlock: hooks ? later : 0
1643
+ }
1644
+ ];
1645
+ });
1646
+ });
1647
+ }
1648
+
1649
+ // src/clients/install.ts
1650
+ import { access as access4 } from "fs/promises";
1651
+ import { join as join12, resolve as resolve9 } from "path";
1652
+
1653
+ // src/integrations/project-mcp.ts
1654
+ import { access as access3, mkdir as mkdir5, readFile as readFile8, rm as rm4 } from "fs/promises";
1655
+ import { dirname as dirname7, join as join9, resolve as resolve8 } from "path";
1656
+ async function writeProjectMcpConfig(options) {
1657
+ const path = join9(options.repositoryRoot, ".mcp.json");
1658
+ const current = await readProjectMcpConfig(path);
1659
+ current.mcpServers ??= {};
1660
+ current.mcpServers[`codebuddy-${options.namespace}`] = codebuddyMcpServer(
1661
+ options.repositoryRoot,
1662
+ options.namespace
1663
+ );
1664
+ if (options.graphify) {
1665
+ current.mcpServers.graphify = {
1666
+ command: "python",
1667
+ args: [
1668
+ "-m",
1669
+ "graphify.serve",
1670
+ resolve8(options.repositoryRoot, options.graphPath ?? "graphify-out/graph.json")
1671
+ ],
1672
+ env: { CODEBUDDY_PROJECT_ROOT: options.repositoryRoot }
1673
+ };
1674
+ } else if (current.mcpServers.graphify) {
1675
+ delete current.mcpServers.graphify;
1676
+ }
1677
+ await writeAtomic(path, `${JSON.stringify(current, null, 2)}
1678
+ `, { mode: 420 });
1679
+ return { path, servers: Object.keys(current.mcpServers).sort() };
1680
+ }
1681
+ var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
1682
+ function codebuddyMcpServer(repositoryRoot, namespace) {
1683
+ return {
1684
+ command: "codebuddy",
1685
+ args: ["serve"],
1686
+ env: {
1687
+ CODEBUDDY_PROJECT_ROOT: repositoryRoot,
1688
+ CODEBUDDY_NAMESPACE: namespace,
1689
+ CODEBUDDY_TOOL_PROFILE: "core"
1690
+ }
1691
+ };
1692
+ }
1693
+ async function projectMcpPath(root, file, format) {
1694
+ const path = join9(root, file);
1695
+ if (format !== "opencode" || await exists(path)) return path;
1696
+ return await exists(`${path}c`) ? `${path}c` : path;
1697
+ }
1698
+ async function exists(path) {
1699
+ return access3(path).then(
1700
+ () => true,
1701
+ () => false
1702
+ );
1703
+ }
1704
+ function formatEntry(format, server) {
1705
+ if (format === "mcpServers") return server;
1706
+ return {
1707
+ type: "local",
1708
+ command: [server.command, ...server.args],
1709
+ ...server.env ? { environment: server.env } : {},
1710
+ enabled: true
1711
+ };
1712
+ }
1713
+ function serversKey(format) {
1714
+ return format === "opencode" ? "mcp" : "mcpServers";
1715
+ }
1716
+ async function upsertMcpServer(path, format, name, server) {
1717
+ const existed = await exists(path);
1718
+ const { config, comments } = await readConfig(path);
1719
+ const key = serversKey(format);
1720
+ const servers2 = { ...config[key] ?? {} };
1721
+ const entry = formatEntry(format, server);
1722
+ if (JSON.stringify(servers2[name]) === JSON.stringify(entry)) return "unchanged";
1723
+ if (comments) return "manual";
1724
+ servers2[name] = entry;
1725
+ const next = format === "opencode" && !existed ? { $schema: OPENCODE_SCHEMA, ...config, [key]: servers2 } : { ...config, [key]: servers2 };
1726
+ await mkdir5(dirname7(path), { recursive: true });
1727
+ await writeAtomic(path, `${JSON.stringify(next, null, 2)}
1728
+ `, { mode: 420 });
1729
+ return existed ? "updated" : "created";
1730
+ }
1731
+ async function removeMcpServer(path, format, name) {
1732
+ const { config, comments } = await readConfig(path);
1733
+ const key = serversKey(format);
1734
+ const servers2 = { ...config[key] ?? {} };
1735
+ if (!(name in servers2)) return "unchanged";
1736
+ if (comments) return "manual";
1737
+ delete servers2[name];
1738
+ const next = { ...config, [key]: servers2 };
1739
+ const leftover = Object.keys(next).filter((k) => k !== key && k !== "$schema");
1740
+ if (Object.keys(servers2).length === 0 && leftover.length === 0) await rm4(path, { force: true });
1741
+ else await writeAtomic(path, `${JSON.stringify(next, null, 2)}
1742
+ `, { mode: 420 });
1743
+ return "removed";
1744
+ }
1745
+ async function hasMcpServer(path, format, name) {
1746
+ const { config } = await readConfig(path);
1747
+ return name in (config[serversKey(format)] ?? {});
1748
+ }
1749
+ function parseJsonc(raw) {
1750
+ let comments = false;
1751
+ const json2 = raw.replace(/"(?:[^"\\]|\\.)*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (match) => {
1752
+ if (match.startsWith('"')) return match;
1753
+ comments = true;
1754
+ return " ";
1755
+ }).replace(/"(?:[^"\\]|\\.)*"|,(\s*[}\]])/g, (match, tail) => tail ?? match);
1756
+ return { value: JSON.parse(json2), comments };
1757
+ }
1758
+ async function readConfig(path) {
1759
+ try {
1760
+ const { value, comments } = parseJsonc(await readFile8(path, "utf8"));
1761
+ const config = value && typeof value === "object" ? value : {};
1762
+ return { config, comments };
1763
+ } catch (error) {
1764
+ if (error.code === "ENOENT") return { config: {}, comments: false };
1765
+ throw new Error(`Could not read ${path}: ${error.message}`);
1766
+ }
1767
+ }
1768
+ async function readProjectMcpConfig(path) {
1769
+ return (await readConfig(path)).config;
1770
+ }
1771
+
1772
+ // src/cli/commands/codex.ts
1773
+ import { existsSync as existsSync2 } from "fs";
1774
+ import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
1775
+ import { homedir } from "os";
1776
+ import { dirname as dirname8, join as join10 } from "path";
1777
+ function codexConfigPath(home = homedir()) {
1778
+ return join10(home, ".codex", "config.toml");
1779
+ }
1780
+ async function readCodexConfig(path) {
1781
+ if (!existsSync2(path)) return "";
1782
+ return readFile9(path, "utf8");
1783
+ }
1784
+ async function writeCodexConfig(path, content) {
1785
+ await mkdir6(dirname8(path), { recursive: true });
1786
+ await writeAtomic(path, content, { mode: 384 });
1787
+ }
1788
+ async function installCodexEntry(options) {
1789
+ const path = codexConfigPath();
1790
+ const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
1791
+ const current = await readCodexConfig(path);
1792
+ const existed = hasServerBlock(current, serverKey);
1793
+ const entry = buildEntry(options);
1794
+ if (options.toolProfile)
1795
+ entry.env = { ...entry.env, CODEBUDDY_TOOL_PROFILE: options.toolProfile };
1796
+ const next = upsertServerBlock(current, serverKey, entry);
1797
+ if (next !== current) await writeCodexConfig(path, next);
1798
+ return { path, serverKey, created: !existed, changed: next !== current };
1799
+ }
1800
+ async function removeCodexEntry(serverKey) {
1801
+ const path = codexConfigPath();
1802
+ const current = await readCodexConfig(path);
1803
+ const next = removeServerBlock(current, serverKey);
1804
+ const removed = next !== current;
1805
+ const trimmed = next.trimEnd();
1806
+ if (removed) await writeCodexConfig(path, trimmed ? `${trimmed}
1807
+ ` : "");
1808
+ return { removed, path };
1809
+ }
1810
+ async function listCodexEntries() {
1811
+ const path = codexConfigPath();
1812
+ const config = await readCodexConfig(path);
1813
+ 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) => {
1814
+ const block = getServerBlock(config, key);
1815
+ return {
1816
+ key,
1817
+ namespace: readEnvValue(block, "CODEBUDDY_NAMESPACE"),
1818
+ databaseUrl: readEnvValue(block, "DATABASE_URL")
1819
+ };
1820
+ });
1821
+ return { path, entries };
1822
+ }
1823
+ function upsertServerBlock(config, serverKey, entry) {
1824
+ const trimmed = removeServerBlock(config, serverKey).trimEnd();
1825
+ const block = formatServerBlock(serverKey, entry);
1826
+ return `${trimmed}${trimmed ? "\n\n" : ""}${block}
1827
+ `;
1828
+ }
1829
+ function removeServerBlock(config, serverKey) {
1830
+ const escaped = escapeRegExp(serverKey);
1831
+ const end = String.raw`(?![\s\S])`;
1832
+ const sectionPattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
1833
+ const envPattern = String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
1834
+ return config.replace(new RegExp(sectionPattern, "gm"), "").replace(new RegExp(envPattern, "gm"), "");
1835
+ }
1836
+ function getServerBlock(config, serverKey) {
1837
+ const escaped = escapeRegExp(serverKey);
1838
+ const end = String.raw`(?![\s\S])`;
1839
+ const pattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})|^\[[^\]]+\]\s*$|${end})`;
1840
+ const server = config.match(new RegExp(pattern, "m"))?.[0] ?? "";
1841
+ const env = config.match(
1842
+ new RegExp(
1843
+ String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`,
1844
+ "m"
1845
+ )
1846
+ )?.[0] ?? "";
1847
+ return `${server}
1848
+ ${env}`;
1849
+ }
1850
+ function hasServerBlock(config, serverKey) {
1851
+ return new RegExp(String.raw`^\[mcp_servers\."${escapeRegExp(serverKey)}"\]\s*$`, "m").test(
1852
+ config
1853
+ );
1854
+ }
1855
+ function formatServerBlock(serverKey, entry) {
1856
+ const lines = [
1857
+ `[mcp_servers.${tomlKey(serverKey)}]`,
1858
+ `command = ${tomlString(entry.command)}`,
1859
+ `args = ${tomlArray(entry.args)}`
1860
+ ];
1861
+ if (entry.env && Object.keys(entry.env).length > 0) {
1862
+ lines.push("", `[mcp_servers.${tomlKey(serverKey)}.env]`);
1863
+ for (const [key, value] of Object.entries(entry.env).sort(
1864
+ ([left], [right]) => left.localeCompare(right)
1865
+ )) {
1866
+ lines.push(`${key} = ${tomlString(value)}`);
1867
+ }
1868
+ }
1869
+ return lines.join("\n");
1870
+ }
1871
+ function readEnvValue(block, key) {
1872
+ return block.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
1873
+ }
1874
+ function tomlKey(key) {
1875
+ return `"${key.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
1876
+ }
1877
+ function tomlString(value) {
1878
+ return JSON.stringify(value);
1879
+ }
1880
+ function tomlArray(values) {
1881
+ return `[${values.map(tomlString).join(", ")}]`;
1882
+ }
1883
+ function escapeRegExp(value) {
1884
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1885
+ }
1886
+
1887
+ // src/integrations/hermes-config.ts
1888
+ import { mkdir as mkdir7, readFile as readFile10 } from "fs/promises";
1889
+ import { homedir as homedir2 } from "os";
1890
+ import { dirname as dirname9, join as join11 } from "path";
1891
+ import { isDeepStrictEqual } from "util";
1892
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1893
+ function hermesConfigPath(home = homedir2()) {
1894
+ return join11(process.env.HERMES_HOME ?? join11(home, ".hermes"), "config.yaml");
1895
+ }
1896
+ async function readRaw(path) {
1897
+ try {
1898
+ return await readFile10(path, "utf8");
1899
+ } catch (error) {
1900
+ if (error.code === "ENOENT") return "";
1901
+ throw error;
1902
+ }
1903
+ }
1904
+ function parseConfig(raw) {
1905
+ try {
1906
+ const value = parseYaml(raw) ?? {};
1907
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1908
+ } catch {
1909
+ return null;
1910
+ }
1911
+ }
1912
+ function servers(config) {
1913
+ const value = config.mcp_servers;
1914
+ return value && typeof value === "object" ? value : {};
1915
+ }
1916
+ var isItem = (line) => line.trim() !== "" && !line.trim().startsWith("#");
1917
+ var indentOf = (line) => line.length - line.trimStart().length;
1918
+ function editMcpServers(raw, name, entry) {
1919
+ const lines = raw.split("\n");
1920
+ const header = lines.findIndex((line) => /^mcp_servers:\s*(#.*)?$/.test(line));
1921
+ const render = (indent2) => stringifyYaml({ [name]: entry }).trimEnd().split("\n").map((line) => `${" ".repeat(indent2)}${line}`);
1922
+ if (header === -1) {
1923
+ if (entry === null) return raw;
1924
+ const body = raw === "" || raw.endsWith("\n") ? raw : `${raw}
1925
+ `;
1926
+ return `${body}mcp_servers:
1927
+ ${render(2).join("\n")}
1928
+ `;
1929
+ }
1930
+ let end = header + 1;
1931
+ while (end < lines.length && !/^\S/.test(lines[end] ?? "")) end++;
1932
+ const block = Array.from({ length: end - header - 1 }, (_, i) => header + 1 + i);
1933
+ const items = block.filter((i) => isItem(lines[i] ?? ""));
1934
+ const indent = items.length > 0 ? indentOf(lines[items[0]] ?? "") : 2;
1935
+ const quoted = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1936
+ const keyLine = new RegExp(`^ {${indent}}(?:${quoted}|"${quoted}"|'${quoted}'):\\s*(#.*)?$`);
1937
+ const start = items.find((i) => keyLine.test(lines[i] ?? ""));
1938
+ if (start === void 0) {
1939
+ if (entry === null) return raw;
1940
+ const at = (items.at(-1) ?? header) + 1;
1941
+ lines.splice(at, 0, ...render(indent));
1942
+ return lines.join("\n");
1943
+ }
1944
+ const next = block.find(
1945
+ (i) => i > start && (lines[i] ?? "").trim() !== "" && indentOf(lines[i] ?? "") <= indent
1946
+ );
1947
+ let stop = next ?? end;
1948
+ while (stop > start + 1 && (lines[stop - 1] ?? "").trim() === "") stop--;
1949
+ lines.splice(start, stop - start, ...entry === null ? [] : render(indent));
1950
+ if (entry === null && items.length > 0 && items.every((i) => i >= start && i < stop)) {
1951
+ lines.splice(header, 1);
1952
+ }
1953
+ return lines.join("\n");
1954
+ }
1955
+ async function setHermesMcpServer(path, name, entry) {
1956
+ const raw = await readRaw(path);
1957
+ const before = parseConfig(raw);
1958
+ if (!before) return "manual";
1959
+ const current = servers(before)[name];
1960
+ if (entry === null ? current === void 0 : isDeepStrictEqual(current, entry)) {
1961
+ return "unchanged";
1962
+ }
1963
+ const nextServers = { ...servers(before) };
1964
+ if (entry === null) delete nextServers[name];
1965
+ else nextServers[name] = entry;
1966
+ const { mcp_servers: _, ...rest } = before;
1967
+ const expected = Object.keys(nextServers).length > 0 ? { ...rest, mcp_servers: nextServers } : rest;
1968
+ const next = editMcpServers(raw, name, entry);
1969
+ if (!isDeepStrictEqual(parseConfig(next), expected)) return "manual";
1970
+ await mkdir7(dirname9(path), { recursive: true });
1971
+ await writeAtomic(path, next, { mode: 384 });
1972
+ return entry === null ? "removed" : current === void 0 ? "created" : "updated";
1973
+ }
1974
+ async function readHermesConfig(path) {
1975
+ return parseConfig(await readRaw(path));
1976
+ }
1977
+ async function hasHermesMcpServer(path, name) {
1978
+ const config = await readHermesConfig(path);
1979
+ return config !== null && name in servers(config);
1980
+ }
1981
+
1982
+ // src/clients/global-mcp.ts
1983
+ function globalMcpPath(target) {
1984
+ return target === "codex" ? codexConfigPath() : hermesConfigPath();
1985
+ }
1986
+ async function setGlobalMcp(target, options) {
1987
+ const { name, root, namespace, enabled } = options;
1988
+ if (target === "hermes") {
1989
+ return setHermesMcpServer(
1990
+ hermesConfigPath(),
1991
+ name,
1992
+ enabled ? codebuddyMcpServer(root, namespace) : null
1993
+ );
1994
+ }
1995
+ if (!enabled) return (await removeCodexEntry(name)).removed ? "removed" : "unchanged";
1996
+ const result = await installCodexEntry({
1997
+ namespace,
1998
+ projectRoot: root,
1999
+ serverName: name,
2000
+ toolProfile: "core"
2001
+ });
2002
+ return result.created ? "created" : result.changed ? "updated" : "unchanged";
2003
+ }
2004
+ async function hasGlobalMcp(target, name) {
2005
+ if (target === "hermes") return hasHermesMcpServer(hermesConfigPath(), name);
2006
+ return (await listCodexEntries()).entries.some((entry) => entry.key === name);
2007
+ }
2008
+
2009
+ // src/clients/install.ts
2010
+ async function installClient(options) {
2011
+ const adapter = CLIENT_ADAPTERS[options.client];
2012
+ if (!supportedModes(adapter).includes(options.mode)) {
2013
+ throw new Error(
2014
+ `${adapter.name} does not support ${MODE_LABELS[options.mode]} mode: ${adapter.gap ?? "no lifecycle hooks"}.`
2015
+ );
2016
+ }
2017
+ return apply(options, options.mode, options.mode !== "lean");
2018
+ }
2019
+ async function ensureIndex(root) {
2020
+ const target = join12(root, ".codebuddy", "cache", "index.json");
2021
+ const existed = await access4(target).then(
2022
+ () => true,
2023
+ () => false
2024
+ );
2025
+ if (!existed) await indexRepository({ repositoryRoot: root });
2026
+ return { surface: "index", target, action: existed ? "unchanged" : "built" };
2027
+ }
2028
+ async function uninstallClient(options) {
2029
+ return apply(options, "mcp-only", false);
2030
+ }
2031
+ async function apply(options, mode, keepMcp) {
2032
+ const root = resolve9(options.projectRoot ?? process.cwd());
2033
+ const adapter = CLIENT_ADAPTERS[options.client];
2034
+ const steps = [];
2035
+ const notes = [];
2036
+ const namespace = options.namespace ?? await resolveNamespaceFrom(root);
2037
+ const name = `codebuddy-${namespace}`;
2038
+ const shared = sharedRulesReaders(options.client);
2039
+ const liveSiblings = mode === "mcp-only" || mode === "lean" ? await installedWithMcp(root, shared, namespace) : [];
2040
+ if (liveSiblings.length > 0) {
2041
+ const path = join12(root, adapter.rulesFile);
2042
+ steps.push({ surface: "rules", target: path, action: "kept" });
2043
+ notes.push(
2044
+ `Kept the CodeBuddy block in ${adapter.rulesFile}: still used by ${liveSiblings.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
2045
+ );
2046
+ } else {
2047
+ const hooked = mode === "agent" ? await hookedRulesSiblings(root, options.client) : [];
2048
+ const rules = await installWorkflowRules({
2049
+ client: options.client,
2050
+ projectRoot: root,
2051
+ // Lean sends no rules: the block would cost tokens every turn and shadow AGENTS.md.
2052
+ mode: mode === "lean" ? "mcp-only" : hooked.length > 0 ? "hooks" : mode
2053
+ });
2054
+ steps.push({ surface: "rules", target: rules.path, action: rules.action });
2055
+ if (hooked.length > 0) {
2056
+ notes.push(
2057
+ `Kept ${adapter.rulesFile} hook-driven: ${hooked.map((id) => CLIENT_ADAPTERS[id].name).join(", ")} hooks are installed; agents without hooks follow its agent-driven fallback.`
2058
+ );
2059
+ }
2060
+ }
2061
+ if (shared.length > 0) {
2062
+ notes.push(
2063
+ `${adapter.rulesFile} is also read by ${shared.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
2064
+ );
2065
+ }
2066
+ const hooks = await setHooks(
2067
+ root,
2068
+ mode === "hooks" || mode === "lean",
2069
+ options.client,
2070
+ mode === "lean" ? "lean" : "full"
2071
+ );
2072
+ if (hooks) steps.push(hooks);
2073
+ if (mode === "lean") steps.push(await ensureIndex(root));
2074
+ if ("file" in adapter.mcp) {
2075
+ const path = await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format);
2076
+ const action = keepMcp ? await upsertMcpServer(path, adapter.mcp.format, name, codebuddyMcpServer(root, namespace)) : await removeMcpServer(path, adapter.mcp.format, name);
2077
+ steps.push({ surface: "mcp", target: path, action });
2078
+ if (action === "manual") {
2079
+ notes.push(
2080
+ `${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)))}` : ""}.`
2081
+ );
2082
+ }
2083
+ } else if (options.global) {
2084
+ const target = globalMcpPath(adapter.mcp.global);
2085
+ const action = await setGlobalMcp(adapter.mcp.global, {
2086
+ name,
2087
+ root,
2088
+ namespace,
2089
+ enabled: keepMcp
2090
+ });
2091
+ steps.push({ surface: "mcp", target, action });
2092
+ if (action === "manual") {
2093
+ notes.push(
2094
+ `${target} is not a YAML map CodeBuddy can edit safely; ${keepMcp ? "add" : "remove"} the "${name}" entry under mcp_servers by hand.`
2095
+ );
2096
+ }
2097
+ } else {
2098
+ notes.push(
2099
+ `MCP config is global; ${keepMcp ? "add" : "remove"} it with --global, or: ${adapter.mcp.manual}.`
2100
+ );
2101
+ }
2102
+ if (adapter.note && mode !== "mcp-only") notes.push(`${adapter.name}: ${adapter.note}.`);
2103
+ return { client: options.client, steps, notes };
2104
+ }
2105
+ async function installedWithMcp(root, clients, namespace) {
2106
+ const live = [];
2107
+ for (const id of clients) {
2108
+ if ((await detectClient(id, root, namespace)).mcp) live.push(id);
2109
+ }
2110
+ return live;
2111
+ }
2112
+ async function setHooks(root, enabled, client = "claude", variant = "full") {
2113
+ const result = await setClientHooks(root, client, enabled, variant);
2114
+ if (!result) return null;
2115
+ const action = result.changed ? enabled ? "installed" : "removed" : "unchanged";
2116
+ return { surface: "hooks", target: result.target, action };
2117
+ }
2118
+ async function detectClient(client, projectRoot = process.cwd(), namespace) {
2119
+ const root = resolve9(projectRoot);
2120
+ const adapter = CLIENT_ADAPTERS[client];
2121
+ const rulesMode = await readManagedMode(join12(root, adapter.rulesFile));
2122
+ const hooks = await clientHooksInstalled(root, client);
2123
+ const name = `codebuddy-${namespace ?? await resolveNamespaceFrom(root)}`;
2124
+ const mcp = "file" in adapter.mcp ? await hasMcpServer(
2125
+ await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format),
2126
+ adapter.mcp.format,
2127
+ name
2128
+ ) : await hasGlobalMcp(adapter.mcp.global, name).catch(() => false);
2129
+ const warnings = [];
2130
+ if (hooks && rulesMode === "agent") {
2131
+ warnings.push(
2132
+ `hooks are installed but ${adapter.rulesFile} is agent-driven: retrieval and capture run twice. Run \`codebuddy clients install ${client} --mode hooks\`.`
2133
+ );
2134
+ }
2135
+ if (rulesMode === "hooks" && hooks === false) {
2136
+ warnings.push(
2137
+ `${adapter.rulesFile} is hook-driven but hooks are not installed: turns are not captured.`
2138
+ );
2139
+ }
2140
+ const effectiveRules = hooks === null && rulesMode === "hooks" ? "agent" : rulesMode;
2141
+ const mode = hooks ? await clientHooksVariant(root, client) === "lean" ? "lean" : "hooks" : effectiveRules ?? (mcp ? "mcp-only" : "none");
2142
+ return { client, mode, rulesMode, hooks, mcp, warnings };
2143
+ }
2144
+
2145
+ // src/clients/verify.ts
2146
+ import { execFile } from "child_process";
2147
+ import { readFile as readFile11, realpath } from "fs/promises";
2148
+ import { join as join13, resolve as resolve10 } from "path";
2149
+ import { pathToFileURL } from "url";
2150
+ var runHost = (bin, args, cwd) => new Promise((done) => {
2151
+ execFile(
2152
+ bin,
2153
+ args,
2154
+ { cwd, timeout: 15e3, maxBuffer: 8 * 1024 * 1024 },
2155
+ (error, stdout) => done({ ok: !error, stdout: String(stdout ?? "") })
2156
+ );
2157
+ });
2158
+ var pass = (name, detail) => ({ name, status: "pass", detail });
2159
+ var fail = (name, detail) => ({ name, status: "fail", detail });
2160
+ var skip = (name, detail) => ({ name, status: "skip", detail });
2161
+ function json(text3) {
2162
+ try {
2163
+ return JSON.parse(text3);
2164
+ } catch {
2165
+ return null;
2166
+ }
2167
+ }
2168
+ var CONFORMANCE = {
2169
+ claude: async ({ root, name, run }) => {
2170
+ const hooks = await clientHooksInstalled(root, "claude") ? pass("hooks", ".claude/settings.json has the CodeBuddy hooks") : skip("hooks", "not installed in this repo");
2171
+ const mcp = await run("claude", ["mcp", "get", name], root);
2172
+ return [
2173
+ hooks,
2174
+ mcp.ok ? pass("mcp", `claude mcp get ${name}: ${mcp.stdout.match(/Scope: (.+)/)?.[1] ?? "found"}`) : skip("mcp", `claude mcp get ${name}: not configured`)
2175
+ ];
2176
+ },
2177
+ opencode: async ({ root, name, run }) => {
2178
+ const result = await run("opencode", ["debug", "config"], root);
2179
+ const config = json(result.stdout);
2180
+ if (!config) return [fail("config", "opencode debug config returned no JSON")];
2181
+ const installed = await clientHooksInstalled(root, "opencode");
2182
+ const href = pathToFileURL(
2183
+ join13(await realpath(root).catch(() => root), ".opencode", "plugins", "codebuddy.js")
2184
+ ).href;
2185
+ const loaded = config.plugin?.includes(href) === true;
2186
+ return [
2187
+ 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"),
2188
+ config.mcp?.[name] ? pass("mcp", `opencode resolves the ${name} MCP entry`) : skip("mcp", `no ${name} entry in the resolved config`)
2189
+ ];
2190
+ },
2191
+ codex: async ({ root, name, run }) => {
2192
+ const result = await run("codex", ["mcp", "list", "--json"], root);
2193
+ const servers2 = json(result.stdout);
2194
+ if (!Array.isArray(servers2)) return [fail("mcp", "codex mcp list --json returned no JSON")];
2195
+ return [
2196
+ servers2.some((server) => server.name === name) ? pass("mcp", `codex mcp list has ${name}`) : skip("mcp", `no ${name} entry (clients install codex --global adds it)`)
2197
+ ];
2198
+ },
2199
+ hermes: async ({ root, name, run, env }) => {
2200
+ const checks = [];
2201
+ const listed = await run("hermes", ["mcp", "list"], root);
2202
+ checks.push(
2203
+ listed.ok && listed.stdout.includes(name) ? pass("mcp", `hermes mcp list has ${name}`) : skip("mcp", `no ${name} entry (clients install hermes --global adds it)`)
2204
+ );
2205
+ if (!await hermesPluginInstalled(root)) {
2206
+ checks.push(skip("plugin", "not installed in this repo"));
2207
+ return checks;
2208
+ }
2209
+ 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(
2210
+ await readFile11(join13(hermesConfigPath(), "..", ".env"), "utf8").catch(() => "")
2211
+ );
2212
+ const plugins = (await readHermesConfig(hermesConfigPath()))?.plugins;
2213
+ const enabled = Array.isArray(plugins?.enabled) && plugins.enabled.includes("codebuddy");
2214
+ checks.push(
2215
+ projectPlugins ? pass("project-plugins", "HERMES_ENABLE_PROJECT_PLUGINS is set") : fail("project-plugins", "set HERMES_ENABLE_PROJECT_PLUGINS=1 (shell or ~/.hermes/.env)"),
2216
+ enabled ? pass("plugin-enabled", "codebuddy is in plugins.enabled") : fail("plugin-enabled", "add codebuddy to plugins.enabled in ~/.hermes/config.yaml")
2217
+ );
2218
+ return checks;
2219
+ }
2220
+ };
2221
+ async function verifyClient(client, options = {}) {
2222
+ const root = resolve10(options.root ?? process.cwd());
2223
+ const run = options.run ?? runHost;
2224
+ const adapter = CLIENT_ADAPTERS[client];
2225
+ const version = await run(adapter.host.bin, ["--version"], root);
2226
+ const installed = version.ok ? version.stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null : null;
2227
+ const events = LIFECYCLE_EVENTS.map((event) => ({
2228
+ event,
2229
+ wired: adapter.hookEvents.includes(event)
2230
+ }));
2231
+ if (!installed) {
2232
+ return {
2233
+ client,
2234
+ installed: null,
2235
+ verified: adapter.host.verified,
2236
+ drift: "not-installed",
2237
+ events,
2238
+ checks: [skip("host", `${adapter.host.bin} not found or printed no version`)]
2239
+ };
2240
+ }
2241
+ const name = `codebuddy-${options.namespace ?? await resolveNamespaceFrom(root)}`;
2242
+ const conformance = CONFORMANCE[client];
2243
+ const checks = conformance ? await conformance({ root, name, run, env: options.env ?? process.env }).catch(
2244
+ (error) => [fail("conformance", error.message)]
2245
+ ) : [];
2246
+ return {
2247
+ client,
2248
+ installed,
2249
+ verified: adapter.host.verified,
2250
+ drift: installed === adapter.host.verified ? "match" : "drift",
2251
+ events,
2252
+ checks: [pass("host", `${adapter.host.bin} ${installed}`), ...checks]
2253
+ };
2254
+ }
2255
+
2256
+ // src/cli/commands/clients.ts
2257
+ function registerClientsCommands(program, runSafely2) {
2258
+ const clients = program.command("clients").description("Client adapters: capabilities and explicit automation modes per coding agent.");
2259
+ clients.command("list").option("--namespace <name>", "Namespace whose MCP entries to look for.").description("Show each client's surfaces, lifecycle coverage, and mode in this repo.").action(async (opts) => {
2260
+ await runSafely2(async () => {
2261
+ for (const client of CLIENT_IDS) {
2262
+ const adapter = CLIENT_ADAPTERS[client];
2263
+ const status = await detectClient(client, process.cwd(), opts.namespace);
2264
+ const mcp = "file" in adapter.mcp ? adapter.mcp.file : "global (--global)";
2265
+ const lifecycle = LIFECYCLE_EVENTS.map(
2266
+ (event) => adapter.hookEvents.includes(event) ? pc2.green(event) : pc2.dim(`${event}\u2717`)
2267
+ ).join(" ");
2268
+ const ruleCost = supportedModes(adapter).flatMap(
2269
+ (mode) => mode === "mcp-only" ? [] : [`${mode} ~${estimateTokens(renderManagedBlock(client, mode))}`]
2270
+ ).join(", ");
2271
+ console.log(
2272
+ `${pc2.bold(adapter.name)} (${client}) ${pc2.dim(`verified ${adapter.host.bin} ${adapter.host.verified}`)}`
2273
+ );
2274
+ console.log(
2275
+ ` rules ${adapter.rulesFile} \xB7 mcp ${mcp} \xB7 modes ${supportedModes(adapter).join("|")}`
2276
+ );
2277
+ console.log(` lifecycle: ${lifecycle}`);
2278
+ console.log(` rules block tokens: ${ruleCost}`);
2279
+ console.log(
2280
+ ` this repo: ${status.mode === "none" ? pc2.dim("not installed") : pc2.cyan(status.mode)}`
2281
+ );
2282
+ if (adapter.gap) console.log(pc2.dim(` missing: ${adapter.gap}`));
2283
+ if (adapter.note) console.log(pc2.dim(` note: ${adapter.note}`));
2284
+ for (const warning of status.warnings) console.log(pc2.yellow(` ! ${warning}`));
2285
+ }
2286
+ console.log(pc2.dim("Per-turn token cost per client: codebuddy clients cost"));
2287
+ });
2288
+ });
2289
+ clients.command("install").argument("<client>", `One of: ${CLIENT_IDS.join(", ")}.`).requiredOption("--mode <mode>", `One of: ${AUTOMATION_MODES.join(", ")}.`).option("--namespace <name>", "Namespace for the MCP entry (defaults to config/folder).").option("--global", "Also add the MCP entry to the global config (Codex, Hermes).").description("Apply one automation mode: rules block, hooks, and MCP config.").action(
2290
+ async (client, opts) => {
2291
+ await runSafely2(async () => {
2292
+ const mode = parseMode(opts.mode);
2293
+ const report = await installClient({
2294
+ client: parseClient(client),
2295
+ mode,
2296
+ ...opts.namespace ? { namespace: opts.namespace } : {},
2297
+ ...opts.global ? { global: true } : {}
2298
+ });
2299
+ printReport2(report, `${MODE_LABELS[mode]} mode`);
2300
+ });
2301
+ }
2302
+ );
2303
+ clients.command("uninstall").argument("<client>", `One of: ${CLIENT_IDS.join(", ")}.`).option("--namespace <name>", "Namespace of the MCP entry to remove.").option("--global", "Also remove the MCP entry from the global config (Codex, Hermes).").description("Remove CodeBuddy's rules block, hooks, and MCP entry (user config untouched).").action(async (client, opts) => {
2304
+ await runSafely2(async () => {
2305
+ const report = await uninstallClient({
2306
+ client: parseClient(client),
2307
+ ...opts.namespace ? { namespace: opts.namespace } : {},
2308
+ ...opts.global ? { global: true } : {}
2309
+ });
2310
+ printReport2(report, "uninstalled");
2311
+ });
2312
+ });
2313
+ clients.command("verify").argument("[client]", `One of: ${CLIENT_IDS.join(", ")} (default: all).`).option("--namespace <name>", "Namespace whose MCP entries to look for.").description(
2314
+ "Compare installed hosts with the verified versions; run no-model conformance checks."
2315
+ ).action(async (client, opts) => {
2316
+ await runSafely2(async () => {
2317
+ const targets = client ? [parseClient(client)] : CLIENT_IDS;
2318
+ for (const id of targets) {
2319
+ const report = await verifyClient(
2320
+ id,
2321
+ opts.namespace ? { namespace: opts.namespace } : {}
2322
+ );
2323
+ const drift = report.drift === "match" ? pc2.green("match") : report.drift === "drift" ? pc2.yellow(`drift (installed ${report.installed})`) : pc2.dim("not installed");
2324
+ console.log(
2325
+ `${pc2.bold(CLIENT_ADAPTERS[id].name)}: verified ${report.verified} \xB7 ${drift}`
2326
+ );
2327
+ console.log(
2328
+ ` events: ${report.events.map(({ event, wired }) => wired ? pc2.green(event) : pc2.dim(`${event}\u2717`)).join(" ")}`
2329
+ );
2330
+ for (const check of report.checks) {
2331
+ const label = check.status === "pass" ? pc2.green("pass") : check.status === "fail" ? pc2.red("fail") : pc2.dim("skip");
2332
+ console.log(` ${label} ${check.name}: ${check.detail}`);
2333
+ }
2334
+ }
2335
+ });
2336
+ });
2337
+ clients.command("cost").option("--namespace <name>", "Namespace to measure (defaults to config/folder).").option("--json", "Print the measurements as JSON.").description("Measured per-turn CodeBuddy token cost per client and mode, for this repo.").action(async (opts) => {
2338
+ await runSafely2(async () => {
2339
+ const costs = await measureClientCosts(process.cwd(), opts.namespace);
2340
+ if (opts.json) {
2341
+ console.log(JSON.stringify(costs, null, 2));
2342
+ return;
2343
+ }
2344
+ console.log(
2345
+ "client mode rules mcp-schema block(1st) block(later) turn(1st) turn(later)"
2346
+ );
2347
+ for (const cost of costs) {
2348
+ const fixed = cost.rules + cost.mcpSchema;
2349
+ console.log(
2350
+ [
2351
+ cost.client.padEnd(10),
2352
+ cost.mode.padEnd(7),
2353
+ String(cost.rules).padStart(5),
2354
+ String(cost.mcpSchema).padStart(11),
2355
+ String(cost.firstBlock).padStart(11),
2356
+ String(cost.laterBlock).padStart(13),
2357
+ String(fixed + cost.firstBlock).padStart(10),
2358
+ String(fixed + cost.laterBlock).padStart(12)
2359
+ ].join(" ")
2360
+ );
2361
+ }
2362
+ console.log(
2363
+ pc2.dim(
2364
+ "Measured with the cl100k estimator on the generated rules block, the MCP tools/list payload and this repo's hook block. Host framing (host-dependent, not measured):"
2365
+ )
2366
+ );
2367
+ for (const id of CLIENT_IDS) console.log(pc2.dim(` ${id}: ${CLIENT_ADAPTERS[id].framing}`));
2368
+ });
2369
+ });
2370
+ }
2371
+ function printReport2(report, title) {
2372
+ console.log(`${pc2.bold(CLIENT_ADAPTERS[report.client].name)}: ${title}`);
2373
+ for (const step of report.steps) {
2374
+ const where = relative(process.cwd(), step.target) || step.target;
2375
+ const label = step.action === "unchanged" ? pc2.dim(step.action) : pc2.green(step.action);
2376
+ console.log(` ${step.surface.padEnd(5)} ${label} ${where}`);
2377
+ }
2378
+ for (const note3 of report.notes) console.log(pc2.dim(` ${note3}`));
2379
+ }
2380
+
2381
+ // src/cli/commands/context.ts
2382
+ import pc3 from "picocolors";
2383
+ async function resolveTokenBudget(cwd = process.cwd()) {
2384
+ const file = await readConfigFile(cwd);
2385
+ return file.tokenBudget;
2386
+ }
2387
+ function parsePaths2(value) {
2388
+ return value.split(",").map((path) => path.trim()).filter(Boolean);
2389
+ }
2390
+ function registerContextCommands(program, runSafely2) {
2391
+ const context = program.command("context").description("Preview the compact project context CodeBuddy would send to an agent.");
2392
+ context.command("bootstrap").option("--json", "Print machine-readable JSON.").description("Show start-of-turn project awareness (plan, policy, incidents, architecture).").action(async (opts) => {
2393
+ await runSafely2(async () => {
2394
+ const [namespace, tokenBudget] = await Promise.all([
2395
+ resolveNamespaceFrom(),
2396
+ resolveTokenBudget()
2397
+ ]);
2398
+ const result = await buildBootstrapContext({
2399
+ namespace,
2400
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
2401
+ });
2402
+ if (opts.json) {
2403
+ console.log(JSON.stringify(result, null, 2));
2404
+ return;
2405
+ }
2406
+ printBootstrap(result);
2407
+ });
2408
+ });
2409
+ context.command("preview").argument("[task...]", "The task you are about to work on.").option("--paths <paths>", "Comma-separated repo-relative paths to target.").option("--plan <id>", "Resolve target files from a CodeBuddy plan.").option("--git", "Resolve target files from current git changes.").option("--json", "Print machine-readable JSON.").description("Show the context CodeBuddy would assemble before editing the given files.").action(
2410
+ async (taskParts, opts) => {
2411
+ await runSafely2(async () => {
2412
+ const result = await runBeforeEdit(taskParts, opts);
2413
+ if (opts.json) {
2414
+ console.log(JSON.stringify(result, null, 2));
2415
+ return;
2416
+ }
2417
+ printBeforeEdit(result);
2418
+ });
2419
+ }
2420
+ );
2421
+ context.command("explain").argument("[task...]", "The task you are about to work on.").option("--paths <paths>", "Comma-separated repo-relative paths to target.").option("--plan <id>", "Resolve target files from a CodeBuddy plan.").option("--git", "Resolve target files from current git changes.").description("Explain why each piece of before-edit context was included.").action(async (taskParts, opts) => {
2422
+ await runSafely2(async () => {
2423
+ const result = await runBeforeEdit(taskParts, opts);
2424
+ console.log(pc3.bold(`context for ${result.targetPaths.length} target path(s):`));
2425
+ for (const line of result.explain) console.log(` ${pc3.dim("\u2022")} ${line}`);
2426
+ });
2427
+ });
2428
+ }
2429
+ async function runBeforeEdit(taskParts, opts) {
2430
+ const [namespace, tokenBudget] = await Promise.all([
2431
+ resolveNamespaceFrom(),
2432
+ resolveTokenBudget()
2433
+ ]);
2434
+ const task = taskParts.join(" ").trim();
2435
+ return buildBeforeEditContext({
2436
+ namespace,
2437
+ ...task ? { task } : {},
2438
+ ...opts.paths ? { paths: parsePaths2(opts.paths) } : {},
2439
+ ...opts.plan ? { planId: opts.plan } : {},
2440
+ ...opts.git !== void 0 ? { useGit: opts.git } : {},
2441
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
2442
+ });
2443
+ }
2444
+ function printBootstrap(ctx) {
2445
+ console.log(`${pc3.bold("project")} ${ctx.project.namespace} ${pc3.dim(ctx.project.root)}`);
2446
+ console.log(
2447
+ `${pc3.bold("plan")} ${ctx.plan ? `${ctx.plan.title} (${ctx.plan.status})` : pc3.dim("none")} ${pc3.dim(`policy=${ctx.policy}`)}`
2448
+ );
2449
+ if (ctx.policyRules.length > 0) {
2450
+ console.log(pc3.bold("policy rules:"));
2451
+ for (const rule of ctx.policyRules) {
2452
+ console.log(` ${rule.id} (${rule.weight}) ${pc3.dim(rule.pattern)}`);
2453
+ }
2454
+ }
2455
+ if (ctx.incidents.length > 0) {
2456
+ console.log(pc3.bold("incident hotspots:"));
2457
+ for (const incident of ctx.incidents) {
2458
+ console.log(
2459
+ ` ${severityColor(incident.severity)} ${incident.subject} ${pc3.dim(incident.summary)}`
2460
+ );
2461
+ }
2462
+ }
2463
+ printMemory(ctx);
2464
+ console.log(
2465
+ `${pc3.bold("architecture")} ${ctx.architecture.moduleCount} modules, ${ctx.architecture.edgeCount} edges`
2466
+ );
2467
+ for (const hotspot of ctx.architecture.hotspots) {
2468
+ console.log(` ${hotspot.path} ${pc3.dim(`in=${hotspot.inbound} out=${hotspot.outbound}`)}`);
2469
+ }
2470
+ printTokens(ctx);
2471
+ }
2472
+ function printBeforeEdit(ctx) {
2473
+ console.log(
2474
+ `${pc3.bold("targets")} ${ctx.targetPaths.length} path(s) ${pc3.dim(`(source: ${ctx.pathSource})`)}`
2475
+ );
2476
+ for (const path of ctx.targetPaths) console.log(` ${path}`);
2477
+ if (ctx.plan) console.log(`${pc3.bold("plan")} ${ctx.plan.title} (${ctx.plan.status})`);
2478
+ if (ctx.policyRules.length > 0) {
2479
+ console.log(pc3.bold("matching policy rules:"));
2480
+ for (const rule of ctx.policyRules) console.log(` ${rule.id} ${pc3.dim(rule.pattern)}`);
2481
+ }
2482
+ if (ctx.incidents.length > 0) {
2483
+ console.log(pc3.bold("incidents on target paths:"));
2484
+ for (const incident of ctx.incidents) {
2485
+ console.log(` ${severityColor(incident.severity)} ${incident.subject}`);
2486
+ }
2487
+ }
2488
+ printMemory(ctx);
2489
+ if (ctx.risks.items.length > 0) {
2490
+ console.log(pc3.bold("risk:"));
2491
+ for (const item of ctx.risks.items) {
2492
+ console.log(` ${item.path} ${scoreColor(item.score)} ${pc3.dim(item.reason)}`);
2493
+ }
2494
+ }
2495
+ if (ctx.neighbours.length > 0) {
2496
+ console.log(pc3.bold("import neighbours:"));
2497
+ for (const neighbour of ctx.neighbours) {
2498
+ console.log(
2499
+ ` ${neighbour.path} ${pc3.dim(`\u2192${neighbour.dependsOn.length} \u2190${neighbour.dependedOnBy.length}`)}`
2500
+ );
2501
+ }
2502
+ }
2503
+ if (ctx.symbols.length > 0) {
2504
+ console.log(pc3.bold("symbols (signatures, in place of full source):"));
2505
+ for (const file of ctx.symbols) {
2506
+ console.log(` ${file.path} ${pc3.dim(`${file.signatures.length} symbol(s)`)}`);
2507
+ }
2508
+ }
2509
+ if (ctx.diffs.length > 0) {
2510
+ console.log(pc3.bold("changed hunks:"));
2511
+ for (const diff of ctx.diffs) {
2512
+ console.log(` ${diff.path}${diff.truncated ? pc3.dim(" (truncated)") : ""}`);
2513
+ for (const line of diff.patch.split("\n").slice(-8)) console.log(pc3.dim(` ${line}`));
2514
+ }
2515
+ }
2516
+ printTokens(ctx);
2517
+ }
2518
+ function printMemory(ctx) {
2519
+ if (ctx.facts.length > 0) {
2520
+ console.log(pc3.bold("facts:"));
2521
+ for (const fact of ctx.facts) {
2522
+ console.log(` ${fact.summary} ${pc3.dim(`(${fact.confidence.toFixed(2)}, ${fact.reason})`)}`);
2523
+ }
2524
+ }
2525
+ if (ctx.handoffs.length > 0) {
2526
+ console.log(pc3.bold("prior work:"));
2527
+ for (const handoff of ctx.handoffs) {
2528
+ const drift = handoff.changedSince.length > 0 ? `, ${handoff.changedSince.length} file(s) changed since` : "";
2529
+ console.log(
2530
+ ` ${handoff.summary.replace(/\s+/g, " ").slice(0, 160)} ${pc3.dim(`(${handoff.ageDays}d ago${drift})`)}`
2531
+ );
2532
+ }
2533
+ }
2534
+ }
2535
+ function printTokens(ctx) {
2536
+ const { returnedEstimate, budget } = ctx.tokens;
2537
+ console.log(pc3.dim(`
2538
+ ~${returnedEstimate} tokens returned (budget ${budget})`));
2539
+ }
2540
+ function severityColor(severity) {
2541
+ if (severity === "critical") return pc3.red(severity);
2542
+ if (severity === "high") return pc3.red(severity);
2543
+ if (severity === "medium") return pc3.yellow(severity);
2544
+ return pc3.dim(severity);
2545
+ }
2546
+ function scoreColor(score) {
2547
+ if (score >= 75) return pc3.red(String(score));
2548
+ if (score >= 50) return pc3.yellow(String(score));
2549
+ return pc3.green(String(score));
2550
+ }
2551
+
2552
+ // src/cli/commands/eval.ts
2553
+ import pc4 from "picocolors";
2554
+
2555
+ // src/eval/runner.ts
2556
+ import { readFile as readFile12 } from "fs/promises";
2557
+ import { dirname as dirname10, isAbsolute, join as join14, resolve as resolve11 } from "path";
2558
+ async function runEvaluation(input = {}) {
2559
+ const datasetPath = resolve11(
2560
+ input.datasetPath ?? join14(process.cwd(), "evals", "change-readiness.json")
2561
+ );
2562
+ const dataset = JSON.parse(await readFile12(datasetPath, "utf8"));
2563
+ if (dataset.version !== 1 || !Array.isArray(dataset.cases)) {
2564
+ throw new Error(`Unsupported evaluation dataset: ${datasetPath}`);
2565
+ }
2566
+ const datasetDirectory = dirname10(datasetPath);
2567
+ const results = await Promise.all(
2568
+ dataset.cases.map(async (evaluationCase) => {
2569
+ const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture : resolve11(input.repositoryRoot ?? datasetDirectory, evaluationCase.fixture);
2570
+ const report = await buildChangeReport({
2571
+ repositoryRoot: fixtureRoot,
2572
+ namespace: `eval:${evaluationCase.id}`,
2573
+ paths: evaluationCase.paths,
2574
+ useGit: false
2575
+ });
2576
+ const failures = [];
2577
+ if (report.status !== evaluationCase.expectedStatus) {
2578
+ failures.push(`status expected ${evaluationCase.expectedStatus}, got ${report.status}`);
2579
+ }
2580
+ if (evaluationCase.minimumRisk !== void 0 && report.risk.highestScore < evaluationCase.minimumRisk) {
2581
+ failures.push(
2582
+ `risk expected >= ${evaluationCase.minimumRisk}, got ${report.risk.highestScore}`
2583
+ );
2584
+ }
2585
+ const categories = [
2586
+ ...new Set(report.suggestions.map((suggestion) => suggestion.category))
2587
+ ];
2588
+ for (const category of evaluationCase.requiredCategories ?? []) {
2589
+ if (!categories.includes(category)) failures.push(`missing category ${category}`);
2590
+ }
2591
+ return {
2592
+ id: evaluationCase.id,
2593
+ description: evaluationCase.description,
2594
+ passed: failures.length === 0,
2595
+ expectedStatus: evaluationCase.expectedStatus,
2596
+ actualStatus: report.status,
2597
+ highestRisk: report.risk.highestScore,
2598
+ categories,
2599
+ failures
2600
+ };
2601
+ })
2602
+ );
2603
+ const passed = results.filter((result) => result.passed).length;
2604
+ return {
2605
+ datasetPath,
2606
+ passed: passed === results.length,
2607
+ cases: results,
2608
+ summary: { total: results.length, passed, failed: results.length - passed }
2609
+ };
2610
+ }
2611
+
2612
+ // src/cli/commands/eval.ts
2613
+ function registerEvalCommand(program, runSafely2) {
2614
+ program.command("eval").argument("[dataset]", "Evaluation dataset JSON path.").option("--json", "Print machine-readable JSON.").description("Run the committed change-readiness evaluation dataset.").action(async (dataset, opts) => {
2615
+ await runSafely2(async () => {
2616
+ const result = await runEvaluation(dataset ? { datasetPath: dataset } : {});
2617
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
2618
+ else printEvaluation(result);
2619
+ if (!result.passed) process.exitCode = 1;
2620
+ });
2621
+ });
2622
+ }
2623
+ function printEvaluation(result) {
2624
+ console.log(
2625
+ `${result.passed ? pc4.green("passed") : pc4.red("failed")} ${result.summary.passed}/${result.summary.total} evaluation cases`
2626
+ );
2627
+ for (const item of result.cases) {
2628
+ console.log(` ${item.passed ? pc4.green("\u2713") : pc4.red("\xD7")} ${item.id}`);
2629
+ if (!item.passed) for (const failure of item.failures) console.log(pc4.dim(` ${failure}`));
2630
+ }
2631
+ console.log(pc4.dim(`dataset: ${result.datasetPath}`));
2632
+ }
2633
+
2634
+ // src/cli/commands/github.ts
2635
+ import pc5 from "picocolors";
2636
+ function registerGitHubCommands(program, runSafely2) {
2637
+ const github = program.command("github").description("Install GitHub workflow integrations.");
2638
+ github.command("install").option("--force", "Replace an existing CodeBuddy workflow.").description("Install the PR change-safety report workflow.").action(async (opts) => {
2639
+ await runSafely2(async () => {
2640
+ const result = await installGitHubWorkflow(process.cwd(), {
2641
+ ...opts.force !== void 0 ? { force: opts.force } : {}
2642
+ });
2643
+ console.log(`${result.created ? "installed" : "already present"} ${pc5.dim(result.path)}`);
2644
+ if (result.created) console.log("Commit this workflow to enable CodeBuddy PR reports.");
2645
+ });
2646
+ });
2647
+ }
2648
+
2649
+ // src/cli/commands/hooks.ts
2650
+ import { relative as relative2 } from "path";
2651
+ import pc6 from "picocolors";
2652
+ function registerHooksCommands(program, runSafely2) {
2653
+ const hooks = program.command("hooks").description("Enforce automatic recall/capture via client lifecycle hooks.");
2654
+ hooks.command("install").option(
2655
+ "--client <client>",
2656
+ "Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
2657
+ "claude"
2658
+ ).description(
2659
+ "Add CodeBuddy's recall/capture hooks (.claude/settings.json, .opencode/plugins/codebuddy.js or .hermes/plugins/codebuddy/)."
2660
+ ).action(async (opts) => {
2661
+ await runSafely2(async () => {
2662
+ const client = assertHookClient(opts.client);
2663
+ const step = await setHooks(process.cwd(), true, client);
2664
+ if (!step) return;
2665
+ const where = relative2(process.cwd(), step.target) || step.target;
2666
+ const changed = step.action !== "unchanged";
2667
+ console.log(
2668
+ `${changed ? pc6.green("installed") : pc6.dim("unchanged")} CodeBuddy hooks in ${where}`
2669
+ );
2670
+ await reconcileRules(client, "hooks");
2671
+ console.log(
2672
+ pc6.dim(
2673
+ ` prompt\u2192recall \xB7 edit\u2192stage \xB7 turn end\u2192capture \xB7 compaction\u2192reset. Restart ${CLIENT_ADAPTERS[client].name} to activate.`
2674
+ )
2675
+ );
2676
+ });
2677
+ });
2678
+ hooks.command("uninstall").option(
2679
+ "--client <client>",
2680
+ "Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
2681
+ "claude"
2682
+ ).description("Remove CodeBuddy's hooks for a client (user hooks untouched).").action(async (opts) => {
2683
+ await runSafely2(async () => {
2684
+ const client = assertHookClient(opts.client);
2685
+ const changed = (await setHooks(process.cwd(), false, client))?.action === "removed";
2686
+ console.log(
2687
+ changed ? pc6.yellow("removed CodeBuddy hooks") : pc6.dim("no CodeBuddy hooks found")
2688
+ );
2689
+ await reconcileRules(client, "agent");
2690
+ });
2691
+ });
2692
+ hooks.command("status").option(
2693
+ "--client <client>",
2694
+ "Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
2695
+ "claude"
2696
+ ).description("Report whether CodeBuddy hooks are installed.").action(async (opts) => {
2697
+ await runSafely2(async () => {
2698
+ const installed = await clientHooksInstalled(process.cwd(), parseClient(opts.client));
2699
+ console.log(
2700
+ installed ? pc6.green("CodeBuddy hooks: installed") : pc6.dim("CodeBuddy hooks: not installed")
2701
+ );
2702
+ });
2703
+ });
2704
+ hooks.command("context").description("[hook] Emit compact project context for injection at prompt submit.").action(async () => {
2705
+ await failSoft(async () => {
2706
+ const input = await readHookInput();
2707
+ await runContextHook(input, writeStdout);
2708
+ });
2709
+ });
2710
+ hooks.command("lean").description("[hook] Emit ranked file pointers (lean mode).").action(async () => {
2711
+ await runLeanHookCommand();
2712
+ });
2713
+ hooks.command("stage").description("[hook] Record the files an edit tool just changed.").action(async () => {
2714
+ await failSoft(async () => {
2715
+ const input = await readHookInput();
2716
+ await runStageHook(input);
2717
+ });
2718
+ });
2719
+ hooks.command("capture").description("[hook] Capture durable memory from the turn's change set.").action(async () => {
2720
+ await failSoft(async () => {
2721
+ const input = await readHookInput();
2722
+ const { changedFiles, result } = await runCaptureHook(input);
2723
+ if (result) {
2724
+ process.stderr.write(
2725
+ `codebuddy: captured from ${changedFiles.length} changed file(s) \u2014 ${result.saved.length} saved, ${result.queuedForReview.length} queued
2726
+ `
2727
+ );
2728
+ }
2729
+ });
2730
+ });
2731
+ hooks.command("pretool").description(
2732
+ "[hook] Outline large unranged Reads and cap content Greps (never grants permission)."
2733
+ ).action(async () => {
2734
+ await runPreToolHookCommand();
2735
+ });
2736
+ hooks.command("reset").description("[hook] Forget what context was sent this session (after compaction or /clear).").action(async () => {
2737
+ await failSoft(async () => {
2738
+ await runResetHook(await readHookInput());
2739
+ });
2740
+ });
2741
+ }
2742
+ function assertHookClient(value) {
2743
+ const client = parseClient(value);
2744
+ const adapter = CLIENT_ADAPTERS[client];
2745
+ if (!supportedModes(adapter).includes("hooks")) {
2746
+ throw new Error(
2747
+ `${adapter.name} has no CodeBuddy hook adapter: ${adapter.gap ?? "unsupported"}.`
2748
+ );
2749
+ }
2750
+ return client;
2751
+ }
2752
+ async function reconcileRules(client, mode) {
2753
+ const result = await installWorkflowRules({ client, mode, onlyIfManaged: true });
2754
+ if (result.action !== "unchanged") {
2755
+ const where = relative2(process.cwd(), result.path) || result.path;
2756
+ console.log(
2757
+ `${pc6.cyan("updated")} ${where} workflow to ${mode === "hooks" ? "hook" : "agent"}-driven mode`
2758
+ );
2759
+ }
2760
+ }
2761
+
2762
+ // src/cli/commands/indexing.ts
2763
+ import pc8 from "picocolors";
2764
+
2765
+ // src/cli/commands/llm.ts
2766
+ import pc7 from "picocolors";
2767
+ import { z } from "zod";
2768
+ var KEY_ENV = { groq: "GROQ_API_KEY", openrouter: "OPENROUTER_API_KEY" };
2769
+ var WATCH_BATCH = 8;
2770
+ async function llmStatusReport(root, env = process.env) {
2771
+ const block = (await readConfigFile(root)).llm;
2772
+ const provider = block?.provider ?? "groq";
2773
+ const config = await resolveLlmConfig(root, env);
2774
+ const model = config.enabled ? config.model : block?.model ?? null;
2775
+ const key = `${provider}:${model}`;
2776
+ const { manifest } = await loadFreshIndex(root);
2777
+ return {
2778
+ enabled: config.enabled,
2779
+ reason: config.enabled ? "on" : config.reason,
2780
+ provider,
2781
+ model,
2782
+ keyEnv: env.CODEBUDDY_LLM_API_KEY ? "CODEBUDDY_LLM_API_KEY" : KEY_ENV[provider],
2783
+ keyPresent: Boolean(env.CODEBUDDY_LLM_API_KEY || env[KEY_ENV[provider]]),
2784
+ features: config.enabled ? config.features : null,
2785
+ usageToday: (await usageToday(root))[key] ?? 0,
2786
+ dailyTokenBudget: config.enabled ? config.dailyTokenBudget : block?.dailyTokenBudget ?? 4e5,
2787
+ cooldownMs: await coolingDown(root, key),
2788
+ summaries: {
2789
+ covered: (await loadLlmSummaries(root, manifest)).size,
2790
+ indexed: Object.keys(manifest.entries).length
2791
+ }
2792
+ };
2793
+ }
2794
+ function formatLlmStatus(report) {
2795
+ const features = report.features ? Object.entries(report.features).map(([name, on]) => `${name} ${on ? "on" : "off"}`).join(", ") : "-";
2796
+ return [
2797
+ `status: ${report.enabled ? "on" : `off (${report.reason})`}`,
2798
+ `provider: ${report.provider}${report.model ? ` ${report.model}` : ""}`,
2799
+ `key: ${report.keyPresent ? "present" : "missing"} (${report.keyEnv})`,
2800
+ `features: ${features}`,
2801
+ `usage today: ${report.usageToday} / ${report.dailyTokenBudget} tokens`,
2802
+ ...report.cooldownMs > 0 ? [`cooldown: rate limited, ${Math.ceil(report.cooldownMs / 1e3)} s left`] : [],
2803
+ `summaries: ${report.summaries.covered} / ${report.summaries.indexed} files`
2804
+ ].join("\n");
2805
+ }
2806
+ async function summarizeAfterIndex(root, env = process.env) {
2807
+ const config = await resolveLlmConfig(root, env);
2808
+ if (!config.enabled || !config.features.summaries) return null;
2809
+ return summarizeRepository(root, { config, limit: WATCH_BATCH, pace: false });
2810
+ }
2811
+ function registerLlmCommands(program, runSafely2) {
2812
+ const llm = program.command("llm").description(
2813
+ "Optional bring-your-own-key small model (Groq/OpenRouter) for summaries, rerank and memory."
2814
+ );
2815
+ llm.command("status", { isDefault: true }).option("--json", "Print machine-readable JSON.").description(
2816
+ "Show provider, key presence (never the key), usage, cooldown and summary coverage."
2817
+ ).action(async (opts) => {
2818
+ await runSafely2(async () => {
2819
+ const report = await llmStatusReport(process.cwd());
2820
+ console.log(opts.json ? JSON.stringify(report, null, 2) : formatLlmStatus(report));
2821
+ if (!report.keyPresent) console.log(pc7.dim(`next: export ${report.keyEnv}=<your key>`));
2822
+ });
2823
+ });
2824
+ llm.command("enable").option("--provider <provider>", "groq or openrouter", "groq").option("--model <model>", "Model id (default: llama-3.1-8b-instant on groq).").description("Turn the layer on in .codebuddy/config.json (the key stays in your environment).").action(async (opts) => {
2825
+ await runSafely2(async () => {
2826
+ if (opts.provider !== "groq" && opts.provider !== "openrouter") {
2827
+ throw new Error("--provider must be groq or openrouter");
2828
+ }
2829
+ await setLlmEnabled(true, {
2830
+ provider: opts.provider,
2831
+ ...opts.model ? { model: opts.model } : {}
2832
+ });
2833
+ console.log(formatLlmStatus(await llmStatusReport(process.cwd())));
2834
+ });
2835
+ });
2836
+ llm.command("disable").description("Turn the layer off; CodeBuddy falls back to deterministic retrieval and capture.").action(async () => {
2837
+ await runSafely2(async () => {
2838
+ await setLlmEnabled(false);
2839
+ console.log("llm layer disabled");
2840
+ });
2841
+ });
2842
+ llm.command("test").description("Send one tiny request and print the model and latency.").action(async () => {
2843
+ await runSafely2(async () => {
2844
+ const config = await resolveLlmConfig(process.cwd());
2845
+ if (!config.enabled) throw new Error(`llm layer is off: ${config.reason}`);
2846
+ const result = await chatJson(config, {
2847
+ system: 'Answer with JSON only: {"ok":true}.',
2848
+ user: "ping",
2849
+ schema: z.object({ ok: z.boolean() }),
2850
+ maxTokens: 10,
2851
+ deadlineMs: 1e4,
2852
+ purpose: "prompt",
2853
+ repositoryRoot: process.cwd()
2854
+ });
2855
+ if (!result.ok) throw new Error(`${result.reason}: ${result.detail}`);
2856
+ console.log(`ok: ${result.model} answered in ${result.latencyMs} ms`);
2857
+ });
2858
+ });
2859
+ llm.command("summarize").option("--limit <n>", "Summarize at most this many files.", Number).description(
2860
+ "Write one-line summaries of changed files now (resumable; paced to the free tier)."
2861
+ ).action(async (opts) => {
2862
+ await runSafely2(async () => {
2863
+ const controller = new AbortController();
2864
+ process.once("SIGINT", () => controller.abort());
2865
+ const result = await summarizeRepository(process.cwd(), {
2866
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {},
2867
+ signal: controller.signal,
2868
+ onProgress: (done, total) => console.log(pc7.dim(`summarized ${done}/${total}`))
2869
+ });
2870
+ console.log(
2871
+ `${result.summarized} summarized, ${result.skipped} skipped, ${result.remaining} remaining (${result.stoppedBy})`
2872
+ );
2873
+ });
2874
+ });
2875
+ }
2876
+
2877
+ // src/cli/commands/indexing.ts
2878
+ function registerIndexCommands(program, runSafely2) {
2879
+ program.command("index").option("--full", "Re-summarize every file, ignoring existing content hashes.").option("--json", "Print machine-readable JSON.").description("Index the repository: content hashes and lightweight file summaries.").action(async (opts) => {
2880
+ await runSafely2(async () => {
2881
+ const result = await indexRepository({ ...opts.full ? { full: true } : {} });
2882
+ if (opts.json) {
2883
+ console.log(JSON.stringify(result, null, 2));
2884
+ return;
2885
+ }
2886
+ printResult(result);
2887
+ });
2888
+ });
2889
+ program.command("watch").option("--debounce <ms>", "Coalesce file-change bursts within this window.", Number).option("--no-llm", "Do not write BYOK model summaries after index passes.").description("Watch the repository and incrementally re-index on change (Ctrl+C to stop).").action(async (opts) => {
2890
+ await runSafely2(async () => {
2891
+ const controller = new AbortController();
2892
+ const stop = () => controller.abort();
2893
+ process.once("SIGINT", stop);
2894
+ process.once("SIGTERM", stop);
2895
+ console.log(pc8.dim("watching for changes\u2026 press Ctrl+C to stop"));
2896
+ await watchRepository({
2897
+ ...opts.debounce !== void 0 ? { debounceMs: opts.debounce } : {},
2898
+ signal: controller.signal,
2899
+ onIndex: (result) => {
2900
+ printResult(result, true);
2901
+ if (!opts.llm || result.added + result.changed === 0) return;
2902
+ void summarizeAfterIndex(process.cwd()).then((summary) => {
2903
+ if (summary && summary.summarized > 0)
2904
+ console.log(pc8.dim(` llm: ${summary.summarized} file(s) summarized`));
2905
+ }).catch((error) => console.error(pc8.red(`llm: ${error.message}`)));
2906
+ },
2907
+ onError: (error) => console.error(pc8.red(error.message))
2908
+ });
2909
+ console.log(pc8.dim("stopped watching"));
2910
+ });
2911
+ });
2912
+ }
2913
+ function printResult(result, compact = false) {
2914
+ const summary = `${result.totalIndexed} indexed (${pc8.green(`+${result.added}`)} ${pc8.yellow(`~${result.changed}`)} ${pc8.dim(`=${result.unchanged}`)} ${pc8.red(`-${result.removed}`)}) in ${result.durationMs}ms`;
2915
+ console.log(compact ? pc8.dim(summary) : summary);
2916
+ if (result.skipped > 0)
2917
+ console.log(pc8.dim(` ${result.skipped} skipped (too large or unreadable)`));
2918
+ for (const error of result.errors) console.log(pc8.red(` ${error.path}: ${error.error}`));
2919
+ }
2920
+
2921
+ // src/cli/commands/init-wizard.ts
2922
+ import { writeFile as writeFile2 } from "fs/promises";
2923
+ import { confirm, isCancel, log, note, password, spinner, text } from "@clack/prompts";
2924
+
2925
+ // src/cli/commands/postgres-docker.ts
2926
+ import { spawn as spawn2 } from "child_process";
2927
+ import { existsSync as existsSync3 } from "fs";
2928
+ import { dirname as dirname11, resolve as resolve12 } from "path";
2929
+ import { fileURLToPath as fileURLToPath2 } from "url";
2930
+ function resolveBundledComposeFile() {
2931
+ const here = dirname11(fileURLToPath2(import.meta.url));
2932
+ const candidates = [
2933
+ // installed package: dist/cli/commands/ -> root
2934
+ resolve12(here, "..", "..", "..", "docker-compose.yml"),
2935
+ // source layout: src/cli/commands/ -> root
2936
+ resolve12(here, "..", "..", "..", "..", "docker-compose.yml"),
2937
+ resolve12(here, "..", "..", "docker-compose.yml"),
2938
+ resolve12(process.cwd(), "docker-compose.yml")
2939
+ ];
2940
+ for (const candidate of candidates) {
2941
+ if (existsSync3(candidate)) return candidate;
2942
+ }
2943
+ throw new Error(`Could not locate docker-compose.yml. Looked in:
2944
+ ${candidates.join("\n ")}`);
2945
+ }
2946
+ async function runDockerCompose(args, options = {}) {
2947
+ const spawnImpl = options.spawnImpl ?? spawn2;
2948
+ const composeFile = resolveBundledComposeFile();
2949
+ const attempts = [
2950
+ { kind: "v2", bin: "docker", args: ["compose", "-f", composeFile, ...args] },
2951
+ { kind: "v1", bin: "docker-compose", args: ["-f", composeFile, ...args] }
2952
+ ];
2953
+ let lastSpawnError = null;
2954
+ for (const attempt of attempts) {
2955
+ try {
2956
+ return await spawnOnce(spawnImpl, attempt);
2957
+ } catch (error) {
2958
+ const err = error;
2959
+ if (err.code !== "ENOENT") throw err;
2960
+ lastSpawnError = err;
2961
+ }
2962
+ }
2963
+ throw new Error(
2964
+ `Could not find docker. Install Docker Desktop (or docker-compose v1) and ensure it is on your PATH. (${lastSpawnError?.message ?? "spawn ENOENT"})`
2965
+ );
2966
+ }
2967
+ function spawnOnce(spawnImpl, attempt) {
2968
+ return new Promise((resolvePromise, rejectPromise) => {
2969
+ const child = spawnImpl(attempt.bin, attempt.args, {
2970
+ stdio: ["ignore", "pipe", "pipe"]
2971
+ });
2972
+ let stdout = "";
2973
+ let stderr = "";
2974
+ child.stdout?.on("data", (chunk) => {
2975
+ stdout += String(chunk);
2976
+ });
2977
+ child.stderr?.on("data", (chunk) => {
2978
+ stderr += String(chunk);
2979
+ });
2980
+ child.on("error", (error) => {
2981
+ rejectPromise(error);
2982
+ });
2983
+ child.on("close", (code) => {
2984
+ resolvePromise({ code: code ?? 0, stdout, stderr });
2985
+ });
2986
+ });
2987
+ }
2988
+ async function postgresUp(options) {
2989
+ return runDockerCompose(["up", "-d"], options ?? {});
2990
+ }
2991
+ async function postgresDown(options) {
2992
+ return runDockerCompose(["down"], options ?? {});
2993
+ }
2994
+ async function postgresStatus(options) {
2995
+ return runDockerCompose(["ps"], options ?? {});
2996
+ }
2997
+
2998
+ // src/cli/commands/init-wizard.ts
2999
+ function bail() {
3000
+ log.warn("Aborted. No config was written.");
3001
+ process.exit(1);
3002
+ }
3003
+ function unwrap(value) {
3004
+ if (isCancel(value)) bail();
3005
+ return value;
3006
+ }
3007
+ async function pingOnce(databaseUrl) {
3008
+ const client = createDatabaseClient({ postgresUrl: databaseUrl });
3009
+ try {
3010
+ return await pingDatabase(client);
3011
+ } catch {
3012
+ return false;
3013
+ } finally {
3014
+ await client.close().catch(() => void 0);
3015
+ }
3016
+ }
3017
+ async function runInitWizard() {
3018
+ const sigintHandler = () => bail();
3019
+ process.on("SIGINT", sigintHandler);
3020
+ try {
3021
+ note(
3022
+ "Sets up your config file, checks Postgres, applies migrations, and (optionally) wires Claude Desktop.",
3023
+ "CodeBuddy init"
3024
+ );
3025
+ const hfToken = unwrap(
3026
+ await password({
3027
+ message: "Hugging Face token (required for embeddings)",
3028
+ validate: (value) => value && value.length > 0 ? void 0 : "HF_TOKEN is required."
3029
+ })
3030
+ );
3031
+ const databaseUrl = unwrap(
3032
+ await text({
3033
+ message: "Postgres URL",
3034
+ initialValue: process.env.DATABASE_URL ?? "postgres://codebuddy:codebuddy@localhost:5432/codebuddy",
3035
+ validate: (value) => value && value.length > 0 ? void 0 : "Postgres URL is required."
3036
+ })
3037
+ );
3038
+ const namespace = unwrap(
3039
+ await text({
3040
+ message: "Default namespace",
3041
+ initialValue: process.env.CODEBUDDY_NAMESPACE ?? "default",
3042
+ validate: (value) => value && /^[a-zA-Z0-9_.:-]+$/.test(value) ? void 0 : "Letters, digits, _ . : - only."
3043
+ })
3044
+ );
3045
+ const dbSpinner = spinner();
3046
+ dbSpinner.start("Checking Postgres connection");
3047
+ let connected = await pingOnce(databaseUrl);
3048
+ dbSpinner.stop(connected ? "Postgres reachable." : "Postgres did not respond.");
3049
+ if (!connected) {
3050
+ const startDocker = unwrap(
3051
+ await confirm({
3052
+ message: "Start the bundled Postgres container with docker compose?",
3053
+ initialValue: true
3054
+ })
3055
+ );
3056
+ if (startDocker) {
3057
+ const dockerSpinner = spinner();
3058
+ dockerSpinner.start("docker compose up -d");
3059
+ try {
3060
+ const result = await postgresUp();
3061
+ if (result.code === 0) {
3062
+ dockerSpinner.stop("Postgres container started.");
3063
+ for (let attempt = 0; attempt < 10 && !connected; attempt++) {
3064
+ await new Promise((resolve14) => setTimeout(resolve14, 500));
3065
+ connected = await pingOnce(databaseUrl);
3066
+ }
3067
+ if (!connected) {
3068
+ log.warn("Container started but did not accept connections in time.");
3069
+ }
3070
+ } else {
3071
+ dockerSpinner.stop(`docker compose failed: ${result.stderr || result.stdout}`);
3072
+ }
3073
+ } catch (error) {
3074
+ dockerSpinner.stop(`docker compose failed: ${error.message}`);
3075
+ }
3076
+ }
3077
+ }
3078
+ if (connected) {
3079
+ const migrateSpinner = spinner();
3080
+ migrateSpinner.start("Enabling pgvector and applying migrations");
3081
+ const client = createDatabaseClient({ postgresUrl: databaseUrl });
3082
+ try {
3083
+ await bootstrapDatabase(client.sql);
3084
+ await runMigrations(client);
3085
+ migrateSpinner.stop("Migrations applied.");
3086
+ } catch (error) {
3087
+ migrateSpinner.stop(`Migration error: ${error.message}`);
3088
+ } finally {
3089
+ await client.close().catch(() => void 0);
3090
+ }
3091
+ } else {
3092
+ log.warn("Skipping migrations \u2014 Postgres is not reachable. Run `codebuddy migrate` later.");
3093
+ }
3094
+ const initialised = await initConfigFile();
3095
+ const fileConfig = {
3096
+ postgresUrl: databaseUrl,
3097
+ namespace,
3098
+ provider: { type: "huggingface", apiKey: hfToken },
3099
+ tokenBudget: 4e3
3100
+ };
3101
+ await writeFile2(initialised.path, `${JSON.stringify(fileConfig, null, 2)}
3102
+ `, {
3103
+ mode: 384
3104
+ });
3105
+ log.success(`Config written to ${initialised.path}`);
3106
+ let claudeInstalled = false;
3107
+ const wireClaude = unwrap(
3108
+ await confirm({
3109
+ message: "Register this namespace with Claude Desktop now?",
3110
+ initialValue: true
3111
+ })
3112
+ );
3113
+ if (wireClaude) {
3114
+ try {
3115
+ const install = await installClaudeEntry({
3116
+ namespace,
3117
+ hfToken,
3118
+ databaseUrl
3119
+ });
3120
+ claudeInstalled = true;
3121
+ log.success(
3122
+ `Added "${install.serverKey}" to ${install.path}. Restart Claude Desktop (\u2318Q) to activate.`
3123
+ );
3124
+ } catch (error) {
3125
+ log.warn(`Claude Desktop install failed: ${error.message}`);
3126
+ }
3127
+ }
3128
+ return {
3129
+ configPath: configPath(),
3130
+ databaseReady: connected,
3131
+ claudeInstalled
3132
+ };
3133
+ } finally {
3134
+ process.off("SIGINT", sigintHandler);
3135
+ }
3136
+ }
3137
+
3138
+ // src/cli/commands/learn.ts
3139
+ import pc9 from "picocolors";
3140
+
3141
+ // src/learning/outcomes.ts
3142
+ async function recordChangeOutcome(input) {
3143
+ if (input.paths.length === 0) throw new Error("At least one path is required.");
3144
+ if (!input.summary.trim()) throw new Error("A non-empty outcome summary is required.");
3145
+ const now = input.now ?? /* @__PURE__ */ new Date();
3146
+ const category = input.outcome === "regression" ? "incident" : "general";
3147
+ const id = generateEntityId("fact", now.getTime());
3148
+ const object = input.outcome === "regression" ? "regression observed" : "verified safe";
3149
+ const content = [
3150
+ `Change outcome: ${input.outcome}`,
3151
+ `Paths: ${input.paths.join(", ")}`,
3152
+ `Summary: ${input.summary.trim()}`,
3153
+ ...input.commit ? [`Commit: ${input.commit}`] : []
3154
+ ].join("\n");
3155
+ const store = new MemoryFileStore(input.repositoryRoot);
3156
+ const subject = `change outcome for ${input.paths[0]}`;
3157
+ const predicate = input.outcome === "regression" ? "caused" : "survived";
3158
+ const persisted = redactPersisted({ subject, predicate, object, content });
3159
+ const path = await store.writeFact({
3160
+ id,
3161
+ namespace: input.namespace,
3162
+ subject,
3163
+ predicate,
3164
+ object,
3165
+ confidence: input.outcome === "regression" ? 0.95 : 0.85,
3166
+ createdAt: now.toISOString(),
3167
+ createdByAgent: "codebuddy:learn",
3168
+ sourceInteractionId: null,
3169
+ sourceDeleted: false,
3170
+ category,
3171
+ paths: input.paths,
3172
+ ...input.outcome === "regression" ? { severity: input.severity ?? "high" } : {},
3173
+ ...input.commit ? { introducedBy: input.commit } : {},
3174
+ ...input.planId ? { sourcePlanId: input.planId } : {},
3175
+ verification: ["explicit change outcome recorded by a project owner"],
3176
+ fingerprint: computeFactHash(input.namespace, persisted.content),
3177
+ content
3178
+ });
3179
+ return { id, path, category };
3180
+ }
3181
+
3182
+ // src/cli/commands/learn.ts
3183
+ function registerLearnCommand(program, runSafely2) {
3184
+ const learn = program.command("learn").description("Record verified team change outcomes.");
3185
+ learn.command("outcome").requiredOption("--outcome <outcome>", "safe or regression").requiredOption("--paths <paths>", "Comma-separated repo-relative paths.").requiredOption("--summary <summary>", "What happened and why it matters.").option("--severity <severity>", "Incident severity: low, medium, high, or critical.").option("--commit <sha>", "Commit associated with the outcome.").option("--plan <id>", "Plan associated with the outcome.").description("Write an explicit, reviewable outcome into shared Markdown memory.").action(
3186
+ async (opts) => {
3187
+ await runSafely2(async () => {
3188
+ if (opts.outcome !== "safe" && opts.outcome !== "regression") {
3189
+ throw new Error("--outcome must be safe or regression.");
3190
+ }
3191
+ const result = await recordChangeOutcome({
3192
+ namespace: await resolveNamespaceFrom(),
3193
+ outcome: opts.outcome,
3194
+ paths: opts.paths.split(",").map((path) => path.trim()).filter(Boolean),
3195
+ summary: opts.summary,
3196
+ ...opts.severity ? { severity: opts.severity } : {},
3197
+ ...opts.commit ? { commit: opts.commit } : {},
3198
+ ...opts.plan ? { planId: opts.plan } : {}
3199
+ });
3200
+ console.log(`${pc9.green("recorded")} ${result.id} (${result.category})`);
3201
+ console.log(pc9.dim(result.path));
3202
+ });
3203
+ }
3204
+ );
3205
+ }
3206
+
3207
+ // src/cli/commands/map.ts
3208
+ import pc10 from "picocolors";
3209
+ function registerMapCommands(program, runSafely2) {
3210
+ const map = program.command("map").description("Query the lightweight architecture map.");
3211
+ map.command("build").description("Scan the current repository and summarize the module graph.").action(async () => {
3212
+ await runSafely2(async () => {
3213
+ const graph = await buildArchitectureMap();
3214
+ console.log(
3215
+ JSON.stringify({ modules: graph.modules.length, edges: graph.edges.length }, null, 2)
3216
+ );
3217
+ });
3218
+ });
3219
+ map.command("deps").argument("<path>", "Repo-relative module path.").option("--json", "Print machine-readable JSON.").description("List modules imported by a file.").action(async (path, opts) => {
3220
+ await runSafely2(async () => {
3221
+ const graph = await buildArchitectureMap();
3222
+ const edges = queryMap(graph, { from: path });
3223
+ if (opts.json) {
3224
+ console.log(JSON.stringify({ edges }, null, 2));
3225
+ return;
3226
+ }
3227
+ if (edges.length === 0) {
3228
+ console.log(pc10.dim("no dependencies found"));
3229
+ return;
3230
+ }
3231
+ for (const edge of edges) console.log(`${edge.from} -> ${edge.to}`);
3232
+ });
3233
+ });
3234
+ map.command("dependents").argument("<path>", "Repo-relative module path.").option("--json", "Print machine-readable JSON.").description("List modules that import a file.").action(async (path, opts) => {
3235
+ await runSafely2(async () => {
3236
+ const graph = await buildArchitectureMap();
3237
+ const result = neighbours(graph, path, "in");
3238
+ if (opts.json) {
3239
+ console.log(JSON.stringify(result, null, 2));
3240
+ return;
3241
+ }
3242
+ if (result.edges.length === 0) {
3243
+ console.log(pc10.dim("no dependents found"));
3244
+ return;
3245
+ }
3246
+ for (const edge of result.edges) console.log(`${edge.from} -> ${edge.to}`);
3247
+ });
3248
+ });
3249
+ }
3250
+
3251
+ // src/cli/commands/memory-review.ts
3252
+ import { spawn as spawn3 } from "child_process";
3253
+ import { relative as relative3 } from "path";
3254
+ import pc11 from "picocolors";
3255
+ function registerMemoryCommands(program, runSafely2) {
3256
+ const memory = program.command("memory").description("Inspect and curate CodeBuddy memory.");
3257
+ const review = memory.command("review").description("Review memory candidates captured automatically after turns.");
3258
+ review.command("list", { isDefault: true }).option("--json", "Print machine-readable JSON.").description("List pending review candidates.").action(async (opts) => {
3259
+ await runSafely2(async () => {
3260
+ const items = await new ReviewStore().list();
3261
+ if (opts.json) {
3262
+ console.log(JSON.stringify(items, null, 2));
3263
+ return;
3264
+ }
3265
+ if (items.length === 0) {
3266
+ console.log(pc11.dim("no pending review items"));
3267
+ return;
3268
+ }
3269
+ for (const item of items) console.log(renderRow(item));
3270
+ console.log(pc11.dim(`
3271
+ ${items.length} pending. approve <id> | reject <id> | show <id>`));
3272
+ });
3273
+ });
3274
+ review.command("show").argument("<id>").description("Show a single review candidate.").action(async (id) => {
3275
+ await runSafely2(async () => {
3276
+ console.log(renderItem(await new ReviewStore().read(id)));
3277
+ });
3278
+ });
3279
+ review.command("approve").argument("<id>").option("--incident <id>", "For a queued incident resolution: the incident to resolve.").description("Promote a review candidate to a durable fact and remove it from the queue.").action(async (id, opts) => {
3280
+ await runSafely2(async () => {
3281
+ const factId = await approveReviewItem(id, {
3282
+ ...opts.incident !== void 0 ? { incidentId: opts.incident } : {}
3283
+ });
3284
+ console.log(`${pc11.green("approved")} ${id} \u2192 ${factId}`);
3285
+ });
3286
+ });
3287
+ review.command("reject").argument("<id>").description("Discard a review candidate without saving it.").action(async (id) => {
3288
+ await runSafely2(async () => {
3289
+ const store = new ReviewStore();
3290
+ await store.read(id);
3291
+ await store.delete(id);
3292
+ console.log(`${pc11.yellow("rejected")} ${id}`);
3293
+ });
3294
+ });
3295
+ review.command("edit").argument("<id>").description("Open a review candidate in $EDITOR and re-validate on save.").action(async (id) => {
3296
+ await runSafely2(async () => {
3297
+ const store = new ReviewStore();
3298
+ const item = await store.read(id);
3299
+ await openEditor(item.path);
3300
+ await store.read(id);
3301
+ console.log(pc11.dim("saved and validated"));
3302
+ });
3303
+ });
3304
+ }
3305
+ function renderRow(item) {
3306
+ const flag = item.sensitive ? pc11.red(" \u26A0 sensitive") : "";
3307
+ return `${classColor(item.class)} ${pc11.dim(item.id)} ${truncate(item.content, 70)}${flag}`;
3308
+ }
3309
+ function renderItem(item) {
3310
+ const lines = [
3311
+ `${classColor(item.class)} ${pc11.dim(item.id)}`,
3312
+ `${pc11.bold("reason:")} ${item.reason}`,
3313
+ `${pc11.bold("confidence:")} ${item.confidence.toFixed(2)}`,
3314
+ `${pc11.bold("triple:")} ${item.subject} \xB7 ${item.predicate} \xB7 ${item.object}`
3315
+ ];
3316
+ if (item.paths.length > 0) lines.push(`${pc11.bold("paths:")} ${item.paths.join(", ")}`);
3317
+ if (item.severity) lines.push(`${pc11.bold("severity:")} ${item.severity}`);
3318
+ if (item.resolvesIncidents) {
3319
+ lines.push(
3320
+ `${pc11.bold("resolves:")} ${item.resolvesIncidents.join(", ") || "(no candidate; approve with --incident <id>)"}`
3321
+ );
3322
+ }
3323
+ if (item.sensitive) lines.push(pc11.red(`\u26A0 sensitive: ${item.sensitiveReasons.join(", ")}`));
3324
+ lines.push("", item.content, "", pc11.dim(relative3(process.cwd(), item.path)));
3325
+ return lines.join("\n");
3326
+ }
3327
+ function classColor(memoryClass) {
3328
+ switch (memoryClass) {
3329
+ case "incident":
3330
+ return pc11.red(memoryClass);
3331
+ case "decision":
3332
+ return pc11.cyan(memoryClass);
3333
+ case "fact":
3334
+ return pc11.green(memoryClass);
3335
+ case "note":
3336
+ return pc11.yellow(memoryClass);
3337
+ default:
3338
+ return pc11.dim(memoryClass);
3339
+ }
3340
+ }
3341
+ function truncate(value, max) {
3342
+ const trimmed = value.trim();
3343
+ return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max - 1)}\u2026`;
3344
+ }
3345
+ function openEditor(path) {
3346
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3347
+ return new Promise((resolvePromise, reject) => {
3348
+ const child = spawn3(editor, [path], { stdio: "inherit" });
3349
+ child.on("error", reject);
3350
+ child.on("close", () => resolvePromise());
3351
+ });
3352
+ }
3353
+
3354
+ // src/cli/commands/plan.ts
3355
+ import { spawn as spawn4 } from "child_process";
3356
+ import { relative as relative4 } from "path";
3357
+ import pc12 from "picocolors";
3358
+ function makeLifecycle(cwd = process.cwd()) {
3359
+ const store = new PlanFileStore(cwd);
3360
+ return { store, lifecycle: new PlanLifecycle(store) };
3361
+ }
3362
+ function statusColor(status) {
3363
+ switch (status) {
3364
+ case "draft":
3365
+ return pc12.dim(status);
3366
+ case "approved":
3367
+ return pc12.cyan(status);
3368
+ case "executing":
3369
+ return pc12.yellow(status);
3370
+ case "complete":
3371
+ return pc12.green(status);
3372
+ case "abandoned":
3373
+ return pc12.red(status);
3374
+ }
3375
+ }
3376
+ function renderPlan(plan) {
3377
+ const lines = [];
3378
+ lines.push(`${pc12.bold(plan.title)} ${statusColor(plan.status)}`);
3379
+ lines.push(pc12.dim(plan.id));
3380
+ lines.push("");
3381
+ lines.push(`${pc12.bold("Brief:")} ${plan.brief}`);
3382
+ if (plan.filesToTouch.length > 0) {
3383
+ lines.push("");
3384
+ lines.push(pc12.bold("Files to touch:"));
3385
+ for (const file of plan.filesToTouch) {
3386
+ lines.push(` ${file.action.padEnd(6)} ${file.path}${file.notes ? ` \u2014 ${file.notes}` : ""}`);
3387
+ }
3388
+ }
3389
+ if (plan.testsToAdd.length > 0) {
3390
+ lines.push("");
3391
+ lines.push(pc12.bold("Tests to add:"));
3392
+ for (const test of plan.testsToAdd) {
3393
+ lines.push(` ${test.path}${test.description ? ` \u2014 ${test.description}` : ""}`);
3394
+ }
3395
+ }
3396
+ if (plan.outOfScope.length > 0) {
3397
+ lines.push("");
3398
+ lines.push(pc12.bold("Out of scope:"));
3399
+ for (const item of plan.outOfScope) lines.push(` - ${item}`);
3400
+ }
3401
+ if (plan.risks.length > 0) {
3402
+ lines.push("");
3403
+ lines.push(pc12.bold("Risks:"));
3404
+ for (const item of plan.risks) lines.push(` - ${item}`);
3405
+ }
3406
+ if (plan.commitSha) {
3407
+ lines.push("");
3408
+ lines.push(`${pc12.bold("Commit:")} ${plan.commitSha}`);
3409
+ }
3410
+ lines.push("");
3411
+ lines.push(pc12.dim("\u2500".repeat(40)));
3412
+ lines.push(plan.body);
3413
+ return lines.join("\n");
3414
+ }
3415
+ function openEditor2(path) {
3416
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3417
+ return new Promise((resolve14, reject) => {
3418
+ const child = spawn4(editor, [path], { stdio: "inherit" });
3419
+ child.on("error", reject);
3420
+ child.on("close", () => resolve14());
3421
+ });
3422
+ }
3423
+ function runGit(args) {
3424
+ return new Promise((resolve14, reject) => {
3425
+ const child = spawn4("git", args, { stdio: ["ignore", "pipe", "pipe"] });
3426
+ let stdout = "";
3427
+ let stderr = "";
3428
+ child.stdout.on("data", (c) => {
3429
+ stdout += String(c);
3430
+ });
3431
+ child.stderr.on("data", (c) => {
3432
+ stderr += String(c);
3433
+ });
3434
+ child.on("error", reject);
3435
+ child.on("close", (code) => resolve14({ code: code ?? 0, stdout, stderr }));
3436
+ });
3437
+ }
3438
+ function registerPlanCommands(program, runSafely2) {
3439
+ const plan = program.command("plan").description("Author and manage durable Markdown plans.");
3440
+ plan.command("new").argument("<brief...>", "One-line description of the work.").option("--title <title>", "Plan title (defaults to the brief).").option("--no-edit", "Skip opening $EDITOR after creating the draft.").description("Create a draft plan and open it in $EDITOR.").action(async (briefParts, opts) => {
3441
+ await runSafely2(async () => {
3442
+ const { lifecycle } = makeLifecycle();
3443
+ const namespace = await resolveNamespaceFrom();
3444
+ const brief = briefParts.join(" ");
3445
+ const created = await lifecycle.create({
3446
+ namespace,
3447
+ title: opts.title ?? brief,
3448
+ brief
3449
+ });
3450
+ console.log(`${pc12.green("created")} ${created.id} (${statusColor(created.status)})`);
3451
+ if (opts.edit !== false && process.stdin.isTTY) {
3452
+ await openEditor2(created.path);
3453
+ const { store } = makeLifecycle();
3454
+ await store.readPlan(created.id);
3455
+ console.log(pc12.dim("saved and validated"));
3456
+ }
3457
+ console.log(pc12.dim(relative4(process.cwd(), created.path)));
3458
+ });
3459
+ });
3460
+ plan.command("list").option("--status <status>", "Filter by status.").option("--all", "Include completed and abandoned plans.").description("List plans in the current namespace.").action(async (opts) => {
3461
+ await runSafely2(async () => {
3462
+ const { store } = makeLifecycle();
3463
+ const namespace = await resolveNamespaceFrom();
3464
+ let plans = (await store.listPlans()).filter((p) => p.namespace === namespace);
3465
+ if (opts.status) plans = plans.filter((p) => p.status === opts.status);
3466
+ else if (!opts.all)
3467
+ plans = plans.filter((p) => p.status !== "complete" && p.status !== "abandoned");
3468
+ if (plans.length === 0) {
3469
+ console.log(pc12.dim("no plans"));
3470
+ return;
3471
+ }
3472
+ for (const p of plans) {
3473
+ console.log(`${statusColor(p.status).padEnd(20)} ${pc12.bold(p.title)} ${pc12.dim(p.id)}`);
3474
+ }
3475
+ });
3476
+ });
3477
+ plan.command("show").argument("<id>").description("Show a single plan.").action(async (id) => {
3478
+ await runSafely2(async () => {
3479
+ const { store } = makeLifecycle();
3480
+ console.log(renderPlan(await store.readPlan(id)));
3481
+ });
3482
+ });
3483
+ plan.command("edit").argument("<id>").description("Open a plan in $EDITOR and validate on save.").action(async (id) => {
3484
+ await runSafely2(async () => {
3485
+ const { store } = makeLifecycle();
3486
+ const existing = await store.readPlan(id);
3487
+ await openEditor2(existing.path);
3488
+ await store.readPlan(id);
3489
+ console.log(pc12.dim("saved and validated"));
3490
+ });
3491
+ });
3492
+ plan.command("approve").argument("<id>").description("Promote a draft to approved (one active plan per namespace).").action(async (id) => {
3493
+ await runSafely2(async () => {
3494
+ const { lifecycle } = makeLifecycle();
3495
+ const next = await lifecycle.approve(id);
3496
+ console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
3497
+ });
3498
+ });
3499
+ plan.command("start").argument("<id>").description("Move an approved plan to executing.").action(async (id) => {
3500
+ await runSafely2(async () => {
3501
+ const { lifecycle } = makeLifecycle();
3502
+ const next = await lifecycle.start(id);
3503
+ console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
3504
+ });
3505
+ });
3506
+ plan.command("complete").argument("<id>").option("--commit <sha>", "Git commit the plan was completed in.").option("--notes <text>", "Completion notes.").description("Mark an executing plan complete.").action(async (id, opts) => {
3507
+ await runSafely2(async () => {
3508
+ const { lifecycle } = makeLifecycle();
3509
+ const next = await lifecycle.complete(id, {
3510
+ ...opts.commit ? { commitSha: opts.commit } : {},
3511
+ ...opts.notes ? { notes: opts.notes } : {}
3512
+ });
3513
+ if (!opts.commit) console.log(pc12.yellow("note: no --commit supplied"));
3514
+ console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
3515
+ });
3516
+ });
3517
+ plan.command("abandon").argument("<id>").option("--reason <text>", "Why the plan was abandoned.").description("Abandon a plan without completing it.").action(async (id, opts) => {
3518
+ await runSafely2(async () => {
3519
+ const { lifecycle } = makeLifecycle();
3520
+ const next = await lifecycle.abandon(id, opts.reason ? { reason: opts.reason } : {});
3521
+ console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
3522
+ });
3523
+ });
3524
+ plan.command("diff").argument("<id>").option("--from <ref>", "Git ref to compare from.", "HEAD").option("--to <ref>", "Git ref to compare to.").description("Show the git diff of a plan file between two revisions.").action(async (id, opts) => {
3525
+ await runSafely2(async () => {
3526
+ const { store } = makeLifecycle();
3527
+ const plan2 = await store.readPlan(id);
3528
+ const relPath = relative4(process.cwd(), plan2.path);
3529
+ const args = ["diff", opts.from, ...opts.to ? [opts.to] : [], "--", relPath];
3530
+ const result = await runGit(args);
3531
+ if (result.stdout.trim().length === 0) {
3532
+ console.log(pc12.dim("no changes"));
3533
+ } else {
3534
+ process.stdout.write(result.stdout);
3535
+ }
3536
+ if (result.code !== 0 && result.stderr) process.stderr.write(result.stderr);
3537
+ });
3538
+ });
3539
+ plan.command("policy").argument("[mode]", `One of: ${PLAN_POLICIES.join(", ")}. Omit to read the current value.`).description("Read or set the plan policy (off | suggest | required).").action(async (mode) => {
3540
+ await runSafely2(async () => {
3541
+ if (mode === void 0) {
3542
+ console.log(await loadPlanPolicy());
3543
+ return;
3544
+ }
3545
+ if (!PLAN_POLICIES.includes(mode)) {
3546
+ throw new Error(`Invalid policy "${mode}". Use one of: ${PLAN_POLICIES.join(", ")}.`);
3547
+ }
3548
+ await setPlanPolicy(mode);
3549
+ console.log(`${pc12.green("plan policy")} = ${mode}`);
3550
+ });
3551
+ });
3552
+ }
3553
+
3554
+ // src/cli/commands/risk.ts
3555
+ import pc13 from "picocolors";
3556
+ function registerRiskCommands(program, runSafely2) {
3557
+ const risk = program.command("risk").description("Assess evidence-backed change risk.");
3558
+ risk.command("assess").option("--plan <id>", "Assess paths listed in a CodeBuddy plan.").option("--paths <paths>", "Comma-separated repo-relative paths to assess.").option("--git", "Assess staged, unstaged, and untracked git changes.").option("--limit <n>", "Maximum risk items to show.", Number).option("--json", "Print machine-readable JSON.").option("--explain", "Show evidence for each risk item.").description("Assess risk for a plan, explicit paths, or current git changes.").action(
3559
+ async (opts) => {
3560
+ await runSafely2(async () => {
3561
+ const namespace = await resolveNamespaceFrom();
3562
+ const assessment = await assessRisk({
3563
+ namespace,
3564
+ ...opts.plan ? { planId: opts.plan } : {},
3565
+ ...opts.paths ? { paths: parsePaths3(opts.paths) } : {},
3566
+ ...opts.git !== void 0 ? { useGit: opts.git } : {},
3567
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {}
3568
+ });
3569
+ if (opts.json) {
3570
+ console.log(JSON.stringify(assessment, null, 2));
3571
+ return;
3572
+ }
3573
+ printAssessment(assessment, opts.explain ?? false);
3574
+ });
3575
+ }
3576
+ );
3577
+ }
3578
+ function parsePaths3(value) {
3579
+ return value.split(",").map((path) => path.trim()).filter(Boolean);
3580
+ }
3581
+ function printAssessment(assessment, explain) {
3582
+ if (assessment.items.length === 0) {
3583
+ console.log(pc13.dim("no risk evidence found"));
3584
+ return;
3585
+ }
3586
+ for (const item of assessment.items) {
3587
+ console.log(
3588
+ `${pc13.bold(item.path)} score ${scoreColor2(item.score)} category ${pc13.cyan(item.category)}`
3589
+ );
3590
+ console.log(` ${item.reason}`);
3591
+ if (explain) {
3592
+ for (const evidence of item.evidence) console.log(` \u21B3 ${formatEvidence(evidence)}`);
3593
+ } else {
3594
+ console.log(pc13.dim(` ${item.evidence.length} evidence item(s)`));
3595
+ }
3596
+ }
3597
+ console.log(
3598
+ pc13.dim(
3599
+ `
3600
+ ${assessment.stats.itemsProduced} item(s), ${assessment.stats.signalsRun} signal(s), ${assessment.stats.pathsConsidered} path(s) considered`
3601
+ )
3602
+ );
3603
+ }
3604
+ function scoreColor2(score) {
3605
+ if (score >= 75) return pc13.red(String(score));
3606
+ if (score >= 50) return pc13.yellow(String(score));
3607
+ return pc13.green(String(score));
3608
+ }
3609
+ function formatEvidence(evidence) {
3610
+ switch (evidence.kind) {
3611
+ case "memory_fact":
3612
+ return `memory ${evidence.factId}: ${truncate2(evidence.content, 90)}`;
3613
+ case "git_commit":
3614
+ return `commit ${evidence.sha.slice(0, 7)} ${evidence.ts}: ${evidence.message}`;
3615
+ case "policy_rule":
3616
+ return `policy ${evidence.ruleId}: ${evidence.pattern}`;
3617
+ case "dependent_module":
3618
+ return `dependent ${evidence.path} depth=${evidence.depth}`;
3619
+ case "ci_failure":
3620
+ return `ci ${evidence.runId} ${evidence.conclusion} ${evidence.ts}`;
3621
+ }
3622
+ }
3623
+ function truncate2(value, max) {
3624
+ return value.length <= max ? value : `${value.slice(0, max - 1)}\u2026`;
3625
+ }
3626
+
3627
+ // src/cli/commands/rules.ts
3628
+ import { relative as relative5 } from "path";
3629
+ import pc14 from "picocolors";
3630
+ async function resolveMode(client, value) {
3631
+ if (value) {
3632
+ const mode = parseMode(value);
3633
+ if (!supportedModes(CLIENT_ADAPTERS[client]).includes(mode)) {
3634
+ throw new Error(`${clientDisplayName(client)} does not support --mode ${mode}.`);
3635
+ }
3636
+ return mode;
3637
+ }
3638
+ return defaultRulesMode(client);
3639
+ }
3640
+ function registerRulesCommands(program, runSafely2) {
3641
+ const rules = program.command("rules").description("Print or install the client workflow templates (CLAUDE.md / AGENTS.md).");
3642
+ rules.command("show").option("--client <client>", `One of: ${WORKFLOW_CLIENTS.join(", ")}.`, "claude").option("--mode <mode>", "agent or hooks (default: follows installed hooks).").description("Print the workflow template for a client.").action(async (opts) => {
3643
+ await runSafely2(async () => {
3644
+ const client = parseClient(opts.client);
3645
+ const mode = await resolveMode(client, opts.mode);
3646
+ if (mode === "mcp-only") {
3647
+ console.log("MCP-only mode installs no workflow rules.");
3648
+ return;
3649
+ }
3650
+ console.log(renderWorkflowTemplate(client, mode));
3651
+ });
3652
+ });
3653
+ rules.command("install").option("--client <client>", `One of: ${WORKFLOW_CLIENTS.join(", ")}.`, "claude").option("--path <file>", "Override the target rules file.").option(
3654
+ "--mode <mode>",
3655
+ `One of: ${AUTOMATION_MODES.join(", ")} (default: follows installed hooks; mcp-only removes the block).`
3656
+ ).description("Install/update the workflow template in the client's rules file (managed block).").action(async (opts) => {
3657
+ await runSafely2(async () => {
3658
+ const client = parseClient(opts.client);
3659
+ const result = await installWorkflowRules({
3660
+ client,
3661
+ mode: await resolveMode(client, opts.mode),
3662
+ ...opts.path ? { targetPath: opts.path } : {}
3663
+ });
3664
+ const where = relative5(process.cwd(), result.path) || result.path;
3665
+ console.log(
3666
+ `${actionLabel(result.action)} ${clientDisplayName(client)} workflow in ${where}`
3667
+ );
3668
+ if (result.action !== "unchanged") {
3669
+ console.log(pc14.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
3670
+ }
3671
+ });
3672
+ });
3673
+ }
3674
+ function actionLabel(action) {
3675
+ switch (action) {
3676
+ case "created":
3677
+ return pc14.green("created");
3678
+ case "added":
3679
+ return pc14.green("added");
3680
+ case "updated":
3681
+ return pc14.cyan("updated");
3682
+ case "removed":
3683
+ return pc14.yellow("removed");
3684
+ default:
3685
+ return pc14.dim("unchanged");
3686
+ }
3687
+ }
3688
+
3689
+ // src/cli/commands/suggest.ts
3690
+ import pc15 from "picocolors";
3691
+ function parsePaths4(value) {
3692
+ return value.split(",").map((path) => path.trim()).filter(Boolean);
3693
+ }
3694
+ function registerSuggestCommand(program, runSafely2) {
3695
+ program.command("suggest").option("--paths <paths>", "Comma-separated repo-relative paths to review.").option("--plan <id>", "Review the files listed in a plan.").option("--git", "Review current git changes.").option("--limit <n>", "Maximum suggestions to show.", Number).option("--json", "Print machine-readable JSON.").description("Suggest evidence-backed, read-only code-quality improvements.").action(
3696
+ async (opts) => {
3697
+ await runSafely2(async () => {
3698
+ const namespace = await resolveNamespaceFrom();
3699
+ const result = await generateSuggestions({
3700
+ namespace,
3701
+ ...opts.paths ? { paths: parsePaths4(opts.paths) } : {},
3702
+ ...opts.plan ? { planId: opts.plan } : {},
3703
+ ...opts.git !== void 0 ? { useGit: opts.git } : {},
3704
+ ...opts.limit !== void 0 ? { limit: opts.limit } : {}
3705
+ });
3706
+ if (opts.json) {
3707
+ console.log(JSON.stringify(result, null, 2));
3708
+ return;
3709
+ }
3710
+ printSuggestions(result.suggestions);
3711
+ console.log(
3712
+ pc15.dim(
3713
+ `
3714
+ ${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} path(s)`
3715
+ )
3716
+ );
3717
+ });
3718
+ }
3719
+ );
3720
+ }
3721
+ function printSuggestions(suggestions) {
3722
+ if (suggestions.length === 0) {
3723
+ console.log(pc15.dim("no suggestions \u2014 nothing high-signal to report"));
3724
+ return;
3725
+ }
3726
+ for (const suggestion of suggestions) {
3727
+ console.log(`${severityBadge2(suggestion.severity)} ${pc15.bold(suggestion.title)}`);
3728
+ console.log(` ${suggestion.detail}`);
3729
+ for (const evidence of suggestion.evidence) {
3730
+ console.log(pc15.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
3731
+ }
3732
+ if (suggestion.commandHint) console.log(pc15.dim(` \u2192 ${suggestion.commandHint}`));
3733
+ }
3734
+ }
3735
+ function severityBadge2(severity) {
3736
+ switch (severity) {
3737
+ case "high":
3738
+ return pc15.red("[high]");
3739
+ case "medium":
3740
+ return pc15.yellow("[med ]");
3741
+ case "low":
3742
+ return pc15.cyan("[low ]");
3743
+ default:
3744
+ return pc15.dim("[info]");
3745
+ }
3746
+ }
3747
+
3748
+ // src/cli/commands/symbols.ts
3749
+ import { readFile as readFile13 } from "fs/promises";
3750
+ import { resolve as resolve13 } from "path";
3751
+ import pc16 from "picocolors";
3752
+ function registerSymbolsCommand(program, runSafely2) {
3753
+ 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) => {
3754
+ await runSafely2(async () => {
3755
+ if (!supportsSymbols(path)) {
3756
+ console.log(pc16.dim(`no symbol extraction for ${path} (unsupported language)`));
3757
+ return;
3758
+ }
3759
+ const content = await readFile13(resolve13(path), "utf8");
3760
+ const table = extractSymbolTable(content);
3761
+ if (opts.json) {
3762
+ console.log(JSON.stringify({ path, symbols: table }, null, 2));
3763
+ return;
3764
+ }
3765
+ if (table.length === 0) {
3766
+ console.log(pc16.dim("no top-level symbols found"));
3767
+ return;
3768
+ }
3769
+ for (const symbol of table) {
3770
+ const badge = symbol.exported ? pc16.green("export") : pc16.dim("local ");
3771
+ console.log(
3772
+ `${badge} ${pc16.cyan(symbol.kind.padEnd(9))} ${pc16.bold(symbol.name)} ${pc16.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
3773
+ );
3774
+ }
3775
+ console.log(pc16.dim(`
3776
+ ${table.length} symbol(s)`));
3777
+ });
3778
+ });
3779
+ }
3780
+
3781
+ // src/cli/commands/use-command.ts
3782
+ import { basename } from "path";
3783
+ import { confirm as confirm2, isCancel as isCancel2, log as log2, note as note2, password as password2, spinner as spinner2, text as text2 } from "@clack/prompts";
3784
+ import pc17 from "picocolors";
3785
+
3786
+ // src/core/global-config.ts
3787
+ import { chmod, mkdir as mkdir8, readFile as readFile14, writeFile as writeFile3 } from "fs/promises";
3788
+ import { homedir as homedir3 } from "os";
3789
+ import { dirname as dirname12, join as join15 } from "path";
3790
+ import { z as z2 } from "zod";
3791
+ var GLOBAL_DIR = ".codebuddy";
3792
+ var GLOBAL_FILE = "global.json";
3793
+ var globalConfigSchema = z2.object({
3794
+ hfToken: z2.string().min(1).optional(),
3795
+ databaseUrl: z2.string().min(1).optional(),
3796
+ // Where the codebuddy install lives on this machine. Cached so future
3797
+ // `codebuddy use` calls in unrelated projects can find the CLI entry
3798
+ // without re-resolving via process.argv every time.
3799
+ installPath: z2.string().optional()
3800
+ });
3801
+ function globalConfigPath(home = homedir3()) {
3802
+ return join15(home, GLOBAL_DIR, GLOBAL_FILE);
3803
+ }
3804
+ async function readGlobalConfig(home = homedir3()) {
3805
+ const path = globalConfigPath(home);
3806
+ try {
3807
+ const raw = await readFile14(path, "utf8");
3808
+ return globalConfigSchema.parse(JSON.parse(raw));
3809
+ } catch (error) {
3810
+ if (error.code === "ENOENT") return {};
3811
+ throw error;
3812
+ }
3813
+ }
3814
+ async function writeGlobalConfig(next, home = homedir3()) {
3815
+ const path = globalConfigPath(home);
3816
+ await mkdir8(dirname12(path), { recursive: true, mode: 448 });
3817
+ await writeFile3(path, `${JSON.stringify(next, null, 2)}
3818
+ `, { mode: 384 });
3819
+ await chmod(path, 384);
3820
+ return { path };
3821
+ }
3822
+ async function mergeGlobalConfig(patch, home = homedir3()) {
3823
+ const current = await readGlobalConfig(home);
3824
+ const merged = { ...current, ...patch };
3825
+ const { path } = await writeGlobalConfig(merged, home);
3826
+ return { path, merged };
3827
+ }
3828
+
3829
+ // src/cli/commands/docker-autostart.ts
3830
+ import { spawn as spawn5 } from "child_process";
3831
+ async function isDockerDaemonRunning() {
3832
+ return new Promise((resolve14) => {
3833
+ const child = spawn5("docker", ["info"], { stdio: "ignore" });
3834
+ child.on("error", () => resolve14(false));
3835
+ child.on("close", (code) => resolve14(code === 0));
3836
+ });
3837
+ }
3838
+ async function tryStartDockerDesktop() {
3839
+ if (process.platform !== "darwin") return { started: false };
3840
+ return new Promise((resolve14) => {
3841
+ const child = spawn5("open", ["-ga", "Docker"], { stdio: "ignore" });
3842
+ child.on("error", () => resolve14({ started: false }));
3843
+ child.on("close", (code) => resolve14({ started: code === 0 }));
3844
+ });
3845
+ }
3846
+ async function waitForDockerDaemon(timeoutMs = 6e4) {
3847
+ const deadline = Date.now() + timeoutMs;
3848
+ while (Date.now() < deadline) {
3849
+ if (await isDockerDaemonRunning()) return true;
3850
+ await new Promise((resolve14) => setTimeout(resolve14, 1e3));
3851
+ }
3852
+ return false;
3853
+ }
3854
+
3855
+ // src/cli/commands/use-command.ts
3856
+ function bail2() {
3857
+ log2.warn("Aborted.");
3858
+ process.exit(1);
3859
+ }
3860
+ function unwrap2(value) {
3861
+ if (isCancel2(value)) bail2();
3862
+ return value;
3863
+ }
3864
+ async function pingOnce2(databaseUrl) {
3865
+ const client = createDatabaseClient({ postgresUrl: databaseUrl });
3866
+ try {
3867
+ return await pingDatabase(client);
3868
+ } catch {
3869
+ return false;
3870
+ } finally {
3871
+ await client.close().catch(() => void 0);
3872
+ }
3873
+ }
3874
+ function sanitizeNamespace(input) {
3875
+ return input.replace(/[^a-zA-Z0-9_.:-]/g, "-").slice(0, 128) || "default";
3876
+ }
3877
+ var DEFAULT_DB_URL = "postgres://codebuddy:codebuddy@localhost:5432/codebuddy";
3878
+ async function runUseCommand(options = {}) {
3879
+ const sigintHandler = () => bail2();
3880
+ process.on("SIGINT", sigintHandler);
3881
+ try {
3882
+ const folderName = basename(process.cwd());
3883
+ const namespace = sanitizeNamespace(options.namespace ?? folderName);
3884
+ await initProjectScaffold();
3885
+ const graphifyEnabled = options.graphify ?? unwrap2(
3886
+ await confirm2({
3887
+ message: "Set up Graphify for richer architecture context?",
3888
+ initialValue: true
3889
+ })
3890
+ );
3891
+ note2(
3892
+ `Project folder: ${pc17.bold(process.cwd())}
3893
+ Namespace: ${pc17.bold(namespace)}`,
3894
+ "codebuddy use"
3895
+ );
3896
+ const stored = await readGlobalConfig();
3897
+ let hfToken;
3898
+ if (process.env.HF_TOKEN) {
3899
+ hfToken = process.env.HF_TOKEN;
3900
+ log2.info("Using HF_TOKEN from environment.");
3901
+ } else if (stored.hfToken) {
3902
+ hfToken = stored.hfToken;
3903
+ log2.info(`Reusing saved Hugging Face token (${pc17.dim("~/.codebuddy/global.json")}).`);
3904
+ } else {
3905
+ hfToken = unwrap2(
3906
+ await password2({
3907
+ message: "Hugging Face token (optional; press Enter for local-only mode)"
3908
+ })
3909
+ );
3910
+ if (!hfToken)
3911
+ log2.info("No Hugging Face token supplied; local file context remains available.");
3912
+ }
3913
+ const initialUrl = options.postgresUrl ?? process.env.DATABASE_URL ?? stored.databaseUrl ?? DEFAULT_DB_URL;
3914
+ const databaseUrl = unwrap2(
3915
+ await text2({
3916
+ message: "Postgres URL (press Enter to accept)",
3917
+ initialValue: initialUrl,
3918
+ validate: (value) => value && value.length > 0 ? void 0 : "Postgres URL is required."
3919
+ })
3920
+ );
3921
+ const dbSpinner = spinner2();
3922
+ dbSpinner.start("Checking Postgres");
3923
+ let connected = await pingOnce2(databaseUrl);
3924
+ if (connected) dbSpinner.stop("Local Postgres reachable.");
3925
+ if (!connected && databaseUrl === DEFAULT_DB_URL) {
3926
+ dbSpinner.stop("Postgres unreachable on the default local URL.");
3927
+ }
3928
+ const shouldUseDocker = !connected && databaseUrl === DEFAULT_DB_URL && (options.useDocker ?? unwrap2(
3929
+ await confirm2({
3930
+ message: "Postgres is unavailable. Start the bundled Docker database now?",
3931
+ initialValue: true
3932
+ })
3933
+ ));
3934
+ if (shouldUseDocker) {
3935
+ if (!await isDockerDaemonRunning()) {
3936
+ const dockerSpinner = spinner2();
3937
+ dockerSpinner.start("Starting Docker Desktop");
3938
+ await tryStartDockerDesktop();
3939
+ const ready = await waitForDockerDaemon(6e4);
3940
+ if (!ready) {
3941
+ dockerSpinner.stop("Docker daemon did not start in 60s. Aborting.");
3942
+ bail2();
3943
+ }
3944
+ dockerSpinner.stop("Docker daemon is up.");
3945
+ }
3946
+ const composeSpinner = spinner2();
3947
+ composeSpinner.start("docker compose up -d");
3948
+ try {
3949
+ const result = await postgresUp();
3950
+ if (result.code === 0) {
3951
+ composeSpinner.stop("Postgres container started.");
3952
+ for (let attempt = 0; attempt < 12 && !connected; attempt++) {
3953
+ await new Promise((resolve14) => setTimeout(resolve14, 500));
3954
+ connected = await pingOnce2(databaseUrl);
3955
+ }
3956
+ if (!connected) {
3957
+ log2.warn("Container running but Postgres did not accept connections in time.");
3958
+ }
3959
+ } else {
3960
+ composeSpinner.stop(`docker compose failed: ${result.stderr || result.stdout}`);
3961
+ }
3962
+ } catch (error) {
3963
+ composeSpinner.stop(`docker compose failed: ${error.message}`);
3964
+ }
3965
+ } else if (!connected) {
3966
+ dbSpinner.stop(connected ? "Postgres reachable." : "Postgres unreachable at this URL.");
3967
+ log2.warn(
3968
+ "Local Postgres is required. Start it, set DATABASE_URL, pass --postgres-url, or allow the bundled Docker database."
3969
+ );
3970
+ bail2();
3971
+ }
3972
+ if (!connected) bail2();
3973
+ const migrateSpinner = spinner2();
3974
+ migrateSpinner.start("Enabling pgvector + applying migrations");
3975
+ {
3976
+ const client = createDatabaseClient({ postgresUrl: databaseUrl });
3977
+ try {
3978
+ await bootstrapDatabase(client.sql);
3979
+ await runMigrations(client);
3980
+ migrateSpinner.stop("Database ready.");
3981
+ } catch (error) {
3982
+ migrateSpinner.stop(`Migration error: ${error.message}`);
3983
+ bail2();
3984
+ } finally {
3985
+ await client.close().catch(() => void 0);
3986
+ }
3987
+ }
3988
+ await mergeGlobalConfig({
3989
+ ...hfToken ? { hfToken } : {},
3990
+ databaseUrl,
3991
+ installPath: process.argv[1] ?? void 0
3992
+ });
3993
+ const existingConfig = await readConfigFile();
3994
+ const indexSpinner = spinner2();
3995
+ indexSpinner.start("Indexing the existing repository");
3996
+ try {
3997
+ const result = await indexRepository({ repositoryRoot: process.cwd() });
3998
+ indexSpinner.stop(`Repository indexed (${result.totalIndexed} files).`);
3999
+ } catch (error) {
4000
+ indexSpinner.stop(`Repository indexing skipped: ${error.message}`);
4001
+ }
4002
+ const graphifySetup = graphifyEnabled ? await setupGraphify({ repositoryRoot: process.cwd() }) : null;
4003
+ const graphifyConfigured = graphifySetup?.available === true && graphifySetup.generated;
4004
+ await writeConfigFile({
4005
+ ...existingConfig,
4006
+ postgresUrl: databaseUrl,
4007
+ namespace,
4008
+ provider: { type: "huggingface", ...hfToken ? { apiKey: hfToken } : {} },
4009
+ tokenBudget: existingConfig.tokenBudget ?? 4e3,
4010
+ graphify: { enabled: graphifyConfigured, graphPath: "graphify-out/graph.json" }
4011
+ });
4012
+ const graphifyMessage = graphifySetup?.message ?? "Graphify disabled; CodeBuddy MCP remains available.";
4013
+ await writeProjectMcpConfig({
4014
+ repositoryRoot: process.cwd(),
4015
+ namespace,
4016
+ graphify: graphifyConfigured,
4017
+ ...graphifySetup?.graphPath ? { graphPath: graphifySetup.graphPath } : {}
4018
+ });
4019
+ await installWorkflowRules({ client: "claude", projectRoot: process.cwd() });
4020
+ await installWorkflowRules({ client: "codex", projectRoot: process.cwd() });
4021
+ if (!options.skipClaude) {
4022
+ try {
4023
+ const install = await installClaudeEntry({
4024
+ namespace,
4025
+ projectRoot: process.cwd(),
4026
+ hfToken,
4027
+ databaseUrl
4028
+ });
4029
+ log2.success(
4030
+ `Registered ${pc17.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
4031
+ );
4032
+ } catch (error) {
4033
+ log2.warn(`Claude Desktop registration skipped: ${error.message}`);
4034
+ }
4035
+ }
4036
+ if (!options.skipCodex) {
4037
+ try {
4038
+ const install = await installCodexEntry({
4039
+ namespace,
4040
+ projectRoot: process.cwd(),
4041
+ hfToken,
4042
+ databaseUrl
4043
+ });
4044
+ log2.success(
4045
+ `Registered ${pc17.bold(install.serverKey)} in Codex. Restart Codex to activate.`
4046
+ );
4047
+ } catch (error) {
4048
+ log2.warn(`Codex registration skipped: ${error.message}`);
4049
+ }
4050
+ }
4051
+ note2(
4052
+ [
4053
+ `${pc17.green("\u2713")} CodeBuddy is ready for ${pc17.bold(folderName)}.`,
4054
+ `${pc17.green("\u2713")} Scaffolded ${pc17.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
4055
+ `${pc17.green("\u2713")} ${graphifyMessage}`,
4056
+ `${pc17.green("\u2713")} Added agent workflow instructions to ${pc17.cyan("CLAUDE.md")} and ${pc17.cyan("AGENTS.md")}.`,
4057
+ ``,
4058
+ `Next:`,
4059
+ ` 1. Restart Claude Desktop and Codex.`,
4060
+ ` 2. Ask Claude or Codex to use the ${pc17.cyan("context_pack")} tool for each coding task.`,
4061
+ ` 3. Indexing, Graphify, Markdown memory import, and embeddings now update automatically while the MCP server is running.`,
4062
+ ` 4. Run ${pc17.cyan("codebuddy doctor")} or ${pc17.cyan("codebuddy db doctor")} if you want a full health check.`,
4063
+ ``,
4064
+ `Manage:`,
4065
+ ` codebuddy claude list ${pc17.dim("# see every project wired up")}`,
4066
+ ` codebuddy codex list ${pc17.dim("# see Codex entries")}`,
4067
+ ` codebuddy db test ${pc17.dim("# quick Postgres connectivity check")}`,
4068
+ ` codebuddy claude remove codebuddy-${namespace}`,
4069
+ ` codebuddy codex remove codebuddy-${namespace}`,
4070
+ ` codebuddy postgres down ${pc17.dim("# stop the DB (data preserved)")}`
4071
+ ].join("\n"),
4072
+ "Done"
4073
+ );
4074
+ } finally {
4075
+ process.off("SIGINT", sigintHandler);
4076
+ }
4077
+ }
4078
+
4079
+ // src/cli/commands/worklog.ts
4080
+ import { relative as relative6 } from "path";
4081
+ import pc18 from "picocolors";
4082
+ function registerWorklogCommands(program, runSafely2) {
4083
+ const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
4084
+ 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) => {
4085
+ await runSafely2(async () => {
4086
+ const entries = (await new WorklogStore().list()).slice(0, Number(opts.limit) || 20);
4087
+ if (opts.json) {
4088
+ console.log(JSON.stringify(entries, null, 2));
4089
+ return;
4090
+ }
4091
+ if (entries.length === 0) {
4092
+ console.log(pc18.dim("no worklog entries yet"));
4093
+ return;
4094
+ }
4095
+ for (const entry of entries) console.log(renderRow2(entry));
4096
+ });
4097
+ });
4098
+ worklog.command("show").argument("<id>").description("Show a single worklog entry.").action(async (id) => {
4099
+ await runSafely2(async () => {
4100
+ console.log(renderEntry(await new WorklogStore().read(id)));
4101
+ });
4102
+ });
4103
+ worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
4104
+ await runSafely2(async () => {
4105
+ const [entry] = await new WorklogStore().list();
4106
+ console.log(entry ? renderEntry(entry) : pc18.dim("no worklog entries yet"));
4107
+ });
4108
+ });
4109
+ }
4110
+ function renderRow2(entry) {
4111
+ const summary = entry.summary.replace(/\s+/g, " ");
4112
+ const short = summary.length <= 70 ? summary : `${summary.slice(0, 69)}\u2026`;
4113
+ return `${pc18.dim(entry.createdAt.slice(0, 16))} ${pc18.dim(entry.id)} ${short} ${pc18.dim(`(${entry.files.length} file(s))`)}`;
4114
+ }
4115
+ function renderEntry(entry) {
4116
+ const lines = [`${pc18.bold(entry.id)} ${pc18.dim(entry.createdAt)} ${pc18.dim(entry.namespace)}`];
4117
+ if (entry.sourceSessionId) lines.push(`${pc18.bold("session:")} ${entry.sourceSessionId}`);
4118
+ if (entry.sourcePlanId) lines.push(`${pc18.bold("plan:")} ${entry.sourcePlanId}`);
4119
+ if (entry.commitSha) lines.push(`${pc18.bold("commit:")} ${entry.commitSha}`);
4120
+ if (entry.files.length > 0) lines.push(`${pc18.bold("files:")} ${entry.files.join(", ")}`);
4121
+ if (entry.verification.length > 0) {
4122
+ lines.push(`${pc18.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
4123
+ }
4124
+ if (entry.savedMemory.length > 0) {
4125
+ lines.push(`${pc18.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
4126
+ }
4127
+ if (entry.queuedForReview.length > 0) {
4128
+ lines.push(`${pc18.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
4129
+ }
4130
+ lines.push("", entry.summary, "", pc18.dim(relative6(process.cwd(), entry.path)));
4131
+ return lines.join("\n");
4132
+ }
4133
+
4134
+ // src/cli/commands/zvec.ts
4135
+ import pc19 from "picocolors";
4136
+ function registerZvecCommands(program, runSafely2) {
4137
+ const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
4138
+ 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) => {
4139
+ await runSafely2(async () => {
4140
+ const report = await zvecStatus(process.cwd());
4141
+ if (opts.json) {
4142
+ console.log(JSON.stringify(report, null, 2));
4143
+ return;
4144
+ }
4145
+ const mark = (ok) => ok ? pc19.green("yes") : pc19.yellow("no");
4146
+ console.log(`enabled: ${mark(report.enabled)}`);
4147
+ console.log(`binary: ${report.binary}`);
4148
+ console.log(
4149
+ `version: ${report.version ?? pc19.yellow("not found")}${report.version && !report.supported ? pc19.yellow(" (unsupported; need 0.2.x)") : ""}`
4150
+ );
4151
+ console.log(`index ready: ${mark(report.indexReady)}`);
4152
+ console.log(
4153
+ `zg server: ${report.server} ${pc19.dim("(running = faster queries; optional)")}`
4154
+ );
4155
+ console.log(`status: ${report.reason}`);
4156
+ if (!report.version) console.log(pc19.dim("install: npm install -g @zvec/zvec-grep"));
4157
+ else if (report.supported && !report.indexReady)
4158
+ console.log(pc19.dim("next: codebuddy zvec index"));
4159
+ else if (report.indexReady && !report.enabled)
4160
+ console.log(pc19.dim("next: codebuddy zvec enable"));
4161
+ });
4162
+ });
4163
+ 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) => {
4164
+ await runSafely2(async () => {
4165
+ const code = await buildZvecIndex(process.cwd(), {
4166
+ model: opts.model,
4167
+ ...opts.rebuild ? { rebuild: true } : {}
4168
+ });
4169
+ if (code !== 0) throw new Error(`zg index exited with code ${code}`);
4170
+ console.log(
4171
+ pc19.dim("tip: add .zvec-grep/ to .gitignore; enable with `codebuddy zvec enable`")
4172
+ );
4173
+ });
4174
+ });
4175
+ zvec.command("enable").description("Use zvec-grep candidates in context_pack / context_before_edit.").action(async () => {
4176
+ await runSafely2(async () => {
4177
+ await setZvecEnabled(true);
4178
+ console.log(
4179
+ `${pc19.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
4180
+ );
4181
+ });
4182
+ });
4183
+ zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
4184
+ await runSafely2(async () => {
4185
+ await setZvecEnabled(false);
4186
+ console.log(`${pc19.yellow("disabled")} zvec-grep retrieval`);
4187
+ });
4188
+ });
4189
+ }
4190
+
4191
+ // src/cli/main.ts
4192
+ function createCli() {
4193
+ const program = new Command();
4194
+ program.name("codebuddy").description(
4195
+ "Local-first MCP memory and automatic project-context engine for multi-agent coding."
4196
+ ).version(VERSION);
4197
+ program.action(async () => {
4198
+ await runSafely(async () => {
4199
+ await runUseCommand();
4200
+ });
4201
+ });
4202
+ program.command("use").description(
4203
+ "One-command setup for the current project folder. Reuses saved HF token + DB URL, auto-starts Docker, runs migrations, and wires Claude Desktop."
4204
+ ).argument("[namespace]", "Override the namespace (defaults to current folder name).").option("--postgres-url <url>", "Postgres URL to use for this project.").option("--skip-claude", "Do not register with Claude Desktop.").option("--skip-codex", "Do not register with Codex.").option("--graphify", "Enable Graphify setup without prompting.").option("--no-graphify", "Skip Graphify setup without prompting.").option(
4205
+ "--docker",
4206
+ "Start CodeBuddy's bundled Postgres container when the default DB is unavailable."
4207
+ ).option("--no-docker", "Do not offer to start CodeBuddy's bundled Postgres container.").action(
4208
+ async (namespace, opts) => {
4209
+ await runSafely(async () => {
4210
+ await runUseCommand({
4211
+ ...namespace !== void 0 ? { namespace } : {},
4212
+ ...opts.postgresUrl !== void 0 ? { postgresUrl: opts.postgresUrl } : {},
4213
+ ...opts.skipClaude !== void 0 ? { skipClaude: opts.skipClaude } : {},
4214
+ ...opts.skipCodex !== void 0 ? { skipCodex: opts.skipCodex } : {},
4215
+ ...opts.graphify !== void 0 ? { graphify: opts.graphify } : {},
4216
+ ...opts.docker !== void 0 ? { useDocker: opts.docker } : {}
4217
+ });
4218
+ });
4219
+ }
4220
+ );
4221
+ program.command("init").description("Interactive setup: config file, Postgres, migrations, Claude Desktop.").option("--non-interactive", "Skip the wizard; just create .codebuddy/config.json.").option("--github-action", "Install the GitHub PR change-safety workflow.").action(async (opts) => {
4222
+ await runSafely(async () => {
4223
+ if (opts.nonInteractive || !process.stdin.isTTY) {
4224
+ const result = await initConfigFile();
4225
+ console.log(`${result.created ? "created" : "updated permissions"} ${result.path}`);
4226
+ if (opts.githubAction) {
4227
+ const workflow = await installGitHubWorkflow();
4228
+ console.log(`${workflow.created ? "installed" : "already present"} ${workflow.path}`);
4229
+ }
4230
+ return;
4231
+ }
4232
+ await runInitWizard();
4233
+ if (opts.githubAction) {
4234
+ const workflow = await installGitHubWorkflow();
4235
+ console.log(`${workflow.created ? "installed" : "already present"} ${workflow.path}`);
4236
+ }
4237
+ });
4238
+ });
4239
+ const graphify = program.command("graphify").description("Build the local Graphify repository graph.");
4240
+ graphify.command("index").argument("[projectRoot]", "Project root (defaults to the current directory).").option("--graph <path>", "Graph path relative to the project root.").description("Generate or incrementally update graphify-out/graph.json.").action(async (projectRoot, opts) => {
4241
+ await runSafely(async () => {
4242
+ const root = projectRoot ?? process.cwd();
4243
+ const result = await generateGraphifyGraph({
4244
+ repositoryRoot: root,
4245
+ ...opts.graph ? { graphPath: opts.graph } : {}
4246
+ });
4247
+ console.log(JSON.stringify(result, null, 2));
4248
+ });
4249
+ });
4250
+ const migrate = program.command("migrate").description("Apply pending database migrations.").action(async () => {
4251
+ await runSafely(async () => {
4252
+ const config = await loadRuntimeConfig();
4253
+ const client = createDatabaseClient({ postgresUrl: config.postgresUrl });
4254
+ try {
4255
+ await bootstrapDatabase(client.sql);
4256
+ await runMigrations(client);
4257
+ console.log("migrations applied");
4258
+ } finally {
4259
+ await client.close();
4260
+ }
4261
+ });
4262
+ });
4263
+ migrate.command("to-files").description("Preview or write existing facts and summaries as Markdown files.").option("--write", "Write files. Without this flag the command is a dry run.").action(async (opts) => {
4264
+ await withRuntime(async (runtime) => {
4265
+ const config = await loadRuntimeConfig();
4266
+ const result = await migrateMemoryToFiles({
4267
+ repository: runtime.repository,
4268
+ store: new MemoryFileStore(config.projectRoot ?? process.cwd()),
4269
+ namespace: config.namespace,
4270
+ write: opts.write ?? false
4271
+ });
4272
+ console.log(JSON.stringify(result, null, 2));
4273
+ });
4274
+ });
4275
+ program.command("reindex").description("Rebuild fact and summary records from Markdown files.").option("--full", "Scan every Markdown memory file.").action(async (_opts) => {
4276
+ await withRuntime(async (runtime) => {
4277
+ console.log(JSON.stringify(await runtime.memory.reindexMemory(), null, 2));
4278
+ });
4279
+ });
4280
+ const postgres = program.command("postgres").description("Manage the bundled Postgres container.");
4281
+ postgres.command("up").description("Start the bundled Postgres container (docker compose up -d).").action(async () => {
4282
+ await runSafely(async () => {
4283
+ const result = await postgresUp();
4284
+ process.stdout.write(result.stdout);
4285
+ if (result.code !== 0) {
4286
+ process.stderr.write(result.stderr);
4287
+ process.exitCode = result.code;
4288
+ }
4289
+ });
4290
+ });
4291
+ const db = program.command("db").description("Check database connectivity and setup health.");
4292
+ db.command("doctor").description("Check Postgres reachability, pgvector, and namespace assumptions.").action(async () => {
4293
+ await runSafely(async () => {
4294
+ const report = await runDoctor({ skipModelCheck: true });
4295
+ printDbDoctor(report);
4296
+ if (!report.db.ok || !report.pgvector.ok || !report.namespace.ok) process.exitCode = 1;
4297
+ });
4298
+ });
4299
+ db.command("test").description("Run a simple database connectivity test.").action(async () => {
4300
+ await runSafely(async () => {
4301
+ const report = await runDoctor({ skipModelCheck: true });
4302
+ if (!report.db.ok) {
4303
+ console.log("database test: fail");
4304
+ if (report.db.error) console.log(` ${report.db.error}`);
4305
+ process.exitCode = 1;
4306
+ return;
4307
+ }
4308
+ console.log("database test: ok");
4309
+ });
4310
+ });
4311
+ postgres.command("down").description("Stop the bundled Postgres container (data volume preserved).").action(async () => {
4312
+ await runSafely(async () => {
4313
+ const result = await postgresDown();
4314
+ process.stdout.write(result.stdout);
4315
+ if (result.code !== 0) {
4316
+ process.stderr.write(result.stderr);
4317
+ process.exitCode = result.code;
4318
+ }
4319
+ });
4320
+ });
4321
+ postgres.command("status").description("docker compose ps for the bundled Postgres container.").action(async () => {
4322
+ await runSafely(async () => {
4323
+ const result = await postgresStatus();
4324
+ process.stdout.write(result.stdout);
4325
+ if (result.code !== 0) {
4326
+ process.stderr.write(result.stderr);
4327
+ process.exitCode = result.code;
4328
+ }
4329
+ });
4330
+ });
4331
+ const claude = program.command("claude").description("Register CodeBuddy as an MCP server in Claude Desktop.");
4332
+ claude.command("install").option("--namespace <name>", "Namespace for this Claude Desktop entry.").option("--server-name <name>", "Override the server key. Defaults to codebuddy-<namespace>.").description("Add or update a codebuddy MCP entry in Claude Desktop's config.").action(async (opts) => {
4333
+ await runSafely(async () => {
4334
+ const config = await loadRuntimeConfig();
4335
+ const namespace = opts.namespace ?? config.namespace ?? "default";
4336
+ const install = await installClaudeEntry({
4337
+ namespace,
4338
+ projectRoot: process.cwd(),
4339
+ ...config.provider.apiKey ? { hfToken: config.provider.apiKey } : {},
4340
+ ...process.env.GROQ_API_KEY ? { groqApiKey: process.env.GROQ_API_KEY } : {},
4341
+ databaseUrl: config.postgresUrl,
4342
+ ...opts.serverName ? { serverName: opts.serverName } : {}
4343
+ });
4344
+ console.log(
4345
+ `${install.created ? "added" : "updated"} ${install.serverKey} in ${install.path}`
4346
+ );
4347
+ console.log("Restart Claude Desktop (\u2318Q) to activate.");
4348
+ });
4349
+ });
4350
+ claude.command("list").description("List codebuddy entries currently registered in Claude Desktop.").action(async () => {
4351
+ await runSafely(async () => {
4352
+ const result = await listClaudeEntries();
4353
+ if (result.entries.length === 0) {
4354
+ console.log(`No codebuddy entries in ${result.path}.`);
4355
+ return;
4356
+ }
4357
+ console.log(`Claude Desktop config: ${result.path}`);
4358
+ for (const entry of result.entries) {
4359
+ console.log(
4360
+ ` ${entry.key} namespace=${entry.namespace ?? "?"} db=${entry.databaseUrl ?? "?"}`
4361
+ );
4362
+ }
4363
+ });
4364
+ });
4365
+ claude.command("remove").argument("<serverKey>", "e.g. codebuddy-work").description("Remove a codebuddy entry from Claude Desktop's config.").action(async (serverKey) => {
4366
+ await runSafely(async () => {
4367
+ const result = await removeClaudeEntry(serverKey);
4368
+ if (result.removed) {
4369
+ console.log(`removed ${serverKey} from ${result.path}`);
4370
+ console.log("Restart Claude Desktop (\u2318Q) for the change to take effect.");
4371
+ } else {
4372
+ console.log(`no entry "${serverKey}" found in ${result.path}`);
4373
+ process.exitCode = 1;
4374
+ }
4375
+ });
4376
+ });
4377
+ const codex = program.command("codex").description("Register CodeBuddy as an MCP server in Codex.");
4378
+ codex.command("install").option("--namespace <name>", "Namespace for this Codex entry.").option("--server-name <name>", "Override the server key. Defaults to codebuddy-<namespace>.").description("Add or update a codebuddy MCP entry in ~/.codex/config.toml.").action(async (opts) => {
4379
+ await runSafely(async () => {
4380
+ const config = await loadRuntimeConfig();
4381
+ const namespace = opts.namespace ?? config.namespace ?? "default";
4382
+ const install = await installCodexEntry({
4383
+ namespace,
4384
+ projectRoot: process.cwd(),
4385
+ ...config.provider.apiKey ? { hfToken: config.provider.apiKey } : {},
4386
+ databaseUrl: config.postgresUrl,
4387
+ ...opts.serverName ? { serverName: opts.serverName } : {}
4388
+ });
4389
+ console.log(
4390
+ `${install.created ? "added" : "updated"} ${install.serverKey} in ${install.path}`
4391
+ );
4392
+ console.log("Restart Codex to activate.");
4393
+ });
4394
+ });
4395
+ codex.command("list").description("List codebuddy entries currently registered in Codex.").action(async () => {
4396
+ await runSafely(async () => {
4397
+ const result = await listCodexEntries();
4398
+ if (result.entries.length === 0) {
4399
+ console.log(`No codebuddy entries in ${result.path}.`);
4400
+ return;
4401
+ }
4402
+ console.log(`Codex config: ${result.path}`);
4403
+ for (const entry of result.entries) {
4404
+ console.log(
4405
+ ` ${entry.key} namespace=${entry.namespace ?? "?"} db=${entry.databaseUrl ?? "?"}`
4406
+ );
4407
+ }
4408
+ });
4409
+ });
4410
+ codex.command("remove").argument("<serverKey>", "e.g. codebuddy-work").description("Remove a codebuddy entry from Codex config.").action(async (serverKey) => {
4411
+ await runSafely(async () => {
4412
+ const result = await removeCodexEntry(serverKey);
4413
+ if (result.removed) {
4414
+ console.log(`removed ${serverKey} from ${result.path}`);
4415
+ console.log("Restart Codex for the change to take effect.");
4416
+ } else {
4417
+ console.log(`no entry "${serverKey}" found in ${result.path}`);
4418
+ process.exitCode = 1;
4419
+ }
4420
+ });
4421
+ });
4422
+ program.command("serve").description("Start the CodeBuddy MCP stdio server.").action(async () => runSafely(() => startMcpServer()));
4423
+ program.command("inspect").argument("[namespace]").description("Inspect namespaces or a namespace.").action(async (namespace) => {
4424
+ await withRuntime(async (runtime) => {
4425
+ console.log(JSON.stringify(await inspectNamespace(runtime.repository, namespace), null, 2));
4426
+ });
4427
+ });
4428
+ program.command("namespaces").description("List namespaces.").action(async () => {
4429
+ await withRuntime(async (runtime) => {
4430
+ console.log(
4431
+ JSON.stringify({ namespaces: await runtime.repository.listNamespaces() }, null, 2)
4432
+ );
4433
+ });
4434
+ });
4435
+ program.command("prune").requiredOption("--older-than <duration>", "Duration such as 30d, 12h, or 60m.").description("Delete memories older than a duration.").action(async (opts) => {
4436
+ await withRuntime(async (runtime) => {
4437
+ const durationMs = parseDuration(opts.olderThan);
4438
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
4439
+ throw new Error("--older-than must be a positive duration (e.g. 30d, 12h, 60m).");
4440
+ }
4441
+ const cutoff = new Date(Date.now() - durationMs);
4442
+ console.log(JSON.stringify(await runtime.repository.pruneBefore(cutoff), null, 2));
4443
+ });
4444
+ });
4445
+ program.command("export").argument("<namespace>").description("Export facts from a namespace as JSON.").action(async (namespace) => {
4446
+ await withRuntime(async (runtime) => {
4447
+ const row = await runtime.repository.getNamespaceByName(namespace);
4448
+ if (!row) throw new Error(`Namespace ${namespace} does not exist.`);
4449
+ const facts = await runtime.repository.listFacts({ namespaceId: row.id, limit: 1e4 });
4450
+ console.log(JSON.stringify({ namespace, facts }, null, 2));
4451
+ });
4452
+ });
4453
+ program.command("stats").description("Show CodeBuddy storage and usage stats.").action(async () => {
4454
+ await withRuntime(async (runtime) => {
4455
+ console.log(JSON.stringify(await runtime.repository.getStats(), null, 2));
4456
+ });
4457
+ });
4458
+ program.command("doctor").description("Check CodeBuddy environment health.").option("--skip-model-check", "Do not call Hugging Face models.").action(async (opts) => {
4459
+ await runSafely(async () => {
4460
+ const report = await runDoctor({ skipModelCheck: opts.skipModelCheck ?? true });
4461
+ printDoctor(report);
4462
+ if (!report.db.ok || !report.pgvector.ok || report.config.warning || !report.scaffold.policies.ok || !report.scaffold.codebuddyDir.ok || !report.namespace.ok) {
4463
+ process.exitCode = 1;
4464
+ }
4465
+ });
4466
+ });
4467
+ program.command("list-facts").option("--subject <subject>").option("--limit <limit>", "Maximum facts to print.", Number).option("--cursor <cursor>").description("List facts from the configured namespace.").action(async (opts) => {
4468
+ await withRuntime(async (runtime) => {
4469
+ console.log(
4470
+ JSON.stringify(await listFactsPage(runtime.memory, runtime.repository, opts), null, 2)
4471
+ );
4472
+ });
4473
+ });
4474
+ registerPlanCommands(program, runSafely);
4475
+ registerChangeCommand(program, runSafely);
4476
+ registerRiskCommands(program, runSafely);
4477
+ registerMapCommands(program, runSafely);
4478
+ registerContextCommands(program, runSafely);
4479
+ registerEvalCommand(program, runSafely);
4480
+ registerIndexCommands(program, runSafely);
4481
+ registerLearnCommand(program, runSafely);
4482
+ registerMemoryCommands(program, runSafely);
4483
+ registerWorklogCommands(program, runSafely);
4484
+ registerZvecCommands(program, runSafely);
4485
+ registerLlmCommands(program, runSafely);
4486
+ registerSuggestCommand(program, runSafely);
4487
+ registerRulesCommands(program, runSafely);
4488
+ registerHooksCommands(program, runSafely);
4489
+ registerClientsCommands(program, runSafely);
4490
+ registerGitHubCommands(program, runSafely);
4491
+ registerSymbolsCommand(program, runSafely);
4492
+ program.action(async () => {
4493
+ await runSafely(() => runUseCommand());
4494
+ });
4495
+ return program;
4496
+ }
4497
+ async function withRuntime(fn) {
4498
+ await runSafely(async () => {
4499
+ const runtime = await createRuntime(await loadRuntimeConfig());
4500
+ try {
4501
+ await fn(runtime);
4502
+ } finally {
4503
+ await runtime.close();
4504
+ }
4505
+ });
4506
+ }
4507
+ async function runSafely(fn) {
4508
+ try {
4509
+ await fn();
4510
+ } catch (error) {
4511
+ console.error(pc20.red(String(redactSecrets(error.message))));
4512
+ process.exitCode = 1;
4513
+ }
4514
+ }
4515
+ function parseDuration(value) {
4516
+ const match = /^(\d+)([dhm])$/.exec(value);
4517
+ if (!match) throw new Error("--older-than must look like 30d, 12h, or 60m.");
4518
+ const amount = Number(match[1]);
4519
+ const unit = match[2];
4520
+ if (unit === "d") return amount * 24 * 60 * 60 * 1e3;
4521
+ if (unit === "h") return amount * 60 * 60 * 1e3;
4522
+ return amount * 60 * 1e3;
4523
+ }
4524
+ function printDoctor(report) {
4525
+ const ok = pc20.green("ok");
4526
+ const bad = pc20.red("fail");
4527
+ const warn = pc20.yellow("warn");
4528
+ console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok : bad}`);
4529
+ if (report.scaffold.codebuddyDir.warning)
4530
+ console.log(` ${report.scaffold.codebuddyDir.warning}`);
4531
+ console.log(`policy scaffold: ${report.scaffold.policies.ok ? ok : bad}`);
4532
+ if (report.scaffold.policies.warning) console.log(` ${report.scaffold.policies.warning}`);
4533
+ console.log(`DB connectivity: ${report.db.ok ? ok : bad}`);
4534
+ if (report.db.error) console.log(` ${report.db.error}`);
4535
+ console.log(`pgvector: ${report.pgvector.ok ? ok : bad}`);
4536
+ if (report.pgvector.error) console.log(` ${report.pgvector.error}`);
4537
+ console.log(`namespace: ${report.namespace.ok ? ok : warn}`);
4538
+ console.log(
4539
+ ` runtime=${report.namespace.runtime ?? "unset"} default=${report.namespace.expectedDefault}`
4540
+ );
4541
+ if (report.namespace.warning) console.log(` ${report.namespace.warning}`);
4542
+ console.log(
4543
+ `vectors: ${report.vectors.count} index=${report.vectors.index ?? "missing"} tuning=${report.vectors.needsTuning ? "recommended" : "not-needed"}`
4544
+ );
4545
+ console.log(
4546
+ `recent calls: 60s=${report.usage.last60s} hour=${report.usage.lastHour} 24h=${report.usage.last24h}`
4547
+ );
4548
+ console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
4549
+ if (report.usage.warning80Percent)
4550
+ console.log(pc20.yellow("usage warning: daily cap is over 80% used"));
4551
+ console.log(
4552
+ `config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok : warn}`
4553
+ );
4554
+ if (report.config.warning) console.log(` ${report.config.warning}`);
4555
+ for (const model of report.models) {
4556
+ console.log(`model ${model.id}: ${model.status}${model.error ? ` (${model.error})` : ""}`);
4557
+ }
4558
+ }
4559
+ function printDbDoctor(report) {
4560
+ const ok = pc20.green("ok");
4561
+ const bad = pc20.red("fail");
4562
+ const warn = pc20.yellow("warn");
4563
+ console.log(`DB connectivity: ${report.db.ok ? ok : bad}`);
4564
+ if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
4565
+ if (report.db.error) console.log(` ${report.db.error}`);
4566
+ console.log(`pgvector: ${report.pgvector.ok ? ok : bad}`);
4567
+ if (report.pgvector.error) console.log(` ${report.pgvector.error}`);
4568
+ console.log(`namespace: ${report.namespace.ok ? ok : warn}`);
4569
+ console.log(` runtime=${report.namespace.runtime ?? "unset"}`);
4570
+ if (report.namespace.warning) console.log(` ${report.namespace.warning}`);
4571
+ }
4572
+ export {
4573
+ createCli
4574
+ };
4575
+ //# sourceMappingURL=main-ZZ3EQLYA.js.map