@orquentra/codebuddy 2.4.0 → 2.5.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 (61) hide show
  1. package/README.md +42 -1090
  2. package/dist/chunk-3XYQGFPB.js +91 -0
  3. package/dist/chunk-3XYQGFPB.js.map +1 -0
  4. package/dist/chunk-4BYGTEJT.js +103 -0
  5. package/dist/chunk-4BYGTEJT.js.map +1 -0
  6. package/dist/{chunk-ITGRANYW.js → chunk-6MKEIDX5.js} +2 -2
  7. package/dist/{chunk-K63EOA4B.js → chunk-776EQXDV.js} +25 -6
  8. package/dist/chunk-776EQXDV.js.map +1 -0
  9. package/dist/{chunk-5OWZVZSO.js → chunk-CQPCHMBS.js} +8 -165
  10. package/dist/chunk-CQPCHMBS.js.map +1 -0
  11. package/dist/chunk-JDZ3VK26.js +4006 -0
  12. package/dist/chunk-JDZ3VK26.js.map +1 -0
  13. package/dist/chunk-JMWJ7V6Y.js +99 -0
  14. package/dist/chunk-JMWJ7V6Y.js.map +1 -0
  15. package/dist/chunk-KYK6RPAJ.js +99 -0
  16. package/dist/chunk-KYK6RPAJ.js.map +1 -0
  17. package/dist/{chunk-IBMJYIEP.js → chunk-L5AYZC3F.js} +7 -7
  18. package/dist/{chunk-3ZELDCR3.js → chunk-PCE4STAY.js} +7 -7
  19. package/dist/{chunk-TI5TQD6D.js → chunk-QDBDYCM2.js} +2 -2
  20. package/dist/{chunk-DSN5CEHO.js → chunk-ROQTT5ZO.js} +3 -3
  21. package/dist/{chunk-IE2OUO54.js → chunk-UFZD3JAA.js} +3 -3
  22. package/dist/chunk-Z5QB367W.js +5028 -0
  23. package/dist/chunk-Z5QB367W.js.map +1 -0
  24. package/dist/cli/index.js +2 -2
  25. package/dist/cloud-GP6G3EAK.js +17 -0
  26. package/dist/{codebuddy-D2xJ6hFk.d.ts → codebuddy-DM-i0Fu5.d.ts} +49 -18
  27. package/dist/{config-S2TULOMK.js → config-KFEHLV6W.js} +3 -3
  28. package/dist/index.d.ts +23 -3
  29. package/dist/index.js +34 -25
  30. package/dist/langgraph/index.d.ts +1 -1
  31. package/dist/lean-command-DA4ZNKG3.js +15 -0
  32. package/dist/local-3NGNESNB.js +13 -0
  33. package/dist/{main-ZZ3EQLYA.js → main-BDVQAGAS.js} +1261 -242
  34. package/dist/main-BDVQAGAS.js.map +1 -0
  35. package/dist/{memory-NIBFAMY4.js → memory-4RGVLPKG.js} +6 -6
  36. package/dist/operations-EQGOFCBD.js +24 -0
  37. package/dist/operations-EQGOFCBD.js.map +1 -0
  38. package/dist/{rerank-Q7MIFNKA.js → rerank-3P3C2UHN.js} +4 -4
  39. package/dist/{summaries-TSE44KUQ.js → summaries-ZBV5IN7Y.js} +6 -6
  40. package/dist/summaries-ZBV5IN7Y.js.map +1 -0
  41. package/dist/{zvec-F27AR357.js → zvec-MAPSJQSR.js} +3 -3
  42. package/dist/zvec-MAPSJQSR.js.map +1 -0
  43. package/package.json +7 -3
  44. package/dist/chunk-5OWZVZSO.js.map +0 -1
  45. package/dist/chunk-K63EOA4B.js.map +0 -1
  46. package/dist/chunk-SYHB3AMM.js +0 -8580
  47. package/dist/chunk-SYHB3AMM.js.map +0 -1
  48. package/dist/lean-command-D6TZJEFX.js +0 -13
  49. package/dist/main-ZZ3EQLYA.js.map +0 -1
  50. /package/dist/{chunk-ITGRANYW.js.map → chunk-6MKEIDX5.js.map} +0 -0
  51. /package/dist/{chunk-IBMJYIEP.js.map → chunk-L5AYZC3F.js.map} +0 -0
  52. /package/dist/{chunk-3ZELDCR3.js.map → chunk-PCE4STAY.js.map} +0 -0
  53. /package/dist/{chunk-TI5TQD6D.js.map → chunk-QDBDYCM2.js.map} +0 -0
  54. /package/dist/{chunk-DSN5CEHO.js.map → chunk-ROQTT5ZO.js.map} +0 -0
  55. /package/dist/{chunk-IE2OUO54.js.map → chunk-UFZD3JAA.js.map} +0 -0
  56. /package/dist/{config-S2TULOMK.js.map → cloud-GP6G3EAK.js.map} +0 -0
  57. /package/dist/{lean-command-D6TZJEFX.js.map → config-KFEHLV6W.js.map} +0 -0
  58. /package/dist/{summaries-TSE44KUQ.js.map → lean-command-DA4ZNKG3.js.map} +0 -0
  59. /package/dist/{zvec-F27AR357.js.map → local-3NGNESNB.js.map} +0 -0
  60. /package/dist/{memory-NIBFAMY4.js.map → memory-4RGVLPKG.js.map} +0 -0
  61. /package/dist/{rerank-Q7MIFNKA.js.map → rerank-3P3C2UHN.js.map} +0 -0
@@ -1,7 +1,16 @@
1
+ import {
2
+ loadLlmSummaries,
3
+ summarizeRepository
4
+ } from "./chunk-PCE4STAY.js";
5
+ import {
6
+ chatJson,
7
+ coolingDown,
8
+ usageToday
9
+ } from "./chunk-UFZD3JAA.js";
1
10
  import {
2
11
  renderLeanBlock,
3
12
  runLeanHookCommand
4
- } from "./chunk-DSN5CEHO.js";
13
+ } from "./chunk-ROQTT5ZO.js";
5
14
  import {
6
15
  runPreToolHookCommand
7
16
  } from "./chunk-WHZSVY2L.js";
@@ -11,7 +20,6 @@ import {
11
20
  writeStdout
12
21
  } from "./chunk-PYLH4MH5.js";
13
22
  import {
14
- MemoryFileStore,
15
23
  PlanFileStore,
16
24
  PlanLifecycle,
17
25
  ReviewStore,
@@ -19,43 +27,30 @@ import {
19
27
  WorklogStore,
20
28
  approveReviewItem,
21
29
  assessRisk,
22
- bootstrapDatabase,
23
30
  buildBeforeEditContext,
24
31
  buildBootstrapContext,
25
- createDatabaseClient,
26
- createRuntime,
27
32
  generateSuggestions,
28
- inspectNamespace,
29
- listFactsPage,
30
33
  migrateMemoryToFiles,
31
- pingDatabase,
34
+ openHookSemantics,
35
+ parseTraceLines,
36
+ readTraces,
32
37
  registerCodeBuddyTools,
33
38
  renderContextBlock,
34
39
  resolveRiskPaths,
35
40
  runCaptureHook,
36
41
  runContextHook,
37
- runDoctor,
38
- runMigrations,
39
42
  runResetHook,
40
43
  runStageHook,
44
+ runSyncHook,
41
45
  startMcpServer,
46
+ tracePath,
42
47
  watchRepository
43
- } from "./chunk-SYHB3AMM.js";
48
+ } from "./chunk-Z5QB367W.js";
49
+ import "./chunk-IHQM2ZDV.js";
44
50
  import {
45
51
  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";
52
+ estimateTokens
53
+ } from "./chunk-CQPCHMBS.js";
59
54
  import {
60
55
  buildArchitectureMap,
61
56
  generateGraphifyGraph,
@@ -65,23 +60,49 @@ import {
65
60
  neighbours,
66
61
  queryMap,
67
62
  setupGraphify
68
- } from "./chunk-IBMJYIEP.js";
63
+ } from "./chunk-L5AYZC3F.js";
69
64
  import {
70
65
  extractSymbolTable,
71
66
  supportsSymbols
72
67
  } from "./chunk-PQK3GKZH.js";
73
- import "./chunk-IHQM2ZDV.js";
74
68
  import {
75
- chatJson,
76
- coolingDown,
77
- usageToday
78
- } from "./chunk-IE2OUO54.js";
69
+ MemoryFileStore,
70
+ bootstrapDatabase,
71
+ buildProviderFromConfig,
72
+ createDatabaseClient,
73
+ createRuntime,
74
+ embeddingSpaceKey,
75
+ inspectNamespace,
76
+ listFactsPage,
77
+ pingDatabase,
78
+ runDoctor,
79
+ runMigrations
80
+ } from "./chunk-JDZ3VK26.js";
79
81
  import {
80
82
  redactPersisted
81
83
  } from "./chunk-GDJH7XJC.js";
84
+ import {
85
+ computeFactHash,
86
+ generateEntityId
87
+ } from "./chunk-4BYGTEJT.js";
88
+ import "./chunk-KYK6RPAJ.js";
89
+ import {
90
+ modelCacheDir
91
+ } from "./chunk-3XYQGFPB.js";
92
+ import {
93
+ DEFAULT_CLOUD_EMBEDDING_MODELS,
94
+ DEFAULT_RERANK_MODELS,
95
+ LOCAL_EMBEDDING_MODEL,
96
+ getModelDefinition
97
+ } from "./chunk-JMWJ7V6Y.js";
98
+ import {
99
+ DEFAULT_ZVEC_MODEL,
100
+ buildZvecIndex,
101
+ zvecStatus
102
+ } from "./chunk-QDBDYCM2.js";
82
103
  import {
83
104
  resolveLlmConfig
84
- } from "./chunk-ITGRANYW.js";
105
+ } from "./chunk-6MKEIDX5.js";
85
106
  import {
86
107
  PLAN_POLICIES,
87
108
  configPath,
@@ -92,17 +113,18 @@ import {
92
113
  readConfigFile,
93
114
  redactSecrets,
94
115
  resolveNamespaceFrom,
116
+ setConfigBlock,
95
117
  setLlmEnabled,
96
118
  setPlanPolicy,
97
119
  setZvecEnabled,
98
120
  writeAtomic,
99
121
  writeConfigFile
100
- } from "./chunk-K63EOA4B.js";
122
+ } from "./chunk-776EQXDV.js";
101
123
  import "./chunk-PZ5AY32C.js";
102
124
 
103
125
  // src/cli/main.ts
104
126
  import { Command } from "commander";
105
- import pc20 from "picocolors";
127
+ import pc22 from "picocolors";
106
128
 
107
129
  // src/integrations/github.ts
108
130
  import { constants } from "fs";
@@ -2432,14 +2454,21 @@ async function runBeforeEdit(taskParts, opts) {
2432
2454
  resolveTokenBudget()
2433
2455
  ]);
2434
2456
  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
- });
2457
+ const semantics = task ? await openHookSemantics(process.cwd()) : null;
2458
+ try {
2459
+ return await buildBeforeEditContext({
2460
+ namespace,
2461
+ ...task ? { task } : {},
2462
+ ...semantics?.semanticRanker ? { semanticRanker: semantics.semanticRanker } : {},
2463
+ ...semantics?.reranker ? { reranker: semantics.reranker } : {},
2464
+ ...opts.paths ? { paths: parsePaths2(opts.paths) } : {},
2465
+ ...opts.plan ? { planId: opts.plan } : {},
2466
+ ...opts.git !== void 0 ? { useGit: opts.git } : {},
2467
+ ...tokenBudget !== void 0 ? { tokenBudget } : {}
2468
+ });
2469
+ } finally {
2470
+ await semantics?.close();
2471
+ }
2443
2472
  }
2444
2473
  function printBootstrap(ctx) {
2445
2474
  console.log(`${pc3.bold("project")} ${ctx.project.namespace} ${pc3.dim(ctx.project.root)}`);
@@ -2549,9 +2578,479 @@ function scoreColor(score) {
2549
2578
  return pc3.green(String(score));
2550
2579
  }
2551
2580
 
2552
- // src/cli/commands/eval.ts
2581
+ // src/cli/commands/dashboard.ts
2582
+ import { spawn as spawn2 } from "child_process";
2553
2583
  import pc4 from "picocolors";
2554
2584
 
2585
+ // src/dashboard/server.ts
2586
+ import { unwatchFile, watchFile } from "fs";
2587
+ import { open, stat } from "fs/promises";
2588
+ import { createServer } from "http";
2589
+ import { basename } from "path";
2590
+
2591
+ // src/dashboard/page.ts
2592
+ var DASHBOARD_HTML = `<!doctype html>
2593
+ <html lang="en">
2594
+ <head>
2595
+ <meta charset="utf-8">
2596
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2597
+ <title>CodeBuddy Traces</title>
2598
+ <style>
2599
+ :root {
2600
+ --bg: #f3f5f7; --panel: #ffffff; --fg: #141a21; --muted: #5d6874; --line: #dde3e8;
2601
+ --recall: #0a8f80; --tool: #c2410c; --recall-soft: #e0f2ef; --tool-soft: #fbeae2;
2602
+ --bad: #b42318; --bad-soft: #fdecea; --capture: #4b5563;
2603
+ --sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
2604
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
2605
+ }
2606
+ @media (prefers-color-scheme: dark) { :root {
2607
+ --bg: #0e1116; --panel: #161b22; --fg: #e6eaee; --muted: #94a0ad; --line: #28303a;
2608
+ --recall: #0d9488; --tool: #ea580c; --recall-soft: #10302c; --tool-soft: #3a1e12;
2609
+ --bad: #f97066; --bad-soft: #3b1715; --capture: #9aa4ae; color-scheme: dark } }
2610
+ * { box-sizing: border-box; }
2611
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 var(--sans); }
2612
+ .wrap { max-width: 1280px; margin: 0 auto; padding: 20px 20px 48px; display: grid; gap: 16px; }
2613
+ header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
2614
+ h1 { font-size: 18px; margin: 0; letter-spacing: -.01em; }
2615
+ h1 small { font: 500 13px var(--mono); color: var(--muted); margin-left: 8px; }
2616
+ h2 { font-size: 13px; margin: 0 0 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
2617
+ .live { display: inline-flex; align-items: center; gap: 6px; font: 12px var(--mono); color: var(--muted); }
2618
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
2619
+ .live.on .dot { background: var(--recall); box-shadow: 0 0 0 3px var(--recall-soft); }
2620
+ .spacer { flex: 1; }
2621
+ .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
2622
+ .seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 12px; cursor: pointer; }
2623
+ .seg button + button { border-left: 1px solid var(--line); }
2624
+ .seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
2625
+ .seg button:focus-visible { outline: 2px solid var(--recall); outline-offset: -2px; }
2626
+ .wrap > * { min-width: 0; }
2627
+ .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
2628
+ .kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
2629
+ .kpi .label { font-size: 12px; color: var(--muted); }
2630
+ .kpi .value { font: 600 24px/1.2 var(--mono); font-variant-numeric: tabular-nums; margin-top: 4px; }
2631
+ .kpi .sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
2632
+ .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; }
2633
+ .cols { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 16px; }
2634
+ @media (max-width: 900px) { .cols { grid-template-columns: 1fr; } }
2635
+ .legend { display: flex; gap: 16px; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
2636
+ .sw { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; }
2637
+ svg text { fill: var(--muted); font: 11px var(--mono); }
2638
+ .scroll { overflow-x: auto; }
2639
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
2640
+ th { text-align: left; font: 600 11px var(--sans); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
2641
+ td { padding: 7px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
2642
+ tr:last-child td { border-bottom: 0; }
2643
+ td.n { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
2644
+ th.n { text-align: right; }
2645
+ .chip { display: inline-block; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; }
2646
+ .chip.recall { background: var(--recall-soft); color: var(--recall); }
2647
+ .chip.tool { background: var(--tool-soft); color: var(--tool); }
2648
+ .chip.capture, .chip.sync, .chip.kw { background: var(--line); color: var(--capture); }
2649
+ .chip.err { background: var(--bad-soft); color: var(--bad); }
2650
+ .feed { display: grid; gap: 10px; max-height: 520px; overflow: auto; }
2651
+ .prompt { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
2652
+ .prompt .q { font-weight: 500; overflow-wrap: anywhere; }
2653
+ .prompt .meta { font: 12px var(--mono); color: var(--muted); margin-top: 4px; display: flex; flex-wrap: wrap; gap: 4px 12px; overflow-wrap: anywhere; }
2654
+ .prompt ul { margin: 6px 0 0; padding-left: 18px; font-size: 13px; }
2655
+ .prompt .none { font-size: 13px; color: var(--muted); margin-top: 6px; }
2656
+ .mono { font-family: var(--mono); }
2657
+ .muted { color: var(--muted); }
2658
+ .empty { padding: 40px 16px; text-align: center; color: var(--muted); }
2659
+ .empty code { font-family: var(--mono); background: var(--line); padding: 2px 6px; border-radius: 4px; color: var(--fg); }
2660
+ .log { max-height: 420px; overflow: auto; }
2661
+ @media (prefers-reduced-motion: no-preference) { tr.fresh td { animation: flash 1.2s ease-out; } }
2662
+ @keyframes flash { from { background: var(--recall-soft); } to { background: transparent; } }
2663
+ </style>
2664
+ </head>
2665
+ <body>
2666
+ <div class="wrap">
2667
+ <header>
2668
+ <h1>CodeBuddy traces<small id="project"></small></h1>
2669
+ <span class="live" id="live"><span class="dot"></span><span id="liveText">connecting</span></span>
2670
+ <span class="spacer"></span>
2671
+ <div class="seg" role="group" aria-label="Time range" id="range">
2672
+ <button data-range="3600000">1 hour</button>
2673
+ <button data-range="86400000" aria-pressed="true">24 hours</button>
2674
+ <button data-range="604800000">7 days</button>
2675
+ <button data-range="0">All</button>
2676
+ </div>
2677
+ </header>
2678
+
2679
+ <section class="kpis" id="kpis" aria-label="Summary"></section>
2680
+
2681
+ <section class="panel">
2682
+ <h2>Tokens delivered to the agent</h2>
2683
+ <div class="legend"><span><span class="sw" style="background:var(--recall)"></span>prompt recall (hook)</span><span><span class="sw" style="background:var(--tool)"></span>MCP tool results</span></div>
2684
+ <div class="scroll" id="chart"></div>
2685
+ </section>
2686
+
2687
+ <div class="cols">
2688
+ <section class="panel">
2689
+ <h2>Recent prompts and what was recalled</h2>
2690
+ <div class="feed" id="feed"></div>
2691
+ </section>
2692
+ <section class="panel">
2693
+ <h2>MCP tools</h2>
2694
+ <div class="scroll" id="tools"></div>
2695
+ </section>
2696
+ </div>
2697
+
2698
+ <section class="panel">
2699
+ <h2>Activity</h2>
2700
+ <div class="log scroll" id="log"></div>
2701
+ </section>
2702
+ <p class="muted" style="margin:0;font-size:12px;overflow-wrap:anywhere">Token counts are cl100k estimates of the text exchanged. Traces stay on this machine in <span class="mono" id="path"></span>. Turn them off with <span class="mono">CODEBUDDY_TRACE=0</span>.</p>
2703
+ </div>
2704
+ <script>
2705
+ (function () {
2706
+ var events = [];
2707
+ var range = 86400000;
2708
+ var fresh = new Set();
2709
+ var pending = false;
2710
+
2711
+ function esc(value) {
2712
+ return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
2713
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
2714
+ });
2715
+ }
2716
+ function num(n) { return Math.round(n).toLocaleString(); }
2717
+ function compact(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e4 ? (n / 1e3).toFixed(1) + "k" : num(n); }
2718
+ function pct(sorted, p) { if (!sorted.length) return 0; return sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))]; }
2719
+ function ms(n) { return n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
2720
+ function ago(ts) {
2721
+ var s = Math.max(0, (Date.now() - Date.parse(ts)) / 1000);
2722
+ return s < 60 ? Math.floor(s) + "s ago" : s < 3600 ? Math.floor(s / 60) + "m ago" : s < 86400 ? Math.floor(s / 3600) + "h ago" : Math.floor(s / 86400) + "d ago";
2723
+ }
2724
+ function visible() {
2725
+ if (!range) return events;
2726
+ var since = Date.now() - range;
2727
+ return events.filter(function (e) { return Date.parse(e.ts) >= since; });
2728
+ }
2729
+
2730
+ function kpis(list) {
2731
+ var recalls = list.filter(function (e) { return e.kind === "recall"; });
2732
+ var tools = list.filter(function (e) { return e.kind === "tool"; });
2733
+ var delivered = list.reduce(function (s, e) { return s + (e.kind === "recall" || e.kind === "tool" ? e.tokensOut || 0 : 0); }, 0);
2734
+ var withFacts = recalls.filter(function (e) { return e.detail && e.detail.facts && e.detail.facts.length; });
2735
+ var factCount = withFacts.reduce(function (s, e) { return s + e.detail.facts.length; }, 0);
2736
+ var dense = recalls.reduce(function (s, e) { return s + ((e.detail && e.detail.dense) || 0); }, 0);
2737
+ var lat = recalls.map(function (e) { return e.ms; }).sort(function (a, b) { return a - b; });
2738
+ var errors = list.filter(function (e) { return !e.ok; }).length;
2739
+ var captures = list.filter(function (e) { return e.kind === "capture"; });
2740
+ var saved = captures.reduce(function (s, e) { return s + ((e.detail && e.detail.saved) || 0); }, 0);
2741
+ var tiles = [
2742
+ ["Prompts recalled", num(recalls.length), recalls.length ? Math.round(100 * withFacts.length / recalls.length) + "% got facts" : "no prompts yet"],
2743
+ ["Facts injected", num(factCount), factCount ? Math.round(100 * dense / factCount) + "% matched by meaning" : "-"],
2744
+ ["Tokens delivered", compact(delivered), recalls.length + tools.length ? "\u2248 " + num(delivered / (recalls.length + tools.length)) + " per event" : "-"],
2745
+ ["MCP tool calls", num(tools.length), tools.length ? new Set(tools.map(function (e) { return e.name; })).size + " distinct tools" : "-"],
2746
+ ["Recall latency", lat.length ? ms(pct(lat, 0.5)) : "-", lat.length ? "p95 " + ms(pct(lat, 0.95)) : "-"],
2747
+ ["Memories saved", num(saved), captures.length + " capture" + (captures.length === 1 ? "" : "s")],
2748
+ ["Errors", num(errors), errors ? "see Activity" : "all good"],
2749
+ ];
2750
+ document.getElementById("kpis").innerHTML = tiles.map(function (t) {
2751
+ return '<div class="kpi"><div class="label">' + esc(t[0]) + '</div><div class="value">' + esc(t[1]) + '</div><div class="sub">' + esc(t[2]) + "</div></div>";
2752
+ }).join("");
2753
+ }
2754
+
2755
+ function chart(list) {
2756
+ var el = document.getElementById("chart");
2757
+ var points = list.filter(function (e) { return (e.kind === "recall" || e.kind === "tool") && e.tokensOut; });
2758
+ if (!points.length) { el.innerHTML = '<div class="empty">No tokens delivered in this range yet.</div>'; return; }
2759
+ var end = Date.now();
2760
+ // Zoom to the data: a few minutes of traces should not be one sliver of a 24-hour axis.
2761
+ var first = Date.parse(points[0].ts);
2762
+ var start = Math.max(range ? end - range : first, first - 60000);
2763
+ var buckets = 48, size = Math.max(10000, (end - start) / buckets);
2764
+ buckets = Math.ceil((end - start) / size);
2765
+ var rows = [];
2766
+ for (var i = 0; i < buckets; i++) rows.push({ recall: 0, tool: 0, n: 0 });
2767
+ points.forEach(function (e) {
2768
+ var i = Math.min(buckets - 1, Math.max(0, Math.floor((Date.parse(e.ts) - start) / size)));
2769
+ rows[i][e.kind] += e.tokensOut; rows[i].n += 1;
2770
+ });
2771
+ var max = Math.max.apply(null, rows.map(function (r) { return r.recall + r.tool; })) || 1;
2772
+ var W = 960, H = 200, L = 52, B = 22, T = 8, plotW = W - L - 8, plotH = H - B - T;
2773
+ var bw = plotW / buckets;
2774
+ var step = Math.pow(10, Math.floor(Math.log10(max)));
2775
+ var tick = max / step > 5 ? step * 2 : step;
2776
+ var svg = '<svg viewBox="0 0 ' + W + " " + H + '" style="width:100%;min-width:560px;height:auto" role="img" aria-label="Tokens delivered over time">';
2777
+ for (var v = 0; v <= max; v += tick) {
2778
+ var y = T + plotH - (v / max) * plotH;
2779
+ svg += '<line x1="' + L + '" x2="' + (W - 8) + '" y1="' + y + '" y2="' + y + '" stroke="var(--line)"/><text x="' + (L - 6) + '" y="' + (y + 4) + '" text-anchor="end">' + compact(v) + "</text>";
2780
+ }
2781
+ rows.forEach(function (r, i) {
2782
+ var x = L + i * bw + 1, w = Math.max(1, bw - 2);
2783
+ var hr = (r.recall / max) * plotH, ht = (r.tool / max) * plotH;
2784
+ var when = new Date(start + i * size);
2785
+ var label = when.toLocaleString() + ": " + num(r.recall) + " recall + " + num(r.tool) + " tool tokens (" + r.n + " events)";
2786
+ if (r.recall) svg += '<rect x="' + x + '" y="' + (T + plotH - hr) + '" width="' + w + '" height="' + hr + '" rx="2" fill="var(--recall)"><title>' + esc(label) + "</title></rect>";
2787
+ if (r.tool) svg += '<rect x="' + x + '" y="' + (T + plotH - hr - ht - (r.recall ? 2 : 0)) + '" width="' + w + '" height="' + ht + '" rx="2" fill="var(--tool)"><title>' + esc(label) + "</title></rect>";
2788
+ });
2789
+ [0, Math.floor(buckets / 2), buckets - 1].forEach(function (i) {
2790
+ var d = new Date(start + i * size);
2791
+ var text = end - start <= 86400000 ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString([], { month: "short", day: "numeric" });
2792
+ svg += '<text x="' + (L + i * bw + bw / 2) + '" y="' + (H - 6) + '" text-anchor="middle">' + esc(text) + "</text>";
2793
+ });
2794
+ el.innerHTML = svg + "</svg>";
2795
+ }
2796
+
2797
+ function feed(list) {
2798
+ var recalls = list.filter(function (e) { return e.kind === "recall"; }).slice(-30).reverse();
2799
+ var el = document.getElementById("feed");
2800
+ if (!recalls.length) {
2801
+ el.innerHTML = '<div class="empty">No prompts yet. Run <code>codebuddy hooks install</code>, restart your agent, and send a prompt.</div>';
2802
+ return;
2803
+ }
2804
+ el.innerHTML = recalls.map(function (e) {
2805
+ var d = e.detail || {};
2806
+ var facts = d.facts || [];
2807
+ return '<div class="prompt"><div class="q">' + (esc(d.prompt) || '<span class="muted">(no prompt text)</span>') + '</div>' +
2808
+ '<div class="meta"><span>' + ago(e.ts) + "</span><span>" + ms(e.ms) + "</span><span>\u2248" + num(e.tokensOut || 0) + " tokens injected</span>" +
2809
+ (d.dense ? '<span class="chip recall">' + d.dense + " by meaning</span>" : "") +
2810
+ (d.lexical ? '<span class="chip kw">' + d.lexical + " by keyword</span>" : "") +
2811
+ (e.ok ? "" : '<span class="chip err">' + esc(d.dropped || "dropped") + "</span>") + "</div>" +
2812
+ (facts.length ? "<ul>" + facts.map(function (f) { return "<li>" + esc(f) + "</li>"; }).join("") + "</ul>" : '<div class="none">No new facts: nothing matched, or already sent this session.</div>') +
2813
+ (d.files && d.files.length ? '<div class="meta">files: ' + d.files.map(esc).join(", ") + "</div>" : "") +
2814
+ "</div>";
2815
+ }).join("");
2816
+ }
2817
+
2818
+ function tools(list) {
2819
+ var by = {};
2820
+ list.filter(function (e) { return e.kind === "tool"; }).forEach(function (e) {
2821
+ var t = by[e.name] || (by[e.name] = { calls: 0, errors: 0, lat: [], out: 0 });
2822
+ t.calls++; if (!e.ok) t.errors++; t.lat.push(e.ms); t.out += e.tokensOut || 0;
2823
+ });
2824
+ var names = Object.keys(by).sort(function (a, b) { return by[b].calls - by[a].calls; });
2825
+ var el = document.getElementById("tools");
2826
+ if (!names.length) { el.innerHTML = '<div class="empty">No MCP tool calls in this range.</div>'; return; }
2827
+ el.innerHTML = '<table><thead><tr><th>Tool</th><th class="n">Calls</th><th class="n">p50</th><th class="n">p95</th><th class="n">Tokens out</th><th class="n">Errors</th></tr></thead><tbody>' +
2828
+ names.map(function (n) {
2829
+ var t = by[n], lat = t.lat.sort(function (a, b) { return a - b; });
2830
+ return '<tr><td class="mono">' + esc(n) + '</td><td class="n">' + t.calls + '</td><td class="n">' + ms(pct(lat, 0.5)) + '</td><td class="n">' + ms(pct(lat, 0.95)) + '</td><td class="n">' + compact(t.out) + '</td><td class="n">' + (t.errors ? '<span class="chip err">' + t.errors + "</span>" : "0") + "</td></tr>";
2831
+ }).join("") + "</tbody></table>";
2832
+ }
2833
+
2834
+ function summary(e) {
2835
+ var d = e.detail || {};
2836
+ if (e.kind === "recall") return (d.facts ? d.facts.length : 0) + " fact(s), " + (d.files ? d.files.length : 0) + " file(s)";
2837
+ if (e.kind === "capture") return (d.saved || 0) + " saved, " + (d.queued || 0) + " queued, " + (d.changedFiles || 0) + " changed file(s)";
2838
+ if (e.kind === "sync") return (d.imported || 0) + " new, " + (d.updated || 0) + " updated" + (d.reembedding ? ", " + d.reembedding + " re-embedding" : "");
2839
+ return e.session ? "session " + e.session.slice(0, 12) : "";
2840
+ }
2841
+
2842
+ function log(list) {
2843
+ var rows = list.slice(-200).reverse();
2844
+ var el = document.getElementById("log");
2845
+ if (!rows.length) { el.innerHTML = '<div class="empty">Nothing recorded in this range.</div>'; return; }
2846
+ el.innerHTML = '<table><thead><tr><th>When</th><th>Kind</th><th>Name</th><th>Details</th><th class="n">Time</th><th class="n">In</th><th class="n">Out</th></tr></thead><tbody>' +
2847
+ rows.map(function (e) {
2848
+ var key = e.ts + e.name;
2849
+ return '<tr class="' + (fresh.has(key) ? "fresh" : "") + '"><td class="mono muted" title="' + esc(e.ts) + '">' + ago(e.ts) + '</td><td><span class="chip ' + (e.ok ? esc(e.kind) : "err") + '">' + esc(e.ok ? e.kind : e.kind + " failed") + '</span></td><td class="mono">' + esc(e.name) + "</td><td>" + esc(summary(e)) + '</td><td class="n">' + ms(e.ms) + '</td><td class="n">' + num(e.tokensIn || 0) + '</td><td class="n">' + num(e.tokensOut || 0) + "</td></tr>";
2850
+ }).join("") + "</tbody></table>";
2851
+ fresh.clear();
2852
+ }
2853
+
2854
+ function render() {
2855
+ pending = false;
2856
+ var list = visible();
2857
+ kpis(list); chart(list); feed(list); tools(list); log(list);
2858
+ }
2859
+ function schedule() { if (!pending) { pending = true; requestAnimationFrame(render); } }
2860
+
2861
+ function load() {
2862
+ return fetch("/api/traces?limit=10000").then(function (r) { return r.json(); }).then(function (data) {
2863
+ events = data.events || [];
2864
+ document.getElementById("project").textContent = data.project || "";
2865
+ document.getElementById("path").textContent = data.path || "";
2866
+ document.title = "CodeBuddy Traces \xB7 " + (data.project || "");
2867
+ schedule();
2868
+ });
2869
+ }
2870
+
2871
+ document.getElementById("range").addEventListener("click", function (event) {
2872
+ var button = event.target.closest("button");
2873
+ if (!button) return;
2874
+ range = Number(button.dataset.range);
2875
+ this.querySelectorAll("button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
2876
+ schedule();
2877
+ });
2878
+
2879
+ function connect() {
2880
+ var live = document.getElementById("live"), text = document.getElementById("liveText");
2881
+ var source = new EventSource("/events");
2882
+ source.onopen = function () { live.classList.add("on"); text.textContent = "live"; };
2883
+ source.onerror = function () { live.classList.remove("on"); text.textContent = "reconnecting"; };
2884
+ source.addEventListener("trace", function (message) {
2885
+ var e = JSON.parse(message.data);
2886
+ events.push(e); fresh.add(e.ts + e.name);
2887
+ if (events.length > 10000) events = events.slice(-10000);
2888
+ schedule();
2889
+ });
2890
+ source.addEventListener("reset", function () { load(); });
2891
+ }
2892
+
2893
+ load().then(connect);
2894
+ // Relative times ("2m ago") keep moving even when nothing new arrives.
2895
+ setInterval(schedule, 30000);
2896
+ })();
2897
+ </script>
2898
+ </body>
2899
+ </html>`;
2900
+
2901
+ // src/dashboard/server.ts
2902
+ async function startDashboard(options) {
2903
+ const { repositoryRoot } = options;
2904
+ const path = tracePath(repositoryRoot);
2905
+ const clients = /* @__PURE__ */ new Set();
2906
+ let offset = (await stat(path).catch(() => null))?.size ?? 0;
2907
+ let reading = false;
2908
+ const onChange = async () => {
2909
+ if (reading) return;
2910
+ reading = true;
2911
+ try {
2912
+ const size = (await stat(path).catch(() => null))?.size ?? 0;
2913
+ if (size < offset) {
2914
+ offset = 0;
2915
+ broadcast("reset", "{}");
2916
+ }
2917
+ if (size > offset) {
2918
+ const handle = await open(path, "r");
2919
+ try {
2920
+ const buffer = Buffer.alloc(size - offset);
2921
+ await handle.read(buffer, 0, buffer.length, offset);
2922
+ const text3 = buffer.toString("utf8");
2923
+ const complete = text3.slice(0, text3.lastIndexOf("\n") + 1);
2924
+ offset += Buffer.byteLength(complete);
2925
+ for (const event of parseTraceLines(complete)) broadcast("trace", JSON.stringify(event));
2926
+ } finally {
2927
+ await handle.close();
2928
+ }
2929
+ }
2930
+ } finally {
2931
+ reading = false;
2932
+ }
2933
+ };
2934
+ const broadcast = (event, data) => {
2935
+ for (const client of clients) client.write(`event: ${event}
2936
+ data: ${data}
2937
+
2938
+ `);
2939
+ };
2940
+ watchFile(path, { interval: options.pollMs ?? 1e3 }, () => void onChange());
2941
+ const server = createServer(async (request, response) => {
2942
+ const url = new URL(request.url ?? "/", "http://localhost");
2943
+ if (request.method !== "GET") {
2944
+ response.writeHead(405).end();
2945
+ return;
2946
+ }
2947
+ if (url.pathname === "/") {
2948
+ response.writeHead(200, {
2949
+ "content-type": "text/html; charset=utf-8",
2950
+ "cache-control": "no-store"
2951
+ });
2952
+ response.end(DASHBOARD_HTML);
2953
+ return;
2954
+ }
2955
+ if (url.pathname === "/api/traces") {
2956
+ const limit = Math.min(1e4, Math.max(1, Number(url.searchParams.get("limit")) || 2e3));
2957
+ response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
2958
+ response.end(
2959
+ JSON.stringify({
2960
+ project: basename(repositoryRoot),
2961
+ path,
2962
+ events: await readTraces(repositoryRoot, limit)
2963
+ })
2964
+ );
2965
+ return;
2966
+ }
2967
+ if (url.pathname === "/events") {
2968
+ response.writeHead(200, {
2969
+ "content-type": "text/event-stream",
2970
+ "cache-control": "no-store",
2971
+ connection: "keep-alive"
2972
+ });
2973
+ response.write(": connected\n\n");
2974
+ clients.add(response);
2975
+ request.on("close", () => clients.delete(response));
2976
+ return;
2977
+ }
2978
+ response.writeHead(404, { "content-type": "text/plain" }).end("Not found");
2979
+ });
2980
+ const port = await listen(server, options.port ?? 4777);
2981
+ const heartbeat = setInterval(() => {
2982
+ for (const client of clients) client.write(": ping\n\n");
2983
+ }, 25e3);
2984
+ heartbeat.unref();
2985
+ return {
2986
+ url: `http://127.0.0.1:${port}/`,
2987
+ close: () => new Promise((resolve14) => {
2988
+ clearInterval(heartbeat);
2989
+ unwatchFile(path);
2990
+ for (const client of clients) client.end();
2991
+ server.closeAllConnections();
2992
+ server.close(() => resolve14());
2993
+ })
2994
+ };
2995
+ }
2996
+ async function listen(server, port) {
2997
+ for (let attempt = 0; attempt < 10; attempt += 1) {
2998
+ const candidate = port === 0 ? 0 : port + attempt;
2999
+ try {
3000
+ await new Promise((resolve14, reject) => {
3001
+ server.once("error", reject);
3002
+ server.listen(candidate, "127.0.0.1", () => {
3003
+ server.off("error", reject);
3004
+ resolve14();
3005
+ });
3006
+ });
3007
+ return server.address().port;
3008
+ } catch (error) {
3009
+ if (error.code !== "EADDRINUSE" || port === 0) throw error;
3010
+ }
3011
+ }
3012
+ throw new Error(`Ports ${port}-${port + 9} are in use; pass --port.`);
3013
+ }
3014
+
3015
+ // src/cli/commands/dashboard.ts
3016
+ function openBrowser(url) {
3017
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
3018
+ try {
3019
+ spawn2(command, args, { detached: true, stdio: "ignore" }).on("error", () => void 0).unref();
3020
+ } catch {
3021
+ }
3022
+ }
3023
+ function registerDashboardCommand(program, runSafely2) {
3024
+ program.command("dashboard").description("Open a live local page of prompts recalled, MCP tool calls, tokens and latency.").option(
3025
+ "--port <port>",
3026
+ "Port on 127.0.0.1 (the next free one is used if taken).",
3027
+ Number,
3028
+ 4777
3029
+ ).option("--no-open", "Print the URL without opening a browser.").action(async (opts) => {
3030
+ await runSafely2(async () => {
3031
+ const root = process.cwd();
3032
+ const dashboard = await startDashboard({ repositoryRoot: root, port: opts.port });
3033
+ const recorded = (await readTraces(root)).length;
3034
+ console.log(`${pc4.green("\u25CF")} CodeBuddy dashboard ${pc4.bold(dashboard.url)}`);
3035
+ console.log(
3036
+ pc4.dim(
3037
+ recorded > 0 ? ` ${recorded} event(s) in ${tracePath(root)}; new ones appear live.` : " No traces yet: they appear as your agent uses CodeBuddy (hooks or MCP tools)."
3038
+ )
3039
+ );
3040
+ console.log(pc4.dim(" Ctrl+C to stop."));
3041
+ if (opts.open) openBrowser(dashboard.url);
3042
+ await new Promise((resolve14) => {
3043
+ const stop = () => void dashboard.close().then(resolve14);
3044
+ process.once("SIGINT", stop);
3045
+ process.once("SIGTERM", stop);
3046
+ });
3047
+ });
3048
+ });
3049
+ }
3050
+
3051
+ // src/cli/commands/eval.ts
3052
+ import pc5 from "picocolors";
3053
+
2555
3054
  // src/eval/runner.ts
2556
3055
  import { readFile as readFile12 } from "fs/promises";
2557
3056
  import { dirname as dirname10, isAbsolute, join as join14, resolve as resolve11 } from "path";
@@ -2622,17 +3121,17 @@ function registerEvalCommand(program, runSafely2) {
2622
3121
  }
2623
3122
  function printEvaluation(result) {
2624
3123
  console.log(
2625
- `${result.passed ? pc4.green("passed") : pc4.red("failed")} ${result.summary.passed}/${result.summary.total} evaluation cases`
3124
+ `${result.passed ? pc5.green("passed") : pc5.red("failed")} ${result.summary.passed}/${result.summary.total} evaluation cases`
2626
3125
  );
2627
3126
  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}`));
3127
+ console.log(` ${item.passed ? pc5.green("\u2713") : pc5.red("\xD7")} ${item.id}`);
3128
+ if (!item.passed) for (const failure of item.failures) console.log(pc5.dim(` ${failure}`));
2630
3129
  }
2631
- console.log(pc4.dim(`dataset: ${result.datasetPath}`));
3130
+ console.log(pc5.dim(`dataset: ${result.datasetPath}`));
2632
3131
  }
2633
3132
 
2634
3133
  // src/cli/commands/github.ts
2635
- import pc5 from "picocolors";
3134
+ import pc6 from "picocolors";
2636
3135
  function registerGitHubCommands(program, runSafely2) {
2637
3136
  const github = program.command("github").description("Install GitHub workflow integrations.");
2638
3137
  github.command("install").option("--force", "Replace an existing CodeBuddy workflow.").description("Install the PR change-safety report workflow.").action(async (opts) => {
@@ -2640,15 +3139,16 @@ function registerGitHubCommands(program, runSafely2) {
2640
3139
  const result = await installGitHubWorkflow(process.cwd(), {
2641
3140
  ...opts.force !== void 0 ? { force: opts.force } : {}
2642
3141
  });
2643
- console.log(`${result.created ? "installed" : "already present"} ${pc5.dim(result.path)}`);
3142
+ console.log(`${result.created ? "installed" : "already present"} ${pc6.dim(result.path)}`);
2644
3143
  if (result.created) console.log("Commit this workflow to enable CodeBuddy PR reports.");
2645
3144
  });
2646
3145
  });
2647
3146
  }
2648
3147
 
2649
3148
  // src/cli/commands/hooks.ts
3149
+ import { spawn as spawn3 } from "child_process";
2650
3150
  import { relative as relative2 } from "path";
2651
- import pc6 from "picocolors";
3151
+ import pc7 from "picocolors";
2652
3152
  function registerHooksCommands(program, runSafely2) {
2653
3153
  const hooks = program.command("hooks").description("Enforce automatic recall/capture via client lifecycle hooks.");
2654
3154
  hooks.command("install").option(
@@ -2665,11 +3165,12 @@ function registerHooksCommands(program, runSafely2) {
2665
3165
  const where = relative2(process.cwd(), step.target) || step.target;
2666
3166
  const changed = step.action !== "unchanged";
2667
3167
  console.log(
2668
- `${changed ? pc6.green("installed") : pc6.dim("unchanged")} CodeBuddy hooks in ${where}`
3168
+ `${changed ? pc7.green("installed") : pc7.dim("unchanged")} CodeBuddy hooks in ${where}`
2669
3169
  );
2670
3170
  await reconcileRules(client, "hooks");
3171
+ syncInBackground(process.cwd());
2671
3172
  console.log(
2672
- pc6.dim(
3173
+ pc7.dim(
2673
3174
  ` prompt\u2192recall \xB7 edit\u2192stage \xB7 turn end\u2192capture \xB7 compaction\u2192reset. Restart ${CLIENT_ADAPTERS[client].name} to activate.`
2674
3175
  )
2675
3176
  );
@@ -2684,7 +3185,7 @@ function registerHooksCommands(program, runSafely2) {
2684
3185
  const client = assertHookClient(opts.client);
2685
3186
  const changed = (await setHooks(process.cwd(), false, client))?.action === "removed";
2686
3187
  console.log(
2687
- changed ? pc6.yellow("removed CodeBuddy hooks") : pc6.dim("no CodeBuddy hooks found")
3188
+ changed ? pc7.yellow("removed CodeBuddy hooks") : pc7.dim("no CodeBuddy hooks found")
2688
3189
  );
2689
3190
  await reconcileRules(client, "agent");
2690
3191
  });
@@ -2697,7 +3198,7 @@ function registerHooksCommands(program, runSafely2) {
2697
3198
  await runSafely2(async () => {
2698
3199
  const installed = await clientHooksInstalled(process.cwd(), parseClient(opts.client));
2699
3200
  console.log(
2700
- installed ? pc6.green("CodeBuddy hooks: installed") : pc6.dim("CodeBuddy hooks: not installed")
3201
+ installed ? pc7.green("CodeBuddy hooks: installed") : pc7.dim("CodeBuddy hooks: not installed")
2701
3202
  );
2702
3203
  });
2703
3204
  });
@@ -2725,9 +3226,21 @@ function registerHooksCommands(program, runSafely2) {
2725
3226
  `codebuddy: captured from ${changedFiles.length} changed file(s) \u2014 ${result.saved.length} saved, ${result.queuedForReview.length} queued
2726
3227
  `
2727
3228
  );
3229
+ if (result.saved.length > 0) syncInBackground(input.cwd ?? process.cwd());
2728
3230
  }
2729
3231
  });
2730
3232
  });
3233
+ hooks.command("sync").description(
3234
+ "[hook] Reindex Markdown memory into PostgreSQL and embed it locally (runs detached after capture)."
3235
+ ).action(async () => {
3236
+ await failSoft(async () => {
3237
+ const result = await runSyncHook(process.cwd());
3238
+ process.stderr.write(
3239
+ result ? `codebuddy: synced ${result.reindex.facts.imported} new / ${result.reindex.facts.updated} updated fact(s)${result.reindex.reembedding ? `, ${result.reindex.reembedding} re-embedding` : ""}${result.drained ? "" : " (embeddings still pending)"}
3240
+ ` : "codebuddy: no PostgreSQL configured; nothing to sync\n"
3241
+ );
3242
+ });
3243
+ });
2731
3244
  hooks.command("pretool").description(
2732
3245
  "[hook] Outline large unranged Reads and cap content Greps (never grants permission)."
2733
3246
  ).action(async () => {
@@ -2754,16 +3267,28 @@ async function reconcileRules(client, mode) {
2754
3267
  if (result.action !== "unchanged") {
2755
3268
  const where = relative2(process.cwd(), result.path) || result.path;
2756
3269
  console.log(
2757
- `${pc6.cyan("updated")} ${where} workflow to ${mode === "hooks" ? "hook" : "agent"}-driven mode`
3270
+ `${pc7.cyan("updated")} ${where} workflow to ${mode === "hooks" ? "hook" : "agent"}-driven mode`
2758
3271
  );
2759
3272
  }
2760
3273
  }
3274
+ function syncInBackground(cwd) {
3275
+ const entry = process.argv[1];
3276
+ if (!entry) return;
3277
+ try {
3278
+ spawn3(process.execPath, [...process.execArgv, entry, "hooks", "sync"], {
3279
+ cwd,
3280
+ detached: true,
3281
+ stdio: "ignore"
3282
+ }).unref();
3283
+ } catch {
3284
+ }
3285
+ }
2761
3286
 
2762
3287
  // src/cli/commands/indexing.ts
2763
- import pc8 from "picocolors";
3288
+ import pc9 from "picocolors";
2764
3289
 
2765
3290
  // src/cli/commands/llm.ts
2766
- import pc7 from "picocolors";
3291
+ import pc8 from "picocolors";
2767
3292
  import { z } from "zod";
2768
3293
  var KEY_ENV = { groq: "GROQ_API_KEY", openrouter: "OPENROUTER_API_KEY" };
2769
3294
  var WATCH_BATCH = 8;
@@ -2818,7 +3343,7 @@ function registerLlmCommands(program, runSafely2) {
2818
3343
  await runSafely2(async () => {
2819
3344
  const report = await llmStatusReport(process.cwd());
2820
3345
  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>`));
3346
+ if (!report.keyPresent) console.log(pc8.dim(`next: export ${report.keyEnv}=<your key>`));
2822
3347
  });
2823
3348
  });
2824
3349
  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) => {
@@ -2865,7 +3390,7 @@ function registerLlmCommands(program, runSafely2) {
2865
3390
  const result = await summarizeRepository(process.cwd(), {
2866
3391
  ...opts.limit !== void 0 ? { limit: opts.limit } : {},
2867
3392
  signal: controller.signal,
2868
- onProgress: (done, total) => console.log(pc7.dim(`summarized ${done}/${total}`))
3393
+ onProgress: (done, total) => console.log(pc8.dim(`summarized ${done}/${total}`))
2869
3394
  });
2870
3395
  console.log(
2871
3396
  `${result.summarized} summarized, ${result.skipped} skipped, ${result.remaining} remaining (${result.stoppedBy})`
@@ -2892,7 +3417,7 @@ function registerIndexCommands(program, runSafely2) {
2892
3417
  const stop = () => controller.abort();
2893
3418
  process.once("SIGINT", stop);
2894
3419
  process.once("SIGTERM", stop);
2895
- console.log(pc8.dim("watching for changes\u2026 press Ctrl+C to stop"));
3420
+ console.log(pc9.dim("watching for changes\u2026 press Ctrl+C to stop"));
2896
3421
  await watchRepository({
2897
3422
  ...opts.debounce !== void 0 ? { debounceMs: opts.debounce } : {},
2898
3423
  signal: controller.signal,
@@ -2901,21 +3426,21 @@ function registerIndexCommands(program, runSafely2) {
2901
3426
  if (!opts.llm || result.added + result.changed === 0) return;
2902
3427
  void summarizeAfterIndex(process.cwd()).then((summary) => {
2903
3428
  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}`)));
3429
+ console.log(pc9.dim(` llm: ${summary.summarized} file(s) summarized`));
3430
+ }).catch((error) => console.error(pc9.red(`llm: ${error.message}`)));
2906
3431
  },
2907
- onError: (error) => console.error(pc8.red(error.message))
3432
+ onError: (error) => console.error(pc9.red(error.message))
2908
3433
  });
2909
- console.log(pc8.dim("stopped watching"));
3434
+ console.log(pc9.dim("stopped watching"));
2910
3435
  });
2911
3436
  });
2912
3437
  }
2913
3438
  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);
3439
+ const summary = `${result.totalIndexed} indexed (${pc9.green(`+${result.added}`)} ${pc9.yellow(`~${result.changed}`)} ${pc9.dim(`=${result.unchanged}`)} ${pc9.red(`-${result.removed}`)}) in ${result.durationMs}ms`;
3440
+ console.log(compact ? pc9.dim(summary) : summary);
2916
3441
  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}`));
3442
+ console.log(pc9.dim(` ${result.skipped} skipped (too large or unreadable)`));
3443
+ for (const error of result.errors) console.log(pc9.red(` ${error.path}: ${error.error}`));
2919
3444
  }
2920
3445
 
2921
3446
  // src/cli/commands/init-wizard.ts
@@ -2923,7 +3448,7 @@ import { writeFile as writeFile2 } from "fs/promises";
2923
3448
  import { confirm, isCancel, log, note, password, spinner, text } from "@clack/prompts";
2924
3449
 
2925
3450
  // src/cli/commands/postgres-docker.ts
2926
- import { spawn as spawn2 } from "child_process";
3451
+ import { spawn as spawn4 } from "child_process";
2927
3452
  import { existsSync as existsSync3 } from "fs";
2928
3453
  import { dirname as dirname11, resolve as resolve12 } from "path";
2929
3454
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -2944,7 +3469,7 @@ function resolveBundledComposeFile() {
2944
3469
  ${candidates.join("\n ")}`);
2945
3470
  }
2946
3471
  async function runDockerCompose(args, options = {}) {
2947
- const spawnImpl = options.spawnImpl ?? spawn2;
3472
+ const spawnImpl = options.spawnImpl ?? spawn4;
2948
3473
  const composeFile = resolveBundledComposeFile();
2949
3474
  const attempts = [
2950
3475
  { kind: "v2", bin: "docker", args: ["compose", "-f", composeFile, ...args] },
@@ -3024,8 +3549,7 @@ async function runInitWizard() {
3024
3549
  );
3025
3550
  const hfToken = unwrap(
3026
3551
  await password({
3027
- message: "Hugging Face token (required for embeddings)",
3028
- validate: (value) => value && value.length > 0 ? void 0 : "HF_TOKEN is required."
3552
+ message: "Hugging Face token (optional; only for hosted embeddings)"
3029
3553
  })
3030
3554
  );
3031
3555
  const databaseUrl = unwrap(
@@ -3095,7 +3619,7 @@ async function runInitWizard() {
3095
3619
  const fileConfig = {
3096
3620
  postgresUrl: databaseUrl,
3097
3621
  namespace,
3098
- provider: { type: "huggingface", apiKey: hfToken },
3622
+ provider: { type: "local", ...hfToken ? { apiKey: hfToken } : {} },
3099
3623
  tokenBudget: 4e3
3100
3624
  };
3101
3625
  await writeFile2(initialised.path, `${JSON.stringify(fileConfig, null, 2)}
@@ -3136,7 +3660,7 @@ async function runInitWizard() {
3136
3660
  }
3137
3661
 
3138
3662
  // src/cli/commands/learn.ts
3139
- import pc9 from "picocolors";
3663
+ import pc10 from "picocolors";
3140
3664
 
3141
3665
  // src/learning/outcomes.ts
3142
3666
  async function recordChangeOutcome(input) {
@@ -3197,15 +3721,15 @@ function registerLearnCommand(program, runSafely2) {
3197
3721
  ...opts.commit ? { commit: opts.commit } : {},
3198
3722
  ...opts.plan ? { planId: opts.plan } : {}
3199
3723
  });
3200
- console.log(`${pc9.green("recorded")} ${result.id} (${result.category})`);
3201
- console.log(pc9.dim(result.path));
3724
+ console.log(`${pc10.green("recorded")} ${result.id} (${result.category})`);
3725
+ console.log(pc10.dim(result.path));
3202
3726
  });
3203
3727
  }
3204
3728
  );
3205
3729
  }
3206
3730
 
3207
3731
  // src/cli/commands/map.ts
3208
- import pc10 from "picocolors";
3732
+ import pc11 from "picocolors";
3209
3733
  function registerMapCommands(program, runSafely2) {
3210
3734
  const map = program.command("map").description("Query the lightweight architecture map.");
3211
3735
  map.command("build").description("Scan the current repository and summarize the module graph.").action(async () => {
@@ -3225,7 +3749,7 @@ function registerMapCommands(program, runSafely2) {
3225
3749
  return;
3226
3750
  }
3227
3751
  if (edges.length === 0) {
3228
- console.log(pc10.dim("no dependencies found"));
3752
+ console.log(pc11.dim("no dependencies found"));
3229
3753
  return;
3230
3754
  }
3231
3755
  for (const edge of edges) console.log(`${edge.from} -> ${edge.to}`);
@@ -3240,7 +3764,7 @@ function registerMapCommands(program, runSafely2) {
3240
3764
  return;
3241
3765
  }
3242
3766
  if (result.edges.length === 0) {
3243
- console.log(pc10.dim("no dependents found"));
3767
+ console.log(pc11.dim("no dependents found"));
3244
3768
  return;
3245
3769
  }
3246
3770
  for (const edge of result.edges) console.log(`${edge.from} -> ${edge.to}`);
@@ -3249,9 +3773,9 @@ function registerMapCommands(program, runSafely2) {
3249
3773
  }
3250
3774
 
3251
3775
  // src/cli/commands/memory-review.ts
3252
- import { spawn as spawn3 } from "child_process";
3776
+ import { spawn as spawn5 } from "child_process";
3253
3777
  import { relative as relative3 } from "path";
3254
- import pc11 from "picocolors";
3778
+ import pc12 from "picocolors";
3255
3779
  function registerMemoryCommands(program, runSafely2) {
3256
3780
  const memory = program.command("memory").description("Inspect and curate CodeBuddy memory.");
3257
3781
  const review = memory.command("review").description("Review memory candidates captured automatically after turns.");
@@ -3263,11 +3787,11 @@ function registerMemoryCommands(program, runSafely2) {
3263
3787
  return;
3264
3788
  }
3265
3789
  if (items.length === 0) {
3266
- console.log(pc11.dim("no pending review items"));
3790
+ console.log(pc12.dim("no pending review items"));
3267
3791
  return;
3268
3792
  }
3269
3793
  for (const item of items) console.log(renderRow(item));
3270
- console.log(pc11.dim(`
3794
+ console.log(pc12.dim(`
3271
3795
  ${items.length} pending. approve <id> | reject <id> | show <id>`));
3272
3796
  });
3273
3797
  });
@@ -3281,7 +3805,7 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
3281
3805
  const factId = await approveReviewItem(id, {
3282
3806
  ...opts.incident !== void 0 ? { incidentId: opts.incident } : {}
3283
3807
  });
3284
- console.log(`${pc11.green("approved")} ${id} \u2192 ${factId}`);
3808
+ console.log(`${pc12.green("approved")} ${id} \u2192 ${factId}`);
3285
3809
  });
3286
3810
  });
3287
3811
  review.command("reject").argument("<id>").description("Discard a review candidate without saving it.").action(async (id) => {
@@ -3289,7 +3813,7 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
3289
3813
  const store = new ReviewStore();
3290
3814
  await store.read(id);
3291
3815
  await store.delete(id);
3292
- console.log(`${pc11.yellow("rejected")} ${id}`);
3816
+ console.log(`${pc12.yellow("rejected")} ${id}`);
3293
3817
  });
3294
3818
  });
3295
3819
  review.command("edit").argument("<id>").description("Open a review candidate in $EDITOR and re-validate on save.").action(async (id) => {
@@ -3298,44 +3822,44 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
3298
3822
  const item = await store.read(id);
3299
3823
  await openEditor(item.path);
3300
3824
  await store.read(id);
3301
- console.log(pc11.dim("saved and validated"));
3825
+ console.log(pc12.dim("saved and validated"));
3302
3826
  });
3303
3827
  });
3304
3828
  }
3305
3829
  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}`;
3830
+ const flag = item.sensitive ? pc12.red(" \u26A0 sensitive") : "";
3831
+ return `${classColor(item.class)} ${pc12.dim(item.id)} ${truncate(item.content, 70)}${flag}`;
3308
3832
  }
3309
3833
  function renderItem(item) {
3310
3834
  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}`
3835
+ `${classColor(item.class)} ${pc12.dim(item.id)}`,
3836
+ `${pc12.bold("reason:")} ${item.reason}`,
3837
+ `${pc12.bold("confidence:")} ${item.confidence.toFixed(2)}`,
3838
+ `${pc12.bold("triple:")} ${item.subject} \xB7 ${item.predicate} \xB7 ${item.object}`
3315
3839
  ];
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}`);
3840
+ if (item.paths.length > 0) lines.push(`${pc12.bold("paths:")} ${item.paths.join(", ")}`);
3841
+ if (item.severity) lines.push(`${pc12.bold("severity:")} ${item.severity}`);
3318
3842
  if (item.resolvesIncidents) {
3319
3843
  lines.push(
3320
- `${pc11.bold("resolves:")} ${item.resolvesIncidents.join(", ") || "(no candidate; approve with --incident <id>)"}`
3844
+ `${pc12.bold("resolves:")} ${item.resolvesIncidents.join(", ") || "(no candidate; approve with --incident <id>)"}`
3321
3845
  );
3322
3846
  }
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)));
3847
+ if (item.sensitive) lines.push(pc12.red(`\u26A0 sensitive: ${item.sensitiveReasons.join(", ")}`));
3848
+ lines.push("", item.content, "", pc12.dim(relative3(process.cwd(), item.path)));
3325
3849
  return lines.join("\n");
3326
3850
  }
3327
3851
  function classColor(memoryClass) {
3328
3852
  switch (memoryClass) {
3329
3853
  case "incident":
3330
- return pc11.red(memoryClass);
3854
+ return pc12.red(memoryClass);
3331
3855
  case "decision":
3332
- return pc11.cyan(memoryClass);
3856
+ return pc12.cyan(memoryClass);
3333
3857
  case "fact":
3334
- return pc11.green(memoryClass);
3858
+ return pc12.green(memoryClass);
3335
3859
  case "note":
3336
- return pc11.yellow(memoryClass);
3860
+ return pc12.yellow(memoryClass);
3337
3861
  default:
3338
- return pc11.dim(memoryClass);
3862
+ return pc12.dim(memoryClass);
3339
3863
  }
3340
3864
  }
3341
3865
  function truncate(value, max) {
@@ -3345,16 +3869,422 @@ function truncate(value, max) {
3345
3869
  function openEditor(path) {
3346
3870
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3347
3871
  return new Promise((resolvePromise, reject) => {
3348
- const child = spawn3(editor, [path], { stdio: "inherit" });
3872
+ const child = spawn5(editor, [path], { stdio: "inherit" });
3349
3873
  child.on("error", reject);
3350
3874
  child.on("close", () => resolvePromise());
3351
3875
  });
3352
3876
  }
3353
3877
 
3878
+ // src/cli/commands/models.ts
3879
+ import { existsSync as existsSync4 } from "fs";
3880
+ import { join as join15 } from "path";
3881
+ import { spinner as spinner2 } from "@clack/prompts";
3882
+ import pc13 from "picocolors";
3883
+
3884
+ // evals/fact-relevance.json
3885
+ var fact_relevance_default = {
3886
+ version: 1,
3887
+ note: "Task prompts vs durable project facts. Queries are paraphrased so most share few or no words with their fact; every unlisted (query, fact) pair is irrelevant.",
3888
+ facts: {
3889
+ cookie: "OAuth callback breaks unless the session cookie is SameSite=Lax; Strict drops it on the cross-site redirect.",
3890
+ pool: "Postgres connections go through one bounded pool of 10; never open ad-hoc clients in request handlers.",
3891
+ release: "npm releases are published from CI with provenance; never publish from a laptop.",
3892
+ biome: "Biome owns formatting and import order; run biome check --write before committing.",
3893
+ hook: "Prompt hooks must answer within 15 seconds or the host drops the injected block.",
3894
+ migration: "Migrations must create the pgvector extension before 0003 adds the vector column.",
3895
+ redaction: "Secrets are redacted before a memory is written; the content hash is computed after redaction.",
3896
+ ledger: "The session ledger keeps the last 8 acknowledged versions per section so a task switch back is not resent.",
3897
+ zvec: "zvec-grep is optional; any failure falls back to keyword ranking with a retrieval explain line.",
3898
+ tests: "Integration tests skip unless CODEBUDDY_TEST_DATABASE_URL points at a live Postgres.",
3899
+ esm: "Source imports use .js specifiers even for TypeScript files because the package is ESM.",
3900
+ stdout: "Never log to stdout in the MCP server: stdout carries JSON-RPC and any stray line corrupts it.",
3901
+ budget: "The BYOK LLM layer stops background calls at 70% of the daily budget and backs off after a 429.",
3902
+ worktree: "Use a temporary WIP commit instead of git stash; the stash stack is shared across worktrees.",
3903
+ rateLimit: "The public API allows 100 requests per minute per token; bursts above it get HTTP 429.",
3904
+ timezone: "All timestamps are stored in UTC and converted to the user's zone only for display.",
3905
+ avatar: "Profile images are resized to 256px and stored as WebP in the media bucket.",
3906
+ flags: "Feature flags are read once at boot; changing one requires a restart of the worker.",
3907
+ pagination: "List endpoints use cursor pagination with an opaque base64url cursor of createdAt and id.",
3908
+ i18n: "UI strings live in locales/en.json; never hard-code user-facing text in components."
3909
+ },
3910
+ queries: [
3911
+ {
3912
+ q: "users get bounced back to sign-in forever after the identity provider returns",
3913
+ relevant: ["cookie"]
3914
+ },
3915
+ { q: "handlers are exhausting connections to the database under load", relevant: ["pool"] },
3916
+ { q: "cut a new version and push the package to the registry", relevant: ["release"] },
3917
+ { q: "the injected block sometimes never shows up in the chat", relevant: ["hook"] },
3918
+ { q: "fresh database setup fails on the embedding column", relevant: ["migration"] },
3919
+ { q: "an API key ended up in a saved note", relevant: ["redaction"] },
3920
+ { q: "context is sent again after I switch tasks and come back", relevant: ["ledger"] },
3921
+ { q: "semantic code search crashes and the agent gets no files", relevant: ["zvec"] },
3922
+ { q: "why do the db tests not run on my machine", relevant: ["tests"] },
3923
+ { q: "module not found when importing a sibling ts file at runtime", relevant: ["esm"] },
3924
+ { q: "Claude Desktop says Unexpected token in the server response", relevant: ["stdout"] },
3925
+ { q: "the summarizer quietly stopped calling Groq in the afternoon", relevant: ["budget"] },
3926
+ { q: "my uncommitted changes vanished after another session ran", relevant: ["worktree"] },
3927
+ {
3928
+ q: "clients see too many requests errors during the import job",
3929
+ relevant: ["rateLimit"]
3930
+ },
3931
+ { q: "meeting times show up one hour off for users in Berlin", relevant: ["timezone"] },
3932
+ { q: "uploaded profile pictures look blurry", relevant: ["avatar"] },
3933
+ { q: "I toggled the new checkout switch but nothing changed", relevant: ["flags"] },
3934
+ { q: "infinite scroll repeats items on the second page", relevant: ["pagination"] },
3935
+ { q: "translate the onboarding screen to German", relevant: ["i18n"] },
3936
+ { q: "format the code before I commit", relevant: ["biome"] },
3937
+ { q: "make the sidebar collapsible", relevant: [] },
3938
+ { q: "add dark mode to the dashboard", relevant: [] },
3939
+ { q: "write a haiku about the project", relevant: [] },
3940
+ { q: "rename the button label on the settings page", relevant: [] }
3941
+ ]
3942
+ };
3943
+
3944
+ // src/providers/calibration.ts
3945
+ async function calibrateEmbedder(embed) {
3946
+ const data = fact_relevance_default;
3947
+ const factIds = Object.keys(data.facts);
3948
+ const factVectors = await embed(Object.values(data.facts));
3949
+ const queryVectors = await embed(data.queries.map(({ q }) => q));
3950
+ const dot = (a, b) => a.reduce((sum, value, index) => sum + value * (b[index] ?? 0), 0);
3951
+ const scores = queryVectors.map((query) => factVectors.map((fact) => dot(query, fact)));
3952
+ const relevant = (qi, fi) => data.queries[qi]?.relevant.includes(factIds[fi] ?? "") ?? false;
3953
+ const positives = [];
3954
+ const negatives = [];
3955
+ for (const [qi, row2] of scores.entries()) {
3956
+ for (const [fi, score] of row2.entries()) (relevant(qi, fi) ? positives : negatives).push(score);
3957
+ }
3958
+ const auc = positives.reduce((wins, p) => wins + negatives.filter((n) => p > n).length, 0) / (positives.length * negatives.length);
3959
+ let hits = 0;
3960
+ let reciprocal = 0;
3961
+ let answerable = 0;
3962
+ for (const [qi, row2] of scores.entries()) {
3963
+ if (!data.queries[qi]?.relevant.length) continue;
3964
+ answerable += 1;
3965
+ const order = row2.map((score, fi) => ({ score, fi })).sort((a, b) => b.score - a.score);
3966
+ const rank = order.findIndex(({ fi }) => relevant(qi, fi)) + 1;
3967
+ if (rank === 1) hits += 1;
3968
+ reciprocal += 1 / rank;
3969
+ }
3970
+ const at = (floor) => {
3971
+ let tp = 0;
3972
+ let fp = 0;
3973
+ let abstained = 0;
3974
+ let unrelated = 0;
3975
+ for (const [qi, row2] of scores.entries()) {
3976
+ const admitted = row2.flatMap((score, fi) => score >= floor ? [fi] : []);
3977
+ for (const fi of admitted) {
3978
+ if (relevant(qi, fi)) tp += 1;
3979
+ else fp += 1;
3980
+ }
3981
+ if (!data.queries[qi]?.relevant.length) {
3982
+ unrelated += 1;
3983
+ if (admitted.length === 0) abstained += 1;
3984
+ }
3985
+ }
3986
+ const precision = tp + fp === 0 ? 1 : tp / (tp + fp);
3987
+ const recall = tp / positives.length;
3988
+ const f05 = precision + recall === 0 ? 0 : 1.25 * precision * recall / (0.25 * precision + recall);
3989
+ return { floor, precision, recall, f05, abstain: abstained / unrelated };
3990
+ };
3991
+ const sweep = Array.from({ length: 96 }, (_, i) => at(Math.round((0.05 + i * 0.01) * 100) / 100));
3992
+ const best = sweep.reduce((left, right) => right.f05 > left.f05 ? right : left);
3993
+ return { auc, recallAt1: hits / answerable, mrr: reciprocal / answerable, best, at };
3994
+ }
3995
+
3996
+ // src/cli/commands/models.ts
3997
+ var EMBEDDERS = ["local", "huggingface", "openrouter"];
3998
+ var RERANKERS = ["off", "local", "huggingface", "openrouter"];
3999
+ var KEY_ENV2 = { huggingface: "HF_TOKEN", openrouter: "OPENROUTER_API_KEY" };
4000
+ var ok = (text3) => `${pc13.green("\u2713")} ${text3}`;
4001
+ var warn = (text3) => `${pc13.yellow("!")} ${text3}`;
4002
+ var row = (label, value) => ` ${pc13.dim(label.padEnd(11))}${value}`;
4003
+ async function withSpinner(label, work, done) {
4004
+ const progress = spinner2();
4005
+ progress.start(label);
4006
+ try {
4007
+ const value = await work();
4008
+ progress.stop(done(value));
4009
+ return value;
4010
+ } catch (error) {
4011
+ progress.stop(pc13.red(`\u2717 ${label}`), 1);
4012
+ throw error;
4013
+ }
4014
+ }
4015
+ function keyPresent(provider, apiKey) {
4016
+ return Boolean(process.env[KEY_ENV2[provider]] || provider === "huggingface" && apiKey);
4017
+ }
4018
+ function localModelDownloaded(id, revision) {
4019
+ const dir = join15(modelCacheDir(), id, ...revision ? [revision] : []);
4020
+ return existsSync4(join15(dir, "onnx", "model_quantized.onnx"));
4021
+ }
4022
+ async function vectorCounts(postgresUrl, namespace, space) {
4023
+ const client = createDatabaseClient({ postgresUrl, maxConnections: 1, connectTimeoutSeconds: 3 });
4024
+ try {
4025
+ const [counts] = await client.sql`
4026
+ select
4027
+ count(*) filter (where e.status = 'ready' and e.embedding_space = ${space ?? ""})::int as ready,
4028
+ count(*) filter (where e.status in ('pending', 'processing'))::int as pending,
4029
+ count(*) filter (where e.status = 'failed')::int as failed,
4030
+ count(*) filter (where e.status = 'ready' and e.embedding_space is distinct from ${space ?? ""})::int as other
4031
+ from embeddings e join namespaces n on n.id = e.namespace_id
4032
+ where n.name = ${namespace}
4033
+ `;
4034
+ return counts ?? { ready: 0, pending: 0, failed: 0, other: 0 };
4035
+ } catch (error) {
4036
+ return String(redactSecrets(error.message)).split("\n")[0] ?? "unreachable";
4037
+ } finally {
4038
+ await client.close().catch(() => void 0);
4039
+ }
4040
+ }
4041
+ async function printStatus() {
4042
+ const file = await readConfigFile();
4043
+ const config = await loadRuntimeConfig(process.cwd(), { requireDatabase: false });
4044
+ const provider = config.provider;
4045
+ const embedder = buildProviderFromConfig(config.provider);
4046
+ const space = embedder.space;
4047
+ const model = provider.type === "local" ? LOCAL_EMBEDDING_MODEL.id : provider.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS[provider.type];
4048
+ const floor = provider.relevanceFloor ?? getModelDefinition(model)?.relevanceFloor;
4049
+ const next = [];
4050
+ console.log(pc13.bold("Embeddings"));
4051
+ console.log(row("provider", `${provider.type} ${pc13.dim(model)}`));
4052
+ if (provider.type === "local") {
4053
+ const ready = localModelDownloaded(LOCAL_EMBEDDING_MODEL.id, LOCAL_EMBEDDING_MODEL.revision);
4054
+ console.log(
4055
+ row(
4056
+ "model",
4057
+ ready ? ok(`downloaded ${pc13.dim(modelCacheDir())}`) : warn("not downloaded yet (~90 MB)")
4058
+ )
4059
+ );
4060
+ if (!ready) next.push("codebuddy models pull");
4061
+ } else {
4062
+ const present = keyPresent(provider.type, provider.apiKey);
4063
+ console.log(
4064
+ row(
4065
+ "key",
4066
+ present ? ok(KEY_ENV2[provider.type]) : warn(`${KEY_ENV2[provider.type]} is not set`)
4067
+ )
4068
+ );
4069
+ if (!present) next.push(`export ${KEY_ENV2[provider.type]}=\u2026`);
4070
+ }
4071
+ console.log(
4072
+ row(
4073
+ "floor",
4074
+ floor !== void 0 ? `${floor} ${pc13.dim(provider.relevanceFloor !== void 0 ? "(calibrated for this project)" : "(built-in)")}` : warn("not calibrated: dense recall is off for this model")
4075
+ )
4076
+ );
4077
+ if (floor === void 0) next.push("codebuddy models calibrate");
4078
+ const rerank = file.rerank?.provider ?? "off";
4079
+ console.log(pc13.bold("Reranker"));
4080
+ if (rerank === "off") {
4081
+ console.log(
4082
+ row("provider", `off ${pc13.dim("(default: no reranker beat dense order in evals)")}`)
4083
+ );
4084
+ } else {
4085
+ console.log(
4086
+ row(
4087
+ "provider",
4088
+ `${rerank} ${pc13.dim(file.rerank?.model ?? DEFAULT_RERANK_MODELS[rerank].id)}`
4089
+ )
4090
+ );
4091
+ if (rerank !== "local") {
4092
+ const present = keyPresent(rerank, file.provider?.apiKey);
4093
+ console.log(
4094
+ row(
4095
+ "key",
4096
+ present ? ok(KEY_ENV2[rerank]) : warn(`${KEY_ENV2[rerank]} is not set; reranking is skipped`)
4097
+ )
4098
+ );
4099
+ }
4100
+ }
4101
+ console.log(pc13.bold("Memory index"));
4102
+ if (process.env.CODEBUDDY_SEMANTIC === "0") {
4103
+ console.log(row("recall", warn("CODEBUDDY_SEMANTIC=0: lexical only")));
4104
+ }
4105
+ if (!config.postgresUrl) {
4106
+ console.log(row("postgres", warn("not configured: recall is lexical only")));
4107
+ next.push("codebuddy use (sets up PostgreSQL)");
4108
+ } else {
4109
+ const counts = await vectorCounts(
4110
+ config.postgresUrl,
4111
+ config.namespace,
4112
+ space ? embeddingSpaceKey(space) : void 0
4113
+ );
4114
+ if (typeof counts === "string") {
4115
+ console.log(row("postgres", warn(`unreachable: ${counts}`)));
4116
+ next.push("codebuddy postgres up");
4117
+ } else {
4118
+ console.log(row("postgres", ok(`namespace ${config.namespace}`)));
4119
+ console.log(
4120
+ row(
4121
+ "vectors",
4122
+ `${counts.ready} ready` + (counts.pending ? `, ${counts.pending} pending` : "") + (counts.failed ? pc13.red(`, ${counts.failed} failed`) : "") + (counts.other ? pc13.yellow(`, ${counts.other} from another model`) : "")
4123
+ )
4124
+ );
4125
+ if (counts.pending || counts.other)
4126
+ next.push("codebuddy hooks sync (embeds pending and re-embeds old vectors)");
4127
+ }
4128
+ }
4129
+ console.log(
4130
+ next.length > 0 ? `
4131
+ ${pc13.bold("Next")}
4132
+ ${next.map((step) => ` ${pc13.cyan(step)}`).join("\n")}` : `
4133
+ ${ok("Semantic recall is ready.")} ${pc13.dim('Try: codebuddy context preview "<task>"')}`
4134
+ );
4135
+ }
4136
+ function registerModelsCommands(program, runSafely2) {
4137
+ const models = program.command("models").description("Embedding model and reranker behind per-prompt memory recall.").action(async () => runSafely2(printStatus));
4138
+ models.command("status").description("Show the embedding model, reranker, keys and vector index health.").action(async () => runSafely2(printStatus));
4139
+ models.command("use").argument("<provider>", `Embedding provider: ${EMBEDDERS.join(" | ")}.`).option("--model <id>", "Hosted model id (must return 384-d vectors).").option(
4140
+ "--floor <cosine>",
4141
+ "Relevance floor; otherwise built-in or `models calibrate`.",
4142
+ Number
4143
+ ).description("Choose where embeddings run: local (default, no key) or a hosted API.").addHelpText(
4144
+ "after",
4145
+ `
4146
+ Examples:
4147
+ codebuddy models use local
4148
+ codebuddy models use openrouter # ${DEFAULT_CLOUD_EMBEDDING_MODELS.openrouter}, needs OPENROUTER_API_KEY
4149
+ codebuddy models use openrouter --model nvidia/nemotron-3.5-embed-1b:free
4150
+ codebuddy models use huggingface # ${DEFAULT_CLOUD_EMBEDDING_MODELS.huggingface}, needs HF_TOKEN`
4151
+ ).action(async (choice, opts) => {
4152
+ await runSafely2(async () => {
4153
+ if (!EMBEDDERS.includes(choice)) {
4154
+ throw new Error(`Unknown provider "${choice}". Use one of: ${EMBEDDERS.join(", ")}.`);
4155
+ }
4156
+ const type = choice;
4157
+ if (type === "local" && opts.model) {
4158
+ throw new Error(
4159
+ "The local provider has one built-in model; --model is for hosted providers."
4160
+ );
4161
+ }
4162
+ const current = (await readConfigFile()).provider;
4163
+ await setConfigBlock("provider", {
4164
+ type,
4165
+ ...current?.apiKey ? { apiKey: current.apiKey } : {},
4166
+ ...opts.model ? { model: opts.model } : {},
4167
+ ...opts.floor !== void 0 ? { relevanceFloor: opts.floor } : {}
4168
+ });
4169
+ const model = type === "local" ? LOCAL_EMBEDDING_MODEL.id : opts.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS[type];
4170
+ console.log(ok(`Embeddings: ${type} ${pc13.dim(model)}`));
4171
+ const missingKey = type !== "local" && !keyPresent(type, current?.apiKey);
4172
+ if (missingKey) {
4173
+ console.log(
4174
+ warn(
4175
+ `Set ${KEY_ENV2[type]} in your shell (keys are never stored in config), then run ${pc13.cyan("codebuddy hooks sync")}.`
4176
+ )
4177
+ );
4178
+ console.log(pc13.dim(" Existing vectors keep working until the new model can embed."));
4179
+ }
4180
+ if (opts.floor === void 0 && getModelDefinition(model)?.relevanceFloor === void 0) {
4181
+ console.log(
4182
+ warn(
4183
+ `No floor for ${model} yet: run ${pc13.cyan("codebuddy models calibrate")} to turn on dense recall.`
4184
+ )
4185
+ );
4186
+ }
4187
+ if (!missingKey) {
4188
+ syncInBackground(process.cwd());
4189
+ console.log(
4190
+ pc13.dim(" Re-embedding existing memory in the background (codebuddy hooks sync).")
4191
+ );
4192
+ }
4193
+ });
4194
+ });
4195
+ models.command("rerank").argument("<provider>", `Reranker: ${RERANKERS.join(" | ")}.`).option("--model <id>", "Cross-encoder model id.").description("Optionally reorder recalled facts with a cross-encoder (off by default).").addHelpText(
4196
+ "after",
4197
+ `
4198
+ Defaults: local ${DEFAULT_RERANK_MODELS.local.id} \xB7 huggingface ${DEFAULT_RERANK_MODELS.huggingface.id}
4199
+ openrouter ${DEFAULT_RERANK_MODELS.openrouter.id} (paid per search)
4200
+ In CodeBuddy's eval, no reranker beat the dense order; measure on your data
4201
+ before keeping one on. A reranker never admits a fact, it only orders them.`
4202
+ ).action(async (choice, opts) => {
4203
+ await runSafely2(async () => {
4204
+ if (!RERANKERS.includes(choice)) {
4205
+ throw new Error(`Unknown reranker "${choice}". Use one of: ${RERANKERS.join(", ")}.`);
4206
+ }
4207
+ await setConfigBlock("rerank", {
4208
+ provider: choice,
4209
+ ...opts.model ? { model: opts.model } : {}
4210
+ });
4211
+ if (choice === "off") {
4212
+ console.log(ok("Reranker off: facts keep their fused (keyword + vector) order."));
4213
+ return;
4214
+ }
4215
+ const reranker = choice;
4216
+ console.log(
4217
+ ok(`Reranker: ${reranker} ${pc13.dim(opts.model ?? DEFAULT_RERANK_MODELS[reranker].id)}`)
4218
+ );
4219
+ if (reranker !== "local" && !keyPresent(reranker, (await readConfigFile()).provider?.apiKey)) {
4220
+ console.log(warn(`Set ${KEY_ENV2[reranker]}; without it reranking is skipped.`));
4221
+ }
4222
+ if (reranker === "local")
4223
+ console.log(pc13.dim(" Run codebuddy models pull to download it now."));
4224
+ });
4225
+ });
4226
+ models.command("pull").description("Download the local models now instead of on first use.").action(async () => {
4227
+ await runSafely2(async () => {
4228
+ const { LocalModelProvider, rerankLocally } = await import("./local-3NGNESNB.js");
4229
+ const file = await readConfigFile();
4230
+ let started = Date.now();
4231
+ await withSpinner(
4232
+ `Downloading ${LOCAL_EMBEDDING_MODEL.id}`,
4233
+ () => new LocalModelProvider().embed({ input: "warm up" }),
4234
+ () => ok(`${LOCAL_EMBEDDING_MODEL.id} ready ${pc13.dim(`${Date.now() - started} ms`)}`)
4235
+ );
4236
+ if (file.rerank?.provider === "local") {
4237
+ const id = file.rerank.model ?? DEFAULT_RERANK_MODELS.local.id;
4238
+ const revision = id === DEFAULT_RERANK_MODELS.local.id ? DEFAULT_RERANK_MODELS.local.revision : void 0;
4239
+ started = Date.now();
4240
+ await withSpinner(
4241
+ `Downloading ${id}`,
4242
+ () => rerankLocally({ id, ...revision ? { revision } : {} }, "warm", ["up"]),
4243
+ () => ok(`${id} ready ${pc13.dim(`${Date.now() - started} ms`)}`)
4244
+ );
4245
+ }
4246
+ console.log(pc13.dim(` Cache: ${modelCacheDir()}`));
4247
+ });
4248
+ });
4249
+ models.command("calibrate").option("--dry-run", "Measure only; do not save the floor.").description(
4250
+ "Measure the configured embedder on a built-in task set and save its recall floor."
4251
+ ).action(async (opts) => {
4252
+ await runSafely2(async () => {
4253
+ const config = await loadRuntimeConfig(process.cwd(), { requireDatabase: false });
4254
+ const embedder = buildProviderFromConfig(config.provider);
4255
+ const result = await withSpinner(
4256
+ `Embedding 44 calibration texts with ${embedder.space?.model ?? config.provider.type}`,
4257
+ () => calibrateEmbedder(async (texts) => (await embedder.embed({ input: texts })).vectors),
4258
+ () => ok("Measured")
4259
+ );
4260
+ const pct = (value) => `${Math.round(value * 100)}%`;
4261
+ console.log(
4262
+ row(
4263
+ "ranking",
4264
+ `AUC ${result.auc.toFixed(3)} \xB7 right fact first ${pct(result.recallAt1)}`
4265
+ )
4266
+ );
4267
+ console.log(
4268
+ row(
4269
+ "floor",
4270
+ `${result.best.floor}: precision ${pct(result.best.precision)}, recall ${pct(result.best.recall)}, silent on ${pct(result.best.abstain)} of unrelated tasks`
4271
+ )
4272
+ );
4273
+ if (opts.dryRun) return;
4274
+ const file = await readConfigFile();
4275
+ await setConfigBlock("provider", {
4276
+ ...file.provider ?? { type: config.provider.type },
4277
+ relevanceFloor: result.best.floor
4278
+ });
4279
+ console.log(ok(`Saved relevanceFloor ${result.best.floor} to .codebuddy/config.json`));
4280
+ });
4281
+ });
4282
+ }
4283
+
3354
4284
  // src/cli/commands/plan.ts
3355
- import { spawn as spawn4 } from "child_process";
4285
+ import { spawn as spawn6 } from "child_process";
3356
4286
  import { relative as relative4 } from "path";
3357
- import pc12 from "picocolors";
4287
+ import pc14 from "picocolors";
3358
4288
  function makeLifecycle(cwd = process.cwd()) {
3359
4289
  const store = new PlanFileStore(cwd);
3360
4290
  return { store, lifecycle: new PlanLifecycle(store) };
@@ -3362,67 +4292,67 @@ function makeLifecycle(cwd = process.cwd()) {
3362
4292
  function statusColor(status) {
3363
4293
  switch (status) {
3364
4294
  case "draft":
3365
- return pc12.dim(status);
4295
+ return pc14.dim(status);
3366
4296
  case "approved":
3367
- return pc12.cyan(status);
4297
+ return pc14.cyan(status);
3368
4298
  case "executing":
3369
- return pc12.yellow(status);
4299
+ return pc14.yellow(status);
3370
4300
  case "complete":
3371
- return pc12.green(status);
4301
+ return pc14.green(status);
3372
4302
  case "abandoned":
3373
- return pc12.red(status);
4303
+ return pc14.red(status);
3374
4304
  }
3375
4305
  }
3376
4306
  function renderPlan(plan) {
3377
4307
  const lines = [];
3378
- lines.push(`${pc12.bold(plan.title)} ${statusColor(plan.status)}`);
3379
- lines.push(pc12.dim(plan.id));
4308
+ lines.push(`${pc14.bold(plan.title)} ${statusColor(plan.status)}`);
4309
+ lines.push(pc14.dim(plan.id));
3380
4310
  lines.push("");
3381
- lines.push(`${pc12.bold("Brief:")} ${plan.brief}`);
4311
+ lines.push(`${pc14.bold("Brief:")} ${plan.brief}`);
3382
4312
  if (plan.filesToTouch.length > 0) {
3383
4313
  lines.push("");
3384
- lines.push(pc12.bold("Files to touch:"));
4314
+ lines.push(pc14.bold("Files to touch:"));
3385
4315
  for (const file of plan.filesToTouch) {
3386
4316
  lines.push(` ${file.action.padEnd(6)} ${file.path}${file.notes ? ` \u2014 ${file.notes}` : ""}`);
3387
4317
  }
3388
4318
  }
3389
4319
  if (plan.testsToAdd.length > 0) {
3390
4320
  lines.push("");
3391
- lines.push(pc12.bold("Tests to add:"));
4321
+ lines.push(pc14.bold("Tests to add:"));
3392
4322
  for (const test of plan.testsToAdd) {
3393
4323
  lines.push(` ${test.path}${test.description ? ` \u2014 ${test.description}` : ""}`);
3394
4324
  }
3395
4325
  }
3396
4326
  if (plan.outOfScope.length > 0) {
3397
4327
  lines.push("");
3398
- lines.push(pc12.bold("Out of scope:"));
4328
+ lines.push(pc14.bold("Out of scope:"));
3399
4329
  for (const item of plan.outOfScope) lines.push(` - ${item}`);
3400
4330
  }
3401
4331
  if (plan.risks.length > 0) {
3402
4332
  lines.push("");
3403
- lines.push(pc12.bold("Risks:"));
4333
+ lines.push(pc14.bold("Risks:"));
3404
4334
  for (const item of plan.risks) lines.push(` - ${item}`);
3405
4335
  }
3406
4336
  if (plan.commitSha) {
3407
4337
  lines.push("");
3408
- lines.push(`${pc12.bold("Commit:")} ${plan.commitSha}`);
4338
+ lines.push(`${pc14.bold("Commit:")} ${plan.commitSha}`);
3409
4339
  }
3410
4340
  lines.push("");
3411
- lines.push(pc12.dim("\u2500".repeat(40)));
4341
+ lines.push(pc14.dim("\u2500".repeat(40)));
3412
4342
  lines.push(plan.body);
3413
4343
  return lines.join("\n");
3414
4344
  }
3415
4345
  function openEditor2(path) {
3416
4346
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3417
4347
  return new Promise((resolve14, reject) => {
3418
- const child = spawn4(editor, [path], { stdio: "inherit" });
4348
+ const child = spawn6(editor, [path], { stdio: "inherit" });
3419
4349
  child.on("error", reject);
3420
4350
  child.on("close", () => resolve14());
3421
4351
  });
3422
4352
  }
3423
4353
  function runGit(args) {
3424
4354
  return new Promise((resolve14, reject) => {
3425
- const child = spawn4("git", args, { stdio: ["ignore", "pipe", "pipe"] });
4355
+ const child = spawn6("git", args, { stdio: ["ignore", "pipe", "pipe"] });
3426
4356
  let stdout = "";
3427
4357
  let stderr = "";
3428
4358
  child.stdout.on("data", (c) => {
@@ -3447,14 +4377,14 @@ function registerPlanCommands(program, runSafely2) {
3447
4377
  title: opts.title ?? brief,
3448
4378
  brief
3449
4379
  });
3450
- console.log(`${pc12.green("created")} ${created.id} (${statusColor(created.status)})`);
4380
+ console.log(`${pc14.green("created")} ${created.id} (${statusColor(created.status)})`);
3451
4381
  if (opts.edit !== false && process.stdin.isTTY) {
3452
4382
  await openEditor2(created.path);
3453
4383
  const { store } = makeLifecycle();
3454
4384
  await store.readPlan(created.id);
3455
- console.log(pc12.dim("saved and validated"));
4385
+ console.log(pc14.dim("saved and validated"));
3456
4386
  }
3457
- console.log(pc12.dim(relative4(process.cwd(), created.path)));
4387
+ console.log(pc14.dim(relative4(process.cwd(), created.path)));
3458
4388
  });
3459
4389
  });
3460
4390
  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) => {
@@ -3466,11 +4396,11 @@ function registerPlanCommands(program, runSafely2) {
3466
4396
  else if (!opts.all)
3467
4397
  plans = plans.filter((p) => p.status !== "complete" && p.status !== "abandoned");
3468
4398
  if (plans.length === 0) {
3469
- console.log(pc12.dim("no plans"));
4399
+ console.log(pc14.dim("no plans"));
3470
4400
  return;
3471
4401
  }
3472
4402
  for (const p of plans) {
3473
- console.log(`${statusColor(p.status).padEnd(20)} ${pc12.bold(p.title)} ${pc12.dim(p.id)}`);
4403
+ console.log(`${statusColor(p.status).padEnd(20)} ${pc14.bold(p.title)} ${pc14.dim(p.id)}`);
3474
4404
  }
3475
4405
  });
3476
4406
  });
@@ -3486,7 +4416,7 @@ function registerPlanCommands(program, runSafely2) {
3486
4416
  const existing = await store.readPlan(id);
3487
4417
  await openEditor2(existing.path);
3488
4418
  await store.readPlan(id);
3489
- console.log(pc12.dim("saved and validated"));
4419
+ console.log(pc14.dim("saved and validated"));
3490
4420
  });
3491
4421
  });
3492
4422
  plan.command("approve").argument("<id>").description("Promote a draft to approved (one active plan per namespace).").action(async (id) => {
@@ -3510,7 +4440,7 @@ function registerPlanCommands(program, runSafely2) {
3510
4440
  ...opts.commit ? { commitSha: opts.commit } : {},
3511
4441
  ...opts.notes ? { notes: opts.notes } : {}
3512
4442
  });
3513
- if (!opts.commit) console.log(pc12.yellow("note: no --commit supplied"));
4443
+ if (!opts.commit) console.log(pc14.yellow("note: no --commit supplied"));
3514
4444
  console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
3515
4445
  });
3516
4446
  });
@@ -3529,7 +4459,7 @@ function registerPlanCommands(program, runSafely2) {
3529
4459
  const args = ["diff", opts.from, ...opts.to ? [opts.to] : [], "--", relPath];
3530
4460
  const result = await runGit(args);
3531
4461
  if (result.stdout.trim().length === 0) {
3532
- console.log(pc12.dim("no changes"));
4462
+ console.log(pc14.dim("no changes"));
3533
4463
  } else {
3534
4464
  process.stdout.write(result.stdout);
3535
4465
  }
@@ -3546,13 +4476,13 @@ function registerPlanCommands(program, runSafely2) {
3546
4476
  throw new Error(`Invalid policy "${mode}". Use one of: ${PLAN_POLICIES.join(", ")}.`);
3547
4477
  }
3548
4478
  await setPlanPolicy(mode);
3549
- console.log(`${pc12.green("plan policy")} = ${mode}`);
4479
+ console.log(`${pc14.green("plan policy")} = ${mode}`);
3550
4480
  });
3551
4481
  });
3552
4482
  }
3553
4483
 
3554
4484
  // src/cli/commands/risk.ts
3555
- import pc13 from "picocolors";
4485
+ import pc15 from "picocolors";
3556
4486
  function registerRiskCommands(program, runSafely2) {
3557
4487
  const risk = program.command("risk").description("Assess evidence-backed change risk.");
3558
4488
  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(
@@ -3580,31 +4510,31 @@ function parsePaths3(value) {
3580
4510
  }
3581
4511
  function printAssessment(assessment, explain) {
3582
4512
  if (assessment.items.length === 0) {
3583
- console.log(pc13.dim("no risk evidence found"));
4513
+ console.log(pc15.dim("no risk evidence found"));
3584
4514
  return;
3585
4515
  }
3586
4516
  for (const item of assessment.items) {
3587
4517
  console.log(
3588
- `${pc13.bold(item.path)} score ${scoreColor2(item.score)} category ${pc13.cyan(item.category)}`
4518
+ `${pc15.bold(item.path)} score ${scoreColor2(item.score)} category ${pc15.cyan(item.category)}`
3589
4519
  );
3590
4520
  console.log(` ${item.reason}`);
3591
4521
  if (explain) {
3592
4522
  for (const evidence of item.evidence) console.log(` \u21B3 ${formatEvidence(evidence)}`);
3593
4523
  } else {
3594
- console.log(pc13.dim(` ${item.evidence.length} evidence item(s)`));
4524
+ console.log(pc15.dim(` ${item.evidence.length} evidence item(s)`));
3595
4525
  }
3596
4526
  }
3597
4527
  console.log(
3598
- pc13.dim(
4528
+ pc15.dim(
3599
4529
  `
3600
4530
  ${assessment.stats.itemsProduced} item(s), ${assessment.stats.signalsRun} signal(s), ${assessment.stats.pathsConsidered} path(s) considered`
3601
4531
  )
3602
4532
  );
3603
4533
  }
3604
4534
  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));
4535
+ if (score >= 75) return pc15.red(String(score));
4536
+ if (score >= 50) return pc15.yellow(String(score));
4537
+ return pc15.green(String(score));
3608
4538
  }
3609
4539
  function formatEvidence(evidence) {
3610
4540
  switch (evidence.kind) {
@@ -3626,7 +4556,7 @@ function truncate2(value, max) {
3626
4556
 
3627
4557
  // src/cli/commands/rules.ts
3628
4558
  import { relative as relative5 } from "path";
3629
- import pc14 from "picocolors";
4559
+ import pc16 from "picocolors";
3630
4560
  async function resolveMode(client, value) {
3631
4561
  if (value) {
3632
4562
  const mode = parseMode(value);
@@ -3666,7 +4596,7 @@ function registerRulesCommands(program, runSafely2) {
3666
4596
  `${actionLabel(result.action)} ${clientDisplayName(client)} workflow in ${where}`
3667
4597
  );
3668
4598
  if (result.action !== "unchanged") {
3669
- console.log(pc14.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
4599
+ console.log(pc16.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
3670
4600
  }
3671
4601
  });
3672
4602
  });
@@ -3674,20 +4604,20 @@ function registerRulesCommands(program, runSafely2) {
3674
4604
  function actionLabel(action) {
3675
4605
  switch (action) {
3676
4606
  case "created":
3677
- return pc14.green("created");
4607
+ return pc16.green("created");
3678
4608
  case "added":
3679
- return pc14.green("added");
4609
+ return pc16.green("added");
3680
4610
  case "updated":
3681
- return pc14.cyan("updated");
4611
+ return pc16.cyan("updated");
3682
4612
  case "removed":
3683
- return pc14.yellow("removed");
4613
+ return pc16.yellow("removed");
3684
4614
  default:
3685
- return pc14.dim("unchanged");
4615
+ return pc16.dim("unchanged");
3686
4616
  }
3687
4617
  }
3688
4618
 
3689
4619
  // src/cli/commands/suggest.ts
3690
- import pc15 from "picocolors";
4620
+ import pc17 from "picocolors";
3691
4621
  function parsePaths4(value) {
3692
4622
  return value.split(",").map((path) => path.trim()).filter(Boolean);
3693
4623
  }
@@ -3709,7 +4639,7 @@ function registerSuggestCommand(program, runSafely2) {
3709
4639
  }
3710
4640
  printSuggestions(result.suggestions);
3711
4641
  console.log(
3712
- pc15.dim(
4642
+ pc17.dim(
3713
4643
  `
3714
4644
  ${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} path(s)`
3715
4645
  )
@@ -3720,40 +4650,40 @@ ${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} pa
3720
4650
  }
3721
4651
  function printSuggestions(suggestions) {
3722
4652
  if (suggestions.length === 0) {
3723
- console.log(pc15.dim("no suggestions \u2014 nothing high-signal to report"));
4653
+ console.log(pc17.dim("no suggestions \u2014 nothing high-signal to report"));
3724
4654
  return;
3725
4655
  }
3726
4656
  for (const suggestion of suggestions) {
3727
- console.log(`${severityBadge2(suggestion.severity)} ${pc15.bold(suggestion.title)}`);
4657
+ console.log(`${severityBadge2(suggestion.severity)} ${pc17.bold(suggestion.title)}`);
3728
4658
  console.log(` ${suggestion.detail}`);
3729
4659
  for (const evidence of suggestion.evidence) {
3730
- console.log(pc15.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
4660
+ console.log(pc17.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
3731
4661
  }
3732
- if (suggestion.commandHint) console.log(pc15.dim(` \u2192 ${suggestion.commandHint}`));
4662
+ if (suggestion.commandHint) console.log(pc17.dim(` \u2192 ${suggestion.commandHint}`));
3733
4663
  }
3734
4664
  }
3735
4665
  function severityBadge2(severity) {
3736
4666
  switch (severity) {
3737
4667
  case "high":
3738
- return pc15.red("[high]");
4668
+ return pc17.red("[high]");
3739
4669
  case "medium":
3740
- return pc15.yellow("[med ]");
4670
+ return pc17.yellow("[med ]");
3741
4671
  case "low":
3742
- return pc15.cyan("[low ]");
4672
+ return pc17.cyan("[low ]");
3743
4673
  default:
3744
- return pc15.dim("[info]");
4674
+ return pc17.dim("[info]");
3745
4675
  }
3746
4676
  }
3747
4677
 
3748
4678
  // src/cli/commands/symbols.ts
3749
4679
  import { readFile as readFile13 } from "fs/promises";
3750
4680
  import { resolve as resolve13 } from "path";
3751
- import pc16 from "picocolors";
4681
+ import pc18 from "picocolors";
3752
4682
  function registerSymbolsCommand(program, runSafely2) {
3753
4683
  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
4684
  await runSafely2(async () => {
3755
4685
  if (!supportsSymbols(path)) {
3756
- console.log(pc16.dim(`no symbol extraction for ${path} (unsupported language)`));
4686
+ console.log(pc18.dim(`no symbol extraction for ${path} (unsupported language)`));
3757
4687
  return;
3758
4688
  }
3759
4689
  const content = await readFile13(resolve13(path), "utf8");
@@ -3763,30 +4693,30 @@ function registerSymbolsCommand(program, runSafely2) {
3763
4693
  return;
3764
4694
  }
3765
4695
  if (table.length === 0) {
3766
- console.log(pc16.dim("no top-level symbols found"));
4696
+ console.log(pc18.dim("no top-level symbols found"));
3767
4697
  return;
3768
4698
  }
3769
4699
  for (const symbol of table) {
3770
- const badge = symbol.exported ? pc16.green("export") : pc16.dim("local ");
4700
+ const badge = symbol.exported ? pc18.green("export") : pc18.dim("local ");
3771
4701
  console.log(
3772
- `${badge} ${pc16.cyan(symbol.kind.padEnd(9))} ${pc16.bold(symbol.name)} ${pc16.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
4702
+ `${badge} ${pc18.cyan(symbol.kind.padEnd(9))} ${pc18.bold(symbol.name)} ${pc18.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
3773
4703
  );
3774
4704
  }
3775
- console.log(pc16.dim(`
4705
+ console.log(pc18.dim(`
3776
4706
  ${table.length} symbol(s)`));
3777
4707
  });
3778
4708
  });
3779
4709
  }
3780
4710
 
3781
4711
  // 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";
4712
+ import { basename as basename2 } from "path";
4713
+ import { confirm as confirm2, isCancel as isCancel2, log as log2, note as note2, password as password2, select, spinner as spinner3, text as text2 } from "@clack/prompts";
4714
+ import pc19 from "picocolors";
3785
4715
 
3786
4716
  // src/core/global-config.ts
3787
4717
  import { chmod, mkdir as mkdir8, readFile as readFile14, writeFile as writeFile3 } from "fs/promises";
3788
4718
  import { homedir as homedir3 } from "os";
3789
- import { dirname as dirname12, join as join15 } from "path";
4719
+ import { dirname as dirname12, join as join16 } from "path";
3790
4720
  import { z as z2 } from "zod";
3791
4721
  var GLOBAL_DIR = ".codebuddy";
3792
4722
  var GLOBAL_FILE = "global.json";
@@ -3799,7 +4729,7 @@ var globalConfigSchema = z2.object({
3799
4729
  installPath: z2.string().optional()
3800
4730
  });
3801
4731
  function globalConfigPath(home = homedir3()) {
3802
- return join15(home, GLOBAL_DIR, GLOBAL_FILE);
4732
+ return join16(home, GLOBAL_DIR, GLOBAL_FILE);
3803
4733
  }
3804
4734
  async function readGlobalConfig(home = homedir3()) {
3805
4735
  const path = globalConfigPath(home);
@@ -3827,10 +4757,10 @@ async function mergeGlobalConfig(patch, home = homedir3()) {
3827
4757
  }
3828
4758
 
3829
4759
  // src/cli/commands/docker-autostart.ts
3830
- import { spawn as spawn5 } from "child_process";
4760
+ import { spawn as spawn7 } from "child_process";
3831
4761
  async function isDockerDaemonRunning() {
3832
4762
  return new Promise((resolve14) => {
3833
- const child = spawn5("docker", ["info"], { stdio: "ignore" });
4763
+ const child = spawn7("docker", ["info"], { stdio: "ignore" });
3834
4764
  child.on("error", () => resolve14(false));
3835
4765
  child.on("close", (code) => resolve14(code === 0));
3836
4766
  });
@@ -3838,7 +4768,7 @@ async function isDockerDaemonRunning() {
3838
4768
  async function tryStartDockerDesktop() {
3839
4769
  if (process.platform !== "darwin") return { started: false };
3840
4770
  return new Promise((resolve14) => {
3841
- const child = spawn5("open", ["-ga", "Docker"], { stdio: "ignore" });
4771
+ const child = spawn7("open", ["-ga", "Docker"], { stdio: "ignore" });
3842
4772
  child.on("error", () => resolve14({ started: false }));
3843
4773
  child.on("close", (code) => resolve14({ started: code === 0 }));
3844
4774
  });
@@ -3879,7 +4809,7 @@ async function runUseCommand(options = {}) {
3879
4809
  const sigintHandler = () => bail2();
3880
4810
  process.on("SIGINT", sigintHandler);
3881
4811
  try {
3882
- const folderName = basename(process.cwd());
4812
+ const folderName = basename2(process.cwd());
3883
4813
  const namespace = sanitizeNamespace(options.namespace ?? folderName);
3884
4814
  await initProjectScaffold();
3885
4815
  const graphifyEnabled = options.graphify ?? unwrap2(
@@ -3889,26 +4819,61 @@ async function runUseCommand(options = {}) {
3889
4819
  })
3890
4820
  );
3891
4821
  note2(
3892
- `Project folder: ${pc17.bold(process.cwd())}
3893
- Namespace: ${pc17.bold(namespace)}`,
4822
+ `Project folder: ${pc19.bold(process.cwd())}
4823
+ Namespace: ${pc19.bold(namespace)}`,
3894
4824
  "codebuddy use"
3895
4825
  );
3896
4826
  const stored = await readGlobalConfig();
4827
+ const projectConfig = await readConfigFile();
4828
+ let embeddings = projectConfig.provider?.type;
4829
+ if (embeddings) {
4830
+ log2.info(`Embeddings: ${embeddings} (change with ${pc19.cyan("codebuddy models use")}).`);
4831
+ } else {
4832
+ embeddings = unwrap2(
4833
+ await select({
4834
+ message: "Where should memory embeddings run?",
4835
+ initialValue: "local",
4836
+ options: [
4837
+ {
4838
+ value: "local",
4839
+ label: "On this machine",
4840
+ hint: "recommended \xB7 no key \xB7 ~90 MB once"
4841
+ },
4842
+ {
4843
+ value: "openrouter",
4844
+ label: "OpenRouter",
4845
+ hint: "OPENROUTER_API_KEY \xB7 free models listed"
4846
+ },
4847
+ {
4848
+ value: "huggingface",
4849
+ label: "Hugging Face",
4850
+ hint: "HF_TOKEN \xB7 small free monthly credit"
4851
+ }
4852
+ ]
4853
+ })
4854
+ );
4855
+ if (embeddings === "openrouter" && !process.env.OPENROUTER_API_KEY) {
4856
+ log2.warn(
4857
+ "Export OPENROUTER_API_KEY before your agent starts; keys are never stored in config."
4858
+ );
4859
+ }
4860
+ }
3897
4861
  let hfToken;
3898
- if (process.env.HF_TOKEN) {
4862
+ if (embeddings !== "huggingface") {
4863
+ hfToken = process.env.HF_TOKEN ?? stored.hfToken;
4864
+ } else if (process.env.HF_TOKEN) {
3899
4865
  hfToken = process.env.HF_TOKEN;
3900
4866
  log2.info("Using HF_TOKEN from environment.");
3901
4867
  } else if (stored.hfToken) {
3902
4868
  hfToken = stored.hfToken;
3903
- log2.info(`Reusing saved Hugging Face token (${pc17.dim("~/.codebuddy/global.json")}).`);
4869
+ log2.info(`Reusing saved Hugging Face token (${pc19.dim("~/.codebuddy/global.json")}).`);
3904
4870
  } else {
3905
4871
  hfToken = unwrap2(
3906
4872
  await password2({
3907
- message: "Hugging Face token (optional; press Enter for local-only mode)"
4873
+ message: "Hugging Face token (for hosted embeddings)"
3908
4874
  })
3909
4875
  );
3910
- if (!hfToken)
3911
- log2.info("No Hugging Face token supplied; local file context remains available.");
4876
+ if (!hfToken) log2.warn("No token: hosted embeddings will fail until HF_TOKEN is set.");
3912
4877
  }
3913
4878
  const initialUrl = options.postgresUrl ?? process.env.DATABASE_URL ?? stored.databaseUrl ?? DEFAULT_DB_URL;
3914
4879
  const databaseUrl = unwrap2(
@@ -3918,7 +4883,7 @@ Namespace: ${pc17.bold(namespace)}`,
3918
4883
  validate: (value) => value && value.length > 0 ? void 0 : "Postgres URL is required."
3919
4884
  })
3920
4885
  );
3921
- const dbSpinner = spinner2();
4886
+ const dbSpinner = spinner3();
3922
4887
  dbSpinner.start("Checking Postgres");
3923
4888
  let connected = await pingOnce2(databaseUrl);
3924
4889
  if (connected) dbSpinner.stop("Local Postgres reachable.");
@@ -3933,7 +4898,7 @@ Namespace: ${pc17.bold(namespace)}`,
3933
4898
  ));
3934
4899
  if (shouldUseDocker) {
3935
4900
  if (!await isDockerDaemonRunning()) {
3936
- const dockerSpinner = spinner2();
4901
+ const dockerSpinner = spinner3();
3937
4902
  dockerSpinner.start("Starting Docker Desktop");
3938
4903
  await tryStartDockerDesktop();
3939
4904
  const ready = await waitForDockerDaemon(6e4);
@@ -3943,7 +4908,7 @@ Namespace: ${pc17.bold(namespace)}`,
3943
4908
  }
3944
4909
  dockerSpinner.stop("Docker daemon is up.");
3945
4910
  }
3946
- const composeSpinner = spinner2();
4911
+ const composeSpinner = spinner3();
3947
4912
  composeSpinner.start("docker compose up -d");
3948
4913
  try {
3949
4914
  const result = await postgresUp();
@@ -3970,7 +4935,7 @@ Namespace: ${pc17.bold(namespace)}`,
3970
4935
  bail2();
3971
4936
  }
3972
4937
  if (!connected) bail2();
3973
- const migrateSpinner = spinner2();
4938
+ const migrateSpinner = spinner3();
3974
4939
  migrateSpinner.start("Enabling pgvector + applying migrations");
3975
4940
  {
3976
4941
  const client = createDatabaseClient({ postgresUrl: databaseUrl });
@@ -3991,7 +4956,7 @@ Namespace: ${pc17.bold(namespace)}`,
3991
4956
  installPath: process.argv[1] ?? void 0
3992
4957
  });
3993
4958
  const existingConfig = await readConfigFile();
3994
- const indexSpinner = spinner2();
4959
+ const indexSpinner = spinner3();
3995
4960
  indexSpinner.start("Indexing the existing repository");
3996
4961
  try {
3997
4962
  const result = await indexRepository({ repositoryRoot: process.cwd() });
@@ -4005,7 +4970,11 @@ Namespace: ${pc17.bold(namespace)}`,
4005
4970
  ...existingConfig,
4006
4971
  postgresUrl: databaseUrl,
4007
4972
  namespace,
4008
- provider: { type: "huggingface", ...hfToken ? { apiKey: hfToken } : {} },
4973
+ provider: {
4974
+ ...existingConfig.provider,
4975
+ type: embeddings,
4976
+ ...hfToken ? { apiKey: hfToken } : {}
4977
+ },
4009
4978
  tokenBudget: existingConfig.tokenBudget ?? 4e3,
4010
4979
  graphify: { enabled: graphifyConfigured, graphPath: "graphify-out/graph.json" }
4011
4980
  });
@@ -4023,11 +4992,11 @@ Namespace: ${pc17.bold(namespace)}`,
4023
4992
  const install = await installClaudeEntry({
4024
4993
  namespace,
4025
4994
  projectRoot: process.cwd(),
4026
- hfToken,
4995
+ ...hfToken ? { hfToken } : {},
4027
4996
  databaseUrl
4028
4997
  });
4029
4998
  log2.success(
4030
- `Registered ${pc17.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
4999
+ `Registered ${pc19.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
4031
5000
  );
4032
5001
  } catch (error) {
4033
5002
  log2.warn(`Claude Desktop registration skipped: ${error.message}`);
@@ -4038,11 +5007,11 @@ Namespace: ${pc17.bold(namespace)}`,
4038
5007
  const install = await installCodexEntry({
4039
5008
  namespace,
4040
5009
  projectRoot: process.cwd(),
4041
- hfToken,
5010
+ ...hfToken ? { hfToken } : {},
4042
5011
  databaseUrl
4043
5012
  });
4044
5013
  log2.success(
4045
- `Registered ${pc17.bold(install.serverKey)} in Codex. Restart Codex to activate.`
5014
+ `Registered ${pc19.bold(install.serverKey)} in Codex. Restart Codex to activate.`
4046
5015
  );
4047
5016
  } catch (error) {
4048
5017
  log2.warn(`Codex registration skipped: ${error.message}`);
@@ -4050,24 +5019,24 @@ Namespace: ${pc17.bold(namespace)}`,
4050
5019
  }
4051
5020
  note2(
4052
5021
  [
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")}.`,
5022
+ `${pc19.green("\u2713")} CodeBuddy is ready for ${pc19.bold(folderName)}.`,
5023
+ `${pc19.green("\u2713")} Scaffolded ${pc19.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
5024
+ `${pc19.green("\u2713")} ${graphifyMessage}`,
5025
+ `${pc19.green("\u2713")} Added agent workflow instructions to ${pc19.cyan("CLAUDE.md")} and ${pc19.cyan("AGENTS.md")}.`,
4057
5026
  ``,
4058
5027
  `Next:`,
4059
5028
  ` 1. Restart Claude Desktop and Codex.`,
4060
- ` 2. Ask Claude or Codex to use the ${pc17.cyan("context_pack")} tool for each coding task.`,
5029
+ ` 2. Ask Claude or Codex to use the ${pc19.cyan("context_pack")} tool for each coding task.`,
4061
5030
  ` 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.`,
5031
+ ` 4. Run ${pc19.cyan("codebuddy doctor")} or ${pc19.cyan("codebuddy db doctor")} if you want a full health check.`,
4063
5032
  ``,
4064
5033
  `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")}`,
5034
+ ` codebuddy claude list ${pc19.dim("# see every project wired up")}`,
5035
+ ` codebuddy codex list ${pc19.dim("# see Codex entries")}`,
5036
+ ` codebuddy db test ${pc19.dim("# quick Postgres connectivity check")}`,
4068
5037
  ` codebuddy claude remove codebuddy-${namespace}`,
4069
5038
  ` codebuddy codex remove codebuddy-${namespace}`,
4070
- ` codebuddy postgres down ${pc17.dim("# stop the DB (data preserved)")}`
5039
+ ` codebuddy postgres down ${pc19.dim("# stop the DB (data preserved)")}`
4071
5040
  ].join("\n"),
4072
5041
  "Done"
4073
5042
  );
@@ -4078,7 +5047,7 @@ Namespace: ${pc17.bold(namespace)}`,
4078
5047
 
4079
5048
  // src/cli/commands/worklog.ts
4080
5049
  import { relative as relative6 } from "path";
4081
- import pc18 from "picocolors";
5050
+ import pc20 from "picocolors";
4082
5051
  function registerWorklogCommands(program, runSafely2) {
4083
5052
  const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
4084
5053
  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) => {
@@ -4089,7 +5058,7 @@ function registerWorklogCommands(program, runSafely2) {
4089
5058
  return;
4090
5059
  }
4091
5060
  if (entries.length === 0) {
4092
- console.log(pc18.dim("no worklog entries yet"));
5061
+ console.log(pc20.dim("no worklog entries yet"));
4093
5062
  return;
4094
5063
  }
4095
5064
  for (const entry of entries) console.log(renderRow2(entry));
@@ -4103,36 +5072,36 @@ function registerWorklogCommands(program, runSafely2) {
4103
5072
  worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
4104
5073
  await runSafely2(async () => {
4105
5074
  const [entry] = await new WorklogStore().list();
4106
- console.log(entry ? renderEntry(entry) : pc18.dim("no worklog entries yet"));
5075
+ console.log(entry ? renderEntry(entry) : pc20.dim("no worklog entries yet"));
4107
5076
  });
4108
5077
  });
4109
5078
  }
4110
5079
  function renderRow2(entry) {
4111
5080
  const summary = entry.summary.replace(/\s+/g, " ");
4112
5081
  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))`)}`;
5082
+ return `${pc20.dim(entry.createdAt.slice(0, 16))} ${pc20.dim(entry.id)} ${short} ${pc20.dim(`(${entry.files.length} file(s))`)}`;
4114
5083
  }
4115
5084
  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(", ")}`);
5085
+ const lines = [`${pc20.bold(entry.id)} ${pc20.dim(entry.createdAt)} ${pc20.dim(entry.namespace)}`];
5086
+ if (entry.sourceSessionId) lines.push(`${pc20.bold("session:")} ${entry.sourceSessionId}`);
5087
+ if (entry.sourcePlanId) lines.push(`${pc20.bold("plan:")} ${entry.sourcePlanId}`);
5088
+ if (entry.commitSha) lines.push(`${pc20.bold("commit:")} ${entry.commitSha}`);
5089
+ if (entry.files.length > 0) lines.push(`${pc20.bold("files:")} ${entry.files.join(", ")}`);
4121
5090
  if (entry.verification.length > 0) {
4122
- lines.push(`${pc18.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
5091
+ lines.push(`${pc20.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
4123
5092
  }
4124
5093
  if (entry.savedMemory.length > 0) {
4125
- lines.push(`${pc18.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
5094
+ lines.push(`${pc20.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
4126
5095
  }
4127
5096
  if (entry.queuedForReview.length > 0) {
4128
- lines.push(`${pc18.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
5097
+ lines.push(`${pc20.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
4129
5098
  }
4130
- lines.push("", entry.summary, "", pc18.dim(relative6(process.cwd(), entry.path)));
5099
+ lines.push("", entry.summary, "", pc20.dim(relative6(process.cwd(), entry.path)));
4131
5100
  return lines.join("\n");
4132
5101
  }
4133
5102
 
4134
5103
  // src/cli/commands/zvec.ts
4135
- import pc19 from "picocolors";
5104
+ import pc21 from "picocolors";
4136
5105
  function registerZvecCommands(program, runSafely2) {
4137
5106
  const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
4138
5107
  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) => {
@@ -4142,22 +5111,22 @@ function registerZvecCommands(program, runSafely2) {
4142
5111
  console.log(JSON.stringify(report, null, 2));
4143
5112
  return;
4144
5113
  }
4145
- const mark = (ok) => ok ? pc19.green("yes") : pc19.yellow("no");
5114
+ const mark = (ok2) => ok2 ? pc21.green("yes") : pc21.yellow("no");
4146
5115
  console.log(`enabled: ${mark(report.enabled)}`);
4147
5116
  console.log(`binary: ${report.binary}`);
4148
5117
  console.log(
4149
- `version: ${report.version ?? pc19.yellow("not found")}${report.version && !report.supported ? pc19.yellow(" (unsupported; need 0.2.x)") : ""}`
5118
+ `version: ${report.version ?? pc21.yellow("not found")}${report.version && !report.supported ? pc21.yellow(" (unsupported; need 0.2.x)") : ""}`
4150
5119
  );
4151
5120
  console.log(`index ready: ${mark(report.indexReady)}`);
4152
5121
  console.log(
4153
- `zg server: ${report.server} ${pc19.dim("(running = faster queries; optional)")}`
5122
+ `zg server: ${report.server} ${pc21.dim("(running = faster queries; optional)")}`
4154
5123
  );
4155
5124
  console.log(`status: ${report.reason}`);
4156
- if (!report.version) console.log(pc19.dim("install: npm install -g @zvec/zvec-grep"));
5125
+ if (!report.version) console.log(pc21.dim("install: npm install -g @zvec/zvec-grep"));
4157
5126
  else if (report.supported && !report.indexReady)
4158
- console.log(pc19.dim("next: codebuddy zvec index"));
5127
+ console.log(pc21.dim("next: codebuddy zvec index"));
4159
5128
  else if (report.indexReady && !report.enabled)
4160
- console.log(pc19.dim("next: codebuddy zvec enable"));
5129
+ console.log(pc21.dim("next: codebuddy zvec enable"));
4161
5130
  });
4162
5131
  });
4163
5132
  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) => {
@@ -4168,7 +5137,7 @@ function registerZvecCommands(program, runSafely2) {
4168
5137
  });
4169
5138
  if (code !== 0) throw new Error(`zg index exited with code ${code}`);
4170
5139
  console.log(
4171
- pc19.dim("tip: add .zvec-grep/ to .gitignore; enable with `codebuddy zvec enable`")
5140
+ pc21.dim("tip: add .zvec-grep/ to .gitignore; enable with `codebuddy zvec enable`")
4172
5141
  );
4173
5142
  });
4174
5143
  });
@@ -4176,14 +5145,14 @@ function registerZvecCommands(program, runSafely2) {
4176
5145
  await runSafely2(async () => {
4177
5146
  await setZvecEnabled(true);
4178
5147
  console.log(
4179
- `${pc19.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
5148
+ `${pc21.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
4180
5149
  );
4181
5150
  });
4182
5151
  });
4183
5152
  zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
4184
5153
  await runSafely2(async () => {
4185
5154
  await setZvecEnabled(false);
4186
- console.log(`${pc19.yellow("disabled")} zvec-grep retrieval`);
5155
+ console.log(`${pc21.yellow("disabled")} zvec-grep retrieval`);
4187
5156
  });
4188
5157
  });
4189
5158
  }
@@ -4192,7 +5161,7 @@ function registerZvecCommands(program, runSafely2) {
4192
5161
  function createCli() {
4193
5162
  const program = new Command();
4194
5163
  program.name("codebuddy").description(
4195
- "Local-first MCP memory and automatic project-context engine for multi-agent coding."
5164
+ "Project memory and context for AI coding agents: recalls the right facts, files and risks on every prompt."
4196
5165
  ).version(VERSION);
4197
5166
  program.action(async () => {
4198
5167
  await runSafely(async () => {
@@ -4200,7 +5169,7 @@ function createCli() {
4200
5169
  });
4201
5170
  });
4202
5171
  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."
5172
+ "One-command setup for this folder: PostgreSQL (starts Docker if needed), migrations, embeddings, and agent wiring."
4204
5173
  ).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
5174
  "--docker",
4206
5175
  "Start CodeBuddy's bundled Postgres container when the default DB is unavailable."
@@ -4444,9 +5413,9 @@ function createCli() {
4444
5413
  });
4445
5414
  program.command("export").argument("<namespace>").description("Export facts from a namespace as JSON.").action(async (namespace) => {
4446
5415
  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 });
5416
+ const row2 = await runtime.repository.getNamespaceByName(namespace);
5417
+ if (!row2) throw new Error(`Namespace ${namespace} does not exist.`);
5418
+ const facts = await runtime.repository.listFacts({ namespaceId: row2.id, limit: 1e4 });
4450
5419
  console.log(JSON.stringify({ namespace, facts }, null, 2));
4451
5420
  });
4452
5421
  });
@@ -4486,14 +5455,64 @@ function createCli() {
4486
5455
  registerSuggestCommand(program, runSafely);
4487
5456
  registerRulesCommands(program, runSafely);
4488
5457
  registerHooksCommands(program, runSafely);
5458
+ registerModelsCommands(program, runSafely);
5459
+ registerDashboardCommand(program, runSafely);
4489
5460
  registerClientsCommands(program, runSafely);
4490
5461
  registerGitHubCommands(program, runSafely);
4491
5462
  registerSymbolsCommand(program, runSafely);
4492
5463
  program.action(async () => {
4493
5464
  await runSafely(() => runUseCommand());
4494
5465
  });
5466
+ groupHelp(program);
4495
5467
  return program;
4496
5468
  }
5469
+ var HELP_GROUPS = [
5470
+ ["Get started:", ["use", "init", "doctor", "hooks", "clients", "models", "dashboard"]],
5471
+ [
5472
+ "Daily work:",
5473
+ ["context", "plan", "change", "risk", "suggest", "memory", "worklog", "map", "symbols"]
5474
+ ],
5475
+ ["Indexing and search:", ["index", "watch", "reindex", "zvec", "graphify", "llm"]],
5476
+ [
5477
+ "Database:",
5478
+ [
5479
+ "postgres",
5480
+ "db",
5481
+ "migrate",
5482
+ "stats",
5483
+ "inspect",
5484
+ "namespaces",
5485
+ "list-facts",
5486
+ "export",
5487
+ "prune"
5488
+ ]
5489
+ ],
5490
+ ["Integrations:", ["serve", "claude", "codex", "github", "rules", "learn", "eval"]]
5491
+ ];
5492
+ function groupHelp(program) {
5493
+ const groupOf = new Map(
5494
+ HELP_GROUPS.flatMap(([group, names]) => names.map((name) => [name, group]))
5495
+ );
5496
+ for (const command of program.commands) {
5497
+ command.helpGroup(groupOf.get(command.name()) ?? "Other:");
5498
+ }
5499
+ const position = new Map(
5500
+ HELP_GROUPS.flatMap(([, names]) => names).map((name, index) => [name, index])
5501
+ );
5502
+ program.commands.sort(
5503
+ (a, b) => (position.get(a.name()) ?? 999) - (position.get(b.name()) ?? 999)
5504
+ );
5505
+ program.addHelpText(
5506
+ "after",
5507
+ `
5508
+ Quick start:
5509
+ codebuddy set up this project (PostgreSQL, hooks, agent)
5510
+ codebuddy models check the embedding model and memory index
5511
+ codebuddy dashboard live page of recalls, tool calls and tokens
5512
+ codebuddy context preview "fix the login redirect"
5513
+ see exactly what an agent would be given`
5514
+ );
5515
+ }
4497
5516
  async function withRuntime(fn) {
4498
5517
  await runSafely(async () => {
4499
5518
  const runtime = await createRuntime(await loadRuntimeConfig());
@@ -4508,7 +5527,7 @@ async function runSafely(fn) {
4508
5527
  try {
4509
5528
  await fn();
4510
5529
  } catch (error) {
4511
- console.error(pc20.red(String(redactSecrets(error.message))));
5530
+ console.error(pc22.red(String(redactSecrets(error.message))));
4512
5531
  process.exitCode = 1;
4513
5532
  }
4514
5533
  }
@@ -4522,19 +5541,19 @@ function parseDuration(value) {
4522
5541
  return amount * 60 * 1e3;
4523
5542
  }
4524
5543
  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}`);
5544
+ const ok2 = pc22.green("ok");
5545
+ const bad = pc22.red("fail");
5546
+ const warn2 = pc22.yellow("warn");
5547
+ console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok2 : bad}`);
4529
5548
  if (report.scaffold.codebuddyDir.warning)
4530
5549
  console.log(` ${report.scaffold.codebuddyDir.warning}`);
4531
- console.log(`policy scaffold: ${report.scaffold.policies.ok ? ok : bad}`);
5550
+ console.log(`policy scaffold: ${report.scaffold.policies.ok ? ok2 : bad}`);
4532
5551
  if (report.scaffold.policies.warning) console.log(` ${report.scaffold.policies.warning}`);
4533
- console.log(`DB connectivity: ${report.db.ok ? ok : bad}`);
5552
+ console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
4534
5553
  if (report.db.error) console.log(` ${report.db.error}`);
4535
- console.log(`pgvector: ${report.pgvector.ok ? ok : bad}`);
5554
+ console.log(`pgvector: ${report.pgvector.ok ? ok2 : bad}`);
4536
5555
  if (report.pgvector.error) console.log(` ${report.pgvector.error}`);
4537
- console.log(`namespace: ${report.namespace.ok ? ok : warn}`);
5556
+ console.log(`namespace: ${report.namespace.ok ? ok2 : warn2}`);
4538
5557
  console.log(
4539
5558
  ` runtime=${report.namespace.runtime ?? "unset"} default=${report.namespace.expectedDefault}`
4540
5559
  );
@@ -4547,9 +5566,9 @@ function printDoctor(report) {
4547
5566
  );
4548
5567
  console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
4549
5568
  if (report.usage.warning80Percent)
4550
- console.log(pc20.yellow("usage warning: daily cap is over 80% used"));
5569
+ console.log(pc22.yellow("usage warning: daily cap is over 80% used"));
4551
5570
  console.log(
4552
- `config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok : warn}`
5571
+ `config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok2 : warn2}`
4553
5572
  );
4554
5573
  if (report.config.warning) console.log(` ${report.config.warning}`);
4555
5574
  for (const model of report.models) {
@@ -4557,19 +5576,19 @@ function printDoctor(report) {
4557
5576
  }
4558
5577
  }
4559
5578
  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}`);
5579
+ const ok2 = pc22.green("ok");
5580
+ const bad = pc22.red("fail");
5581
+ const warn2 = pc22.yellow("warn");
5582
+ console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
4564
5583
  if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
4565
5584
  if (report.db.error) console.log(` ${report.db.error}`);
4566
- console.log(`pgvector: ${report.pgvector.ok ? ok : bad}`);
5585
+ console.log(`pgvector: ${report.pgvector.ok ? ok2 : bad}`);
4567
5586
  if (report.pgvector.error) console.log(` ${report.pgvector.error}`);
4568
- console.log(`namespace: ${report.namespace.ok ? ok : warn}`);
5587
+ console.log(`namespace: ${report.namespace.ok ? ok2 : warn2}`);
4569
5588
  console.log(` runtime=${report.namespace.runtime ?? "unset"}`);
4570
5589
  if (report.namespace.warning) console.log(` ${report.namespace.warning}`);
4571
5590
  }
4572
5591
  export {
4573
5592
  createCli
4574
5593
  };
4575
- //# sourceMappingURL=main-ZZ3EQLYA.js.map
5594
+ //# sourceMappingURL=main-BDVQAGAS.js.map