@orquentra/codebuddy 2.9.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CLAUDE_CODEX.md +33 -0
  2. package/README.md +27 -3
  3. package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
  4. package/dist/{chunk-GKNYMNFM.js → chunk-4DKYKDHI.js} +6 -1
  5. package/dist/chunk-4DKYKDHI.js.map +1 -0
  6. package/dist/{chunk-SRA4MDVG.js → chunk-7QMHIJBA.js} +30 -3
  7. package/dist/chunk-7QMHIJBA.js.map +1 -0
  8. package/dist/{chunk-6NZEZJYD.js → chunk-ESC4Y3L7.js} +3 -5
  9. package/dist/{chunk-6NZEZJYD.js.map → chunk-ESC4Y3L7.js.map} +1 -1
  10. package/dist/chunk-GQKWUHMA.js +20 -0
  11. package/dist/chunk-GQKWUHMA.js.map +1 -0
  12. package/dist/{chunk-J4T6HHI4.js → chunk-L6BRYZ6G.js} +537 -316
  13. package/dist/chunk-L6BRYZ6G.js.map +1 -0
  14. package/dist/{chunk-UK6XTS2Y.js → chunk-LRQNPYCK.js} +13 -19
  15. package/dist/chunk-LRQNPYCK.js.map +1 -0
  16. package/dist/{chunk-GXOML4UZ.js → chunk-LW75SKZR.js} +2 -2
  17. package/dist/{chunk-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
  18. package/dist/chunk-UOTKHSQ5.js.map +1 -0
  19. package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
  20. package/dist/chunk-V7PT6ER5.js.map +1 -0
  21. package/dist/{chunk-F4BJ3DXW.js → chunk-XSNW6Y3X.js} +1253 -315
  22. package/dist/chunk-XSNW6Y3X.js.map +1 -0
  23. package/dist/{chunk-2WH2Q4TA.js → chunk-YJPCS2GP.js} +256 -68
  24. package/dist/chunk-YJPCS2GP.js.map +1 -0
  25. package/dist/{chunk-TLWGZVPC.js → chunk-YNSTIMCF.js} +746 -66
  26. package/dist/chunk-YNSTIMCF.js.map +1 -0
  27. package/dist/{chunk-QN76UJP7.js → chunk-YWDH6DQ6.js} +70 -4
  28. package/dist/chunk-YWDH6DQ6.js.map +1 -0
  29. package/dist/cli/index.js +23 -9
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
  32. package/dist/index.d.ts +6 -4
  33. package/dist/index.js +8 -8
  34. package/dist/install-EU35ERK5.js +30 -0
  35. package/dist/langgraph/index.d.ts +1 -1
  36. package/dist/lean-43ADYTBW.js +28 -0
  37. package/dist/{main-YJKSYFPT.js → main-SFC5LV4E.js} +704 -242
  38. package/dist/main-SFC5LV4E.js.map +1 -0
  39. package/dist/{operations-OG4ISWIM.js → operations-6Z6ZDVBP.js} +3 -3
  40. package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
  41. package/dist/prompt-gate-TDTFFQW2.js +8 -0
  42. package/dist/{runtime-GCSEIAVD.js → runtime-W5AW32SD.js} +14 -6
  43. package/dist/{summaries-ML53R7EL.js → summaries-32PHOB6L.js} +3 -4
  44. package/dist/{zvec-XDZNZ2OL.js → zvec-24HLAFGN.js} +3 -4
  45. package/dist/zvec-24HLAFGN.js.map +1 -0
  46. package/package.json +11 -2
  47. package/dist/chunk-2WH2Q4TA.js.map +0 -1
  48. package/dist/chunk-ENKDFLOO.js.map +0 -1
  49. package/dist/chunk-F4BJ3DXW.js.map +0 -1
  50. package/dist/chunk-GKNYMNFM.js.map +0 -1
  51. package/dist/chunk-ITQO632W.js +0 -673
  52. package/dist/chunk-ITQO632W.js.map +0 -1
  53. package/dist/chunk-J4T6HHI4.js.map +0 -1
  54. package/dist/chunk-QN76UJP7.js.map +0 -1
  55. package/dist/chunk-SRA4MDVG.js.map +0 -1
  56. package/dist/chunk-TLWGZVPC.js.map +0 -1
  57. package/dist/chunk-UK6XTS2Y.js.map +0 -1
  58. package/dist/chunk-W7GAXL6C.js.map +0 -1
  59. package/dist/install-BXM56QCD.js +0 -19
  60. package/dist/lean-command-7DFW32MA.js +0 -19
  61. package/dist/main-YJKSYFPT.js.map +0 -1
  62. /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
  63. /package/dist/{chunk-GXOML4UZ.js.map → chunk-LW75SKZR.js.map} +0 -0
  64. /package/dist/{install-BXM56QCD.js.map → install-EU35ERK5.js.map} +0 -0
  65. /package/dist/{lean-command-7DFW32MA.js.map → lean-43ADYTBW.js.map} +0 -0
  66. /package/dist/{operations-OG4ISWIM.js.map → operations-6Z6ZDVBP.js.map} +0 -0
  67. /package/dist/{pretool-command-PRFOIHMW.js.map → pretool-command-B5GY5T6E.js.map} +0 -0
  68. /package/dist/{runtime-GCSEIAVD.js.map → prompt-gate-TDTFFQW2.js.map} +0 -0
  69. /package/dist/{summaries-ML53R7EL.js.map → runtime-W5AW32SD.js.map} +0 -0
  70. /package/dist/{zvec-XDZNZ2OL.js.map → summaries-32PHOB6L.js.map} +0 -0
@@ -4,10 +4,30 @@ import {
4
4
  MODE_LABELS,
5
5
  sharedRulesReaders,
6
6
  supportedModes
7
- } from "./chunk-W7GAXL6C.js";
7
+ } from "./chunk-V7PT6ER5.js";
8
+ import {
9
+ runLeanHook
10
+ } from "./chunk-LRQNPYCK.js";
11
+ import {
12
+ appendHostEvents,
13
+ extractEditedPaths,
14
+ isUnfinished,
15
+ latestUnfinished,
16
+ readCheckpoint,
17
+ renderCheckpointNow,
18
+ runCaptureHook,
19
+ runCheckpointHook,
20
+ runContextHook,
21
+ runResetHook,
22
+ runResumeHook,
23
+ runStageHook
24
+ } from "./chunk-XSNW6Y3X.js";
25
+ import {
26
+ hookRoot
27
+ } from "./chunk-TGFTKWOR.js";
8
28
  import {
9
29
  indexRepository
10
- } from "./chunk-ITQO632W.js";
30
+ } from "./chunk-YNSTIMCF.js";
11
31
  import {
12
32
  resolveNamespaceFrom
13
33
  } from "./chunk-KNR2P2UY.js";
@@ -17,18 +37,318 @@ import {
17
37
 
18
38
  // src/clients/install.ts
19
39
  import { access as access2 } from "fs/promises";
20
- import { join as join9, resolve as resolve7 } from "path";
40
+ import { join as join10, resolve as resolve7 } from "path";
41
+
42
+ // src/hooks/codex.ts
43
+ import { join as join2 } from "path";
44
+
45
+ // src/hooks/bridge.ts
46
+ async function runBridgeHook(input, emit) {
47
+ await runCheckpointHook(input);
48
+ const root = hookRoot(input.cwd);
49
+ const session = input.session_id ?? "default";
50
+ const recovery = input.bridge_recovery;
51
+ const checkpoint = recovery === "resume" ? await latestUnfinished(root, session) : recovery ? await readCheckpoint(root, session) : null;
52
+ const task = checkpoint && (recovery === "compact" || isUnfinished(checkpoint)) && (recovery !== "current" || checkpoint.editedPaths.length || checkpoint.findings.length || checkpoint.nextAction || checkpoint.verification.length) ? await renderCheckpointNow(
53
+ root,
54
+ checkpoint,
55
+ recovery === "resume" ? `unfinished, from session ${checkpoint.session}` : "current task"
56
+ ) : "";
57
+ let sent = false;
58
+ const output = async (block2) => {
59
+ await emit?.([block2, task].filter(Boolean).join("\n"));
60
+ sent = true;
61
+ };
62
+ const block = input.injection === "lean" ? await runLeanHook(input, output) : await runContextHook(input, output);
63
+ if (!sent && task) await emit?.(task);
64
+ return [block, task].filter(Boolean).join("\n");
65
+ }
66
+
67
+ // src/hooks/settings.ts
68
+ import { mkdir, readFile } from "fs/promises";
69
+ import { dirname, join, resolve } from "path";
70
+ var HOOK_COMMAND_PREFIX = "codebuddy hooks";
71
+ var TOOL_GUARD_DEF = {
72
+ event: "PreToolUse",
73
+ matcher: "Read|Grep|Bash",
74
+ command: `${HOOK_COMMAND_PREFIX} pretool`
75
+ };
76
+ var CAPTURE_DEFS = [
77
+ {
78
+ event: "PostToolUse",
79
+ matcher: "Edit|Write|MultiEdit",
80
+ command: `${HOOK_COMMAND_PREFIX} stage`
81
+ },
82
+ { event: "Stop", command: `${HOOK_COMMAND_PREFIX} capture` },
83
+ // Journal the transcript before compaction summarizes it.
84
+ { event: "PreCompact", command: `${HOOK_COMMAND_PREFIX} checkpoint` },
85
+ // A new or resumed session sees another session's unfinished task, if any.
86
+ { event: "SessionStart", matcher: "startup|resume", command: `${HOOK_COMMAND_PREFIX} resume` }
87
+ ];
88
+ var [STAGE_DEF, TURN_END_DEF, CHECKPOINT_DEF, RESUME_DEF] = CAPTURE_DEFS;
89
+ var RESET_DEF = {
90
+ event: "SessionStart",
91
+ matcher: "compact|clear",
92
+ command: `${HOOK_COMMAND_PREFIX} reset`
93
+ };
94
+ var INJECTION_DEFS = {
95
+ full: { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} context` },
96
+ /** Lean: prompt pointers only. */
97
+ lean: { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} lean` }
98
+ };
99
+ function hookDefs(variant, capture) {
100
+ return [INJECTION_DEFS[variant], RESET_DEF, ...capture ? CAPTURE_DEFS : [], TOOL_GUARD_DEF];
101
+ }
102
+ var defaultCapture = (variant) => variant === "full";
103
+ function claudeSettingsPath(repositoryRoot = process.cwd()) {
104
+ return join(resolve(repositoryRoot), ".claude", "settings.json");
105
+ }
106
+ function upsertHooks(settings, variant = "full", capture = defaultCapture(variant)) {
107
+ const defs = hookDefs(variant, capture);
108
+ const wanted = new Set(defs.map((def) => def.command));
109
+ const next = { ...settings, hooks: { ...settings.hooks ?? {} } };
110
+ const hooks = next.hooks;
111
+ let changed = false;
112
+ for (const event of Object.keys(hooks)) {
113
+ const groups = [];
114
+ for (const group of hooks[event] ?? []) {
115
+ const kept = (group.hooks ?? []).filter(
116
+ (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX) || wanted.has(hook.command)
117
+ );
118
+ if (kept.length !== (group.hooks ?? []).length) changed = true;
119
+ if (kept.length > 0) groups.push({ ...group, hooks: kept });
120
+ }
121
+ if (groups.length > 0) hooks[event] = groups;
122
+ else delete hooks[event];
123
+ }
124
+ for (const def of defs) {
125
+ const groups = [...hooks[def.event] ?? []];
126
+ const existingIndex = groups.findIndex(
127
+ (group) => group.hooks?.some((hook) => hook.command === def.command)
128
+ );
129
+ if (existingIndex === -1) {
130
+ groups.push({
131
+ ...def.matcher ? { matcher: def.matcher } : {},
132
+ hooks: [{ type: "command", command: def.command }]
133
+ });
134
+ changed = true;
135
+ } else {
136
+ const existing = groups[existingIndex];
137
+ if (existing && (def.matcher ?? void 0) !== (existing.matcher ?? void 0)) {
138
+ const { matcher: _oldMatcher, ...rest } = existing;
139
+ groups[existingIndex] = def.matcher ? { ...rest, matcher: def.matcher } : rest;
140
+ changed = true;
141
+ }
142
+ }
143
+ hooks[def.event] = groups;
144
+ }
145
+ return { settings: next, changed };
146
+ }
147
+ function removeHooks(settings) {
148
+ if (!settings.hooks) return { settings, changed: false };
149
+ const next = { ...settings, hooks: { ...settings.hooks } };
150
+ const hooks = next.hooks;
151
+ let changed = false;
152
+ for (const event of Object.keys(hooks)) {
153
+ const keptGroups = [];
154
+ for (const group of hooks[event] ?? []) {
155
+ const keptHooks = (group.hooks ?? []).filter(
156
+ (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX)
157
+ );
158
+ if (keptHooks.length !== (group.hooks ?? []).length) changed = true;
159
+ if (keptHooks.length === 0) continue;
160
+ keptGroups.push({ ...group, hooks: keptHooks });
161
+ }
162
+ if (keptGroups.length === 0) delete hooks[event];
163
+ else hooks[event] = keptGroups;
164
+ }
165
+ return { settings: next, changed };
166
+ }
167
+ function hooksInstalled(settings) {
168
+ const hooks = settings.hooks ?? {};
169
+ return Object.values(hooks).some(
170
+ (groups) => groups.some((group) => group.hooks?.some((h) => h.command?.startsWith(HOOK_COMMAND_PREFIX)))
171
+ );
172
+ }
173
+ function hooksVariant(settings) {
174
+ if (!hooksInstalled(settings)) return null;
175
+ const leanCommand = `${HOOK_COMMAND_PREFIX} lean`;
176
+ const lean = Object.values(settings.hooks ?? {}).some(
177
+ (groups) => groups.some((group) => group.hooks?.some((hook) => hook.command === leanCommand))
178
+ );
179
+ return lean ? "lean" : "full";
180
+ }
181
+ function hookCapabilities(settings) {
182
+ const commands = new Set(
183
+ Object.values(settings.hooks ?? {}).flatMap(
184
+ (groups) => groups.flatMap((group) => (group.hooks ?? []).map((hook) => hook.command))
185
+ )
186
+ );
187
+ const has = (def) => commands.has(def.command);
188
+ return {
189
+ injection: has(INJECTION_DEFS.lean) ? "lean" : has(INJECTION_DEFS.full) ? "full" : null,
190
+ capture: has(STAGE_DEF) && has(TURN_END_DEF),
191
+ recovery: has(CHECKPOINT_DEF) && has(RESUME_DEF),
192
+ reset: has(RESET_DEF),
193
+ guard: has(TOOL_GUARD_DEF)
194
+ };
195
+ }
196
+ async function readSettings(path) {
197
+ try {
198
+ return JSON.parse(await readFile(path, "utf8"));
199
+ } catch (error) {
200
+ if (error.code === "ENOENT") return {};
201
+ throw error;
202
+ }
203
+ }
204
+ async function writeSettings(path, settings) {
205
+ await mkdir(dirname(path), { recursive: true });
206
+ await writeAtomic(path, `${JSON.stringify(settings, null, 2)}
207
+ `, { mode: 420 });
208
+ }
209
+
210
+ // src/hooks/codex.ts
211
+ var PREFIX = "codebuddy hooks codex";
212
+ var owned = (command) => command === PREFIX || command.startsWith(`${PREFIX} `);
213
+ var codexHooksPath = (root) => join2(root, ".codex", "hooks.json");
214
+ async function setCodexHooks(root, enabled, variant = "full", capture = defaultCapture(variant)) {
215
+ const path = codexHooksPath(root);
216
+ const before = await readSettings(path);
217
+ const hooks = {};
218
+ const next = { ...before, hooks };
219
+ for (const [event, groups] of Object.entries(before.hooks ?? {})) {
220
+ const kept = groups.flatMap((group) => {
221
+ const hooks2 = group.hooks.filter((hook) => !owned(hook.command));
222
+ return hooks2.length ? [{ ...group, hooks: hooks2 }] : [];
223
+ });
224
+ if (kept.length) hooks[event] = kept;
225
+ }
226
+ if (enabled) {
227
+ const command = `${PREFIX} --injection ${variant}${capture ? " --capture" : ""}`;
228
+ for (const [event, matcher] of [
229
+ ["UserPromptSubmit", void 0],
230
+ ...capture ? [
231
+ ["PostToolUse", "Bash|apply_patch|Edit|Write"],
232
+ ["Stop", void 0],
233
+ ["PreCompact", void 0],
234
+ ["Interrupt", void 0]
235
+ ] : [],
236
+ ["SessionStart", "startup|resume|fork|compact|clear"]
237
+ ]) {
238
+ const handler = {
239
+ type: "command",
240
+ command,
241
+ timeout: event === "Interrupt" ? 3 : 20
242
+ };
243
+ hooks[event] ??= [];
244
+ hooks[event].push({ ...matcher ? { matcher } : {}, hooks: [handler] });
245
+ }
246
+ }
247
+ if (!Object.keys(next.hooks ?? {}).length) delete next.hooks;
248
+ if (JSON.stringify(before) === JSON.stringify(next)) return false;
249
+ await writeSettings(path, next);
250
+ return true;
251
+ }
252
+ async function codexHookCapabilities(root) {
253
+ const settings = await readSettings(codexHooksPath(root));
254
+ const commands = (event) => (settings.hooks?.[event] ?? []).flatMap((group) => group.hooks.map((hook) => hook.command)).filter(owned);
255
+ const prompt = commands("UserPromptSubmit")[0];
256
+ const capture = commands("Stop").length > 0;
257
+ return {
258
+ injection: prompt ? prompt.includes("--injection lean") ? "lean" : "full" : null,
259
+ capture,
260
+ recovery: capture && commands("PreCompact").length > 0,
261
+ reset: commands("SessionStart").length > 0,
262
+ guard: false
263
+ };
264
+ }
265
+ function toolOutcome(value) {
266
+ if (value && typeof value === "object") {
267
+ const item = value;
268
+ if (typeof item.exit_code === "number") return item.exit_code === 0;
269
+ if (typeof item.is_error === "boolean") return !item.is_error;
270
+ }
271
+ return void 0;
272
+ }
273
+ async function runCodexHook(input, variant, capture) {
274
+ const root = hookRoot(input.cwd);
275
+ const session = input.session_id ?? "default";
276
+ const turn = input.turn_id ?? input.session_id ?? "default";
277
+ const event = input.hook_event_name;
278
+ let context = "";
279
+ const base = { cwd: root, session_id: session };
280
+ if (event === "UserPromptSubmit") {
281
+ const journal_events = capture && input.prompt ? [{ id: `${turn}:prompt`, kind: "user", text: input.prompt }] : [];
282
+ context = await runBridgeHook({
283
+ ...base,
284
+ ...input.prompt ? { prompt: input.prompt } : {},
285
+ injection: variant,
286
+ journal_events
287
+ });
288
+ } else if (event === "PostToolUse" && capture) {
289
+ const id = input.tool_use_id ?? `${turn}:${JSON.stringify(input.tool_input)}`;
290
+ const events = [];
291
+ if (input.tool_name === "Bash" && typeof input.tool_input?.command === "string") {
292
+ events.push({ id, kind: "command", ref: id, text: input.tool_input.command });
293
+ const ok = toolOutcome(input.tool_response);
294
+ events.push({
295
+ id: `${id}:result`,
296
+ kind: "result",
297
+ ref: id,
298
+ ...ok !== void 0 ? { ok } : {}
299
+ });
300
+ await appendHostEvents(root, session, events);
301
+ } else {
302
+ if (toolOutcome(input.tool_response) === false) return {};
303
+ const tool_input = input.tool_name === "apply_patch" ? { patch: input.tool_input?.command } : input.tool_input;
304
+ extractEditedPaths(tool_input).forEach((path, index) => {
305
+ events.push({ id: `${id}:${index}`, kind: "edit", path });
306
+ });
307
+ await runStageHook({
308
+ ...base,
309
+ ...tool_input ? { tool_input } : {},
310
+ journal_events: events
311
+ });
312
+ }
313
+ } else if (event === "Stop" && capture) {
314
+ const checkpoint = await readCheckpoint(root, session);
315
+ await runCaptureHook({
316
+ ...base,
317
+ ...input.last_assistant_message ? { last_assistant_message: input.last_assistant_message } : {},
318
+ user_prompts: checkpoint?.constraints ?? [],
319
+ shell_commands: checkpoint?.verification.map((run) => ({
320
+ command: run.command,
321
+ exit_code: run.ok === null ? null : run.ok ? 0 : 1
322
+ })) ?? [],
323
+ journal_events: input.last_assistant_message ? [{ id: `${turn}:assistant`, kind: "assistant", text: input.last_assistant_message }] : []
324
+ });
325
+ } else if (event === "SessionStart") {
326
+ if (input.source === "compact" || input.source === "clear") {
327
+ context = await runResetHook({
328
+ ...base,
329
+ ...variant === "full" ? { hook_event_name: "SessionStart" } : {},
330
+ source: capture ? input.source : "clear"
331
+ });
332
+ if (variant === "lean" && capture && input.source === "compact") {
333
+ const checkpoint = await readCheckpoint(root, session);
334
+ if (checkpoint?.goal)
335
+ context = await renderCheckpointNow(root, checkpoint, "before compaction");
336
+ }
337
+ } else if (capture) context = await runResumeHook(base);
338
+ }
339
+ return context ? { hookSpecificOutput: { hookEventName: event, additionalContext: context } } : {};
340
+ }
21
341
 
22
342
  // src/hooks/hermes-plugin.ts
23
- import { readFile as readFile2 } from "fs/promises";
24
- import { dirname as dirname2, join, resolve } from "path";
343
+ import { readFile as readFile3 } from "fs/promises";
344
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
25
345
 
26
346
  // src/hooks/managed-plugin.ts
27
- import { mkdir, readFile, rm, rmdir } from "fs/promises";
28
- import { dirname } from "path";
347
+ import { mkdir as mkdir2, readFile as readFile2, rm, rmdir } from "fs/promises";
348
+ import { dirname as dirname2 } from "path";
29
349
  async function read(path) {
30
350
  try {
31
- return await readFile(path, "utf8");
351
+ return await readFile2(path, "utf8");
32
352
  } catch (error) {
33
353
  if (error.code === "ENOENT") return null;
34
354
  throw error;
@@ -55,7 +375,7 @@ async function setManagedPlugin(plugin3, enabled) {
55
375
  let changed = false;
56
376
  for (const [i, file] of plugin3.files.entries()) {
57
377
  if (enabled && current[i] !== file.content) {
58
- await mkdir(dirname(file.path), { recursive: true });
378
+ await mkdir2(dirname2(file.path), { recursive: true });
59
379
  await writeAtomic(file.path, file.content, { mode: 420 });
60
380
  changed = true;
61
381
  } else if (!enabled && current[i] != null) {
@@ -108,9 +428,14 @@ var HERMES_HOOK_FN = `def _hook(command, payload):
108
428
 
109
429
 
110
430
  `;
111
- function renderHermesPlugin(variant) {
112
- return variant === "lean" ? `${HERMES_PLUGIN_MARKER} v1 lean
113
- # Generated by "codebuddy clients install hermes --mode lean"; removed by
431
+ function renderHermesPlugin(variant, capture = defaultCapture(variant)) {
432
+ const command = variant === "lean" ? "lean" : "context";
433
+ const mode = variant === "lean" ? "lean" : "hooks";
434
+ const captureMarker = capture === defaultCapture(variant) ? "" : `
435
+ # codebuddy-capture: ${capture ? "on" : "off"}`;
436
+ const marker = `${HERMES_PLUGIN_MARKER} v1${variant === "lean" ? " lean" : ""}${captureMarker}${capture ? "\n# codebuddy-recovery: v1" : ""}`;
437
+ return !capture ? `${marker}
438
+ # Generated by "codebuddy clients install hermes --mode ${mode}"; removed by
114
439
  # "codebuddy clients uninstall hermes". Local edits are overwritten.
115
440
  """CodeBuddy lean bridge: forwards each prompt to "codebuddy hooks lean"."""
116
441
 
@@ -120,7 +445,7 @@ from pathlib import Path
120
445
 
121
446
  # <root>/.hermes/plugins/codebuddy/__init__.py
122
447
  ROOT = str(Path(__file__).absolute().parents[3])
123
- TIMEOUT_S = {"lean": 2}
448
+ TIMEOUT_S = {"${command}": ${variant === "lean" ? 2 : 20}}
124
449
 
125
450
 
126
451
  ${HERMES_HOOK_FN}def _pre_llm_call(session_id="", user_message="", **_):
@@ -132,14 +457,14 @@ ${HERMES_HOOK_FN}def _pre_llm_call(session_id="", user_message="", **_):
132
457
  "ephemeral_context": True,
133
458
  "prompt": user_message,
134
459
  }
135
- block = _hook("lean", payload).strip()
460
+ block = _hook("${command}", payload).strip()
136
461
  return {"context": block} if block else None
137
462
 
138
463
 
139
464
  def register(ctx):
140
465
  ctx.register_hook("pre_llm_call", _pre_llm_call)
141
- ` : `${HERMES_PLUGIN_MARKER} v1
142
- # Generated by "codebuddy clients install hermes --mode hooks"; removed by
466
+ ` : `${marker}
467
+ # Generated by "codebuddy clients install hermes --mode ${mode}"; removed by
143
468
  # "codebuddy clients uninstall hermes". Local edits are overwritten.
144
469
  """CodeBuddy lifecycle bridge: forwards Hermes hooks to "codebuddy hooks <command>"."""
145
470
 
@@ -151,7 +476,7 @@ from pathlib import Path
151
476
  # <root>/.hermes/plugins/codebuddy/__init__.py
152
477
  ROOT = str(Path(__file__).absolute().parents[3])
153
478
  EDIT_TOOLS = {"write_file", "patch"}
154
- TIMEOUT_S = {"context": 20, "stage": 10, "capture": 60}
479
+ TIMEOUT_S = {"bridge": 20, "checkpoint": 10, "stage": 10, "capture": 60}
155
480
  _lock = threading.Lock()
156
481
  _turns = {}
157
482
 
@@ -160,20 +485,23 @@ ${HERMES_HOOK_FN}def _turn(turn_id, session_id):
160
485
  # Compression can rotate the session id mid-turn; a turn keeps its first id.
161
486
  with _lock:
162
487
  return _turns.setdefault(
163
- turn_id or session_id, {"session": session_id, "text": "", "commands": []}
488
+ turn_id or session_id, {"session": session_id, "text": "", "commands": [], "prompt": ""}
164
489
  )
165
490
 
166
491
 
167
- def _pre_llm_call(session_id="", turn_id="", user_message="", **_):
168
- _turn(turn_id, session_id)
169
- payload = {"session_id": session_id, "hook_event_name": "pre_llm_call", "ephemeral_context": True}
492
+ def _pre_llm_call(session_id="", turn_id="", user_message="", is_first_turn=False, **_):
493
+ turn = _turn(turn_id, session_id)
494
+ payload = {"session_id": turn["session"], "hook_event_name": "pre_llm_call", "ephemeral_context": True,
495
+ "injection": "${variant}", "bridge_recovery": "resume" if is_first_turn else "current"}
170
496
  if isinstance(user_message, str) and user_message.strip():
171
497
  payload["prompt"] = user_message
172
- block = _hook("context", payload).strip()
498
+ turn["prompt"] = user_message
499
+ payload["journal_events"] = [{"id": (turn_id or session_id) + ":prompt", "kind": "user", "text": user_message}]
500
+ block = _hook("bridge", payload).strip()
173
501
  return {"context": block} if block else None
174
502
 
175
503
 
176
- def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id="", **_):
504
+ def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id="", tool_call_id="", **_):
177
505
  turn = _turn(turn_id, session_id)
178
506
  args = args if isinstance(args, dict) else {}
179
507
  if tool_name in EDIT_TOOLS:
@@ -184,6 +512,7 @@ def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id
184
512
  "hook_event_name": "post_tool_call",
185
513
  "tool_name": tool_name,
186
514
  "tool_input": tool_input,
515
+ "journal_event_id": tool_call_id or turn_id + ":" + json.dumps(tool_input, sort_keys=True),
187
516
  })
188
517
  elif tool_name == "terminal" and isinstance(args.get("command"), str):
189
518
  exit_code = None
@@ -196,18 +525,29 @@ def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id
196
525
  "command": args["command"],
197
526
  "exit_code": exit_code if type(exit_code) is int else None,
198
527
  })
528
+ ref = tool_call_id or turn_id + ":" + args["command"]
529
+ outcome = {"id": ref + ":result", "kind": "result", "ref": ref}
530
+ if type(exit_code) is int:
531
+ outcome["ok"] = exit_code == 0
532
+ _hook("checkpoint", {"session_id": turn["session"], "journal_events": [
533
+ {"id": ref, "kind": "command", "ref": ref, "text": args["command"]}, outcome]})
199
534
 
200
535
 
201
536
  def _post_llm_call(session_id="", turn_id="", assistant_response="", **_):
202
537
  if isinstance(assistant_response, str) and assistant_response.strip():
203
- _turn(turn_id, session_id)["text"] = assistant_response
538
+ turn = _turn(turn_id, session_id)
539
+ turn["text"] = assistant_response
540
+ _hook("checkpoint", {"session_id": turn["session"], "journal_events": [
541
+ {"id": (turn_id or session_id) + ":text:" + assistant_response, "kind": "assistant", "text": assistant_response}]})
204
542
 
205
543
 
206
544
  def _on_session_end(session_id="", turn_id="", **_):
207
545
  with _lock:
208
546
  turn = _turns.pop(turn_id or session_id, None)
209
- turn = turn or {"session": session_id, "text": "", "commands": []}
547
+ turn = turn or {"session": session_id, "text": "", "commands": [], "prompt": ""}
210
548
  payload = {"session_id": turn["session"], "hook_event_name": "on_session_end"}
549
+ if turn.get("prompt"):
550
+ payload["user_prompts"] = [turn["prompt"]]
211
551
  if turn["text"]:
212
552
  payload["last_assistant_message"] = turn["text"]
213
553
  if turn["commands"]:
@@ -224,37 +564,49 @@ def register(ctx):
224
564
  }
225
565
  var HERMES_PLUGIN_SOURCE = renderHermesPlugin("full");
226
566
  function hermesPluginDir(repositoryRoot) {
227
- return join(resolve(repositoryRoot), ".hermes", "plugins", "codebuddy");
567
+ return join3(resolve2(repositoryRoot), ".hermes", "plugins", "codebuddy");
228
568
  }
229
- function plugin(repositoryRoot, variant = "full") {
569
+ function plugin(repositoryRoot, variant = "full", capture = defaultCapture(variant)) {
230
570
  const dir = hermesPluginDir(repositoryRoot);
231
571
  return {
232
572
  marker: HERMES_PLUGIN_MARKER,
233
573
  files: [
234
574
  {
235
- path: join(dir, "plugin.yaml"),
236
- content: variant === "lean" ? HERMES_LEAN_MANIFEST : HERMES_PLUGIN_MANIFEST
575
+ path: join3(dir, "plugin.yaml"),
576
+ content: capture ? HERMES_PLUGIN_MANIFEST : HERMES_LEAN_MANIFEST
237
577
  },
238
- { path: join(dir, "__init__.py"), content: renderHermesPlugin(variant) }
578
+ { path: join3(dir, "__init__.py"), content: renderHermesPlugin(variant, capture) }
239
579
  ],
240
- dirs: [dir, dirname2(dir), dirname2(dirname2(dir))]
580
+ dirs: [dir, dirname3(dir), dirname3(dirname3(dir))]
241
581
  };
242
582
  }
243
583
  function hermesPluginInstalled(repositoryRoot) {
244
584
  return managedPluginInstalled(plugin(repositoryRoot));
245
585
  }
246
- function setHermesPlugin(repositoryRoot, enabled, variant = "full") {
247
- return setManagedPlugin(plugin(repositoryRoot, variant), enabled);
586
+ function setHermesPlugin(repositoryRoot, enabled, variant = "full", capture = defaultCapture(variant)) {
587
+ return setManagedPlugin(plugin(repositoryRoot, variant, capture), enabled);
588
+ }
589
+ async function hermesPluginCapabilities(root) {
590
+ const injection = await hermesPluginVariant(root);
591
+ const source = injection ? await readFile3(join3(hermesPluginDir(root), "__init__.py"), "utf8") : "";
592
+ const explicit = source.match(/^# codebuddy-capture: (on|off)$/m)?.[1];
593
+ return {
594
+ injection,
595
+ capture: explicit ? explicit === "on" : injection === "full",
596
+ recovery: (explicit ? explicit === "on" : injection === "full") && source.includes("codebuddy-recovery: v1"),
597
+ reset: injection !== null,
598
+ guard: false
599
+ };
248
600
  }
249
601
  async function hermesPluginVariant(repositoryRoot) {
250
602
  if (!await hermesPluginInstalled(repositoryRoot)) return null;
251
- const source = await readFile2(join(hermesPluginDir(repositoryRoot), "__init__.py"), "utf8");
603
+ const source = await readFile3(join3(hermesPluginDir(repositoryRoot), "__init__.py"), "utf8");
252
604
  return source.startsWith(`${HERMES_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
253
605
  }
254
606
 
255
607
  // src/hooks/opencode-plugin.ts
256
- import { readFile as readFile3 } from "fs/promises";
257
- import { dirname as dirname3, join as join2, resolve as resolve2 } from "path";
608
+ import { readFile as readFile4 } from "fs/promises";
609
+ import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
258
610
  var OPENCODE_PLUGIN_MARKER = "// codebuddy-managed: opencode-plugin";
259
611
  var OPENCODE_RUNTIME = `const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
260
612
  let lastMs = 0;
@@ -309,13 +661,18 @@ function partId() {
309
661
  }
310
662
 
311
663
  `;
312
- function renderOpencodePlugin(variant) {
313
- return variant === "lean" ? `${OPENCODE_PLUGIN_MARKER} v1 lean
314
- // Generated by \`codebuddy clients install opencode --mode lean\`; removed by
664
+ function renderOpencodePlugin(variant, capture = defaultCapture(variant)) {
665
+ const command = variant === "lean" ? "lean" : "context";
666
+ const mode = variant === "lean" ? "lean" : "hooks";
667
+ const captureMarker = capture === defaultCapture(variant) ? "" : `
668
+ // codebuddy-capture: ${capture ? "on" : "off"}`;
669
+ const marker = `${OPENCODE_PLUGIN_MARKER} v1${variant === "lean" ? " lean" : ""}${captureMarker}${capture ? "\n// codebuddy-recovery: v1" : ""}`;
670
+ return !capture ? `${marker}
671
+ // Generated by \`codebuddy clients install opencode --mode ${mode}\`; removed by
315
672
  // \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
316
673
  import { spawn } from "node:child_process";
317
674
 
318
- const TIMEOUT_MS = { lean: 2000, reset: 10000 };
675
+ const TIMEOUT_MS = { ${command}: ${variant === "lean" ? 2e3 : 2e4}, reset: 10000 };
319
676
  ${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
320
677
  export const CodeBuddyPlugin = async ({ directory }) => {
321
678
  const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
@@ -327,7 +684,7 @@ export const CodeBuddyPlugin = async ({ directory }) => {
327
684
  .join("\\n")
328
685
  .trim();
329
686
  if (!prompt) return;
330
- const block = (await hook("lean", { ...base(input.sessionID, "chat.message"), prompt })).trim();
687
+ const block = (await hook("${command}", { ...base(input.sessionID, "chat.message"), prompt })).trim();
331
688
  if (!block) return;
332
689
  output.parts.push({
333
690
  id: partId(),
@@ -345,19 +702,21 @@ export const CodeBuddyPlugin = async ({ directory }) => {
345
702
  },
346
703
  };
347
704
  };
348
- ` : `${OPENCODE_PLUGIN_MARKER} v1
349
- // Generated by \`codebuddy clients install opencode --mode hooks\`; removed by
705
+ ` : `${marker}
706
+ // Generated by \`codebuddy clients install opencode --mode ${mode}\`; removed by
350
707
  // \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
351
708
  import { spawn } from "node:child_process";
352
709
 
353
710
  const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]);
354
- const TIMEOUT_MS = { context: 20000, stage: 10000, capture: 60000, reset: 10000 };
711
+ const TIMEOUT_MS = { bridge: 20000, checkpoint: 10000, stage: 10000, capture: 60000, reset: 10000 };
355
712
  ${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
356
713
  export const CodeBuddyPlugin = async ({ directory }) => {
357
714
  /** @type {Map<string, { text: string, commands: Array<{ command: string, exit_code: number | null }> }>} */
358
715
  const turns = new Map();
716
+ const sessions = new Set();
717
+ const compacted = new Set();
359
718
  const turn = (sessionID) => {
360
- if (!turns.has(sessionID)) turns.set(sessionID, { text: "", commands: [] });
719
+ if (!turns.has(sessionID)) turns.set(sessionID, { text: "", commands: [], prompt: "" });
361
720
  return turns.get(sessionID);
362
721
  };
363
722
  const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
@@ -369,9 +728,16 @@ export const CodeBuddyPlugin = async ({ directory }) => {
369
728
  .flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
370
729
  .join("\\n")
371
730
  .trim();
731
+ turn(input.sessionID).prompt = prompt;
372
732
  const block = (
373
- await hook("context", { ...base(input.sessionID, "chat.message"), ...(prompt ? { prompt } : {}) })
733
+ await hook("bridge", {
734
+ ...base(input.sessionID, "chat.message"), ...(prompt ? { prompt } : {}),
735
+ injection: "${variant}",
736
+ journal_events: prompt ? [{ id: output.message.id, kind: "user", text: prompt }] : [],
737
+ ...(compacted.has(input.sessionID) ? { bridge_recovery: "compact" } : !sessions.has(input.sessionID) ? { bridge_recovery: "resume" } : {}),
738
+ })
374
739
  ).trim();
740
+ if (block) { sessions.add(input.sessionID); compacted.delete(input.sessionID); }
375
741
  if (!block) return;
376
742
  output.parts.push({
377
743
  id: partId(),
@@ -388,6 +754,7 @@ export const CodeBuddyPlugin = async ({ directory }) => {
388
754
  ...base(input.sessionID, "tool.execute.after"),
389
755
  tool_name: input.tool,
390
756
  tool_input: input.args,
757
+ journal_event_id: input.callID,
391
758
  });
392
759
  } else if (input.tool === "bash" && typeof input.args?.command === "string") {
393
760
  const exit = output.metadata?.exit;
@@ -395,10 +762,23 @@ export const CodeBuddyPlugin = async ({ directory }) => {
395
762
  command: input.args.command,
396
763
  exit_code: typeof exit === "number" ? exit : null,
397
764
  });
765
+ await hook("checkpoint", {
766
+ ...base(input.sessionID, "tool.execute.after"),
767
+ journal_events: [
768
+ { id: input.callID, kind: "command", ref: input.callID, text: input.args.command },
769
+ { id: input.callID + ":result", kind: "result", ref: input.callID, ...(typeof exit === "number" ? { ok: exit === 0 } : {}) },
770
+ ],
771
+ });
398
772
  }
399
773
  },
400
774
  "experimental.text.complete": async (input, output) => {
401
- if (output.text.trim()) turn(input.sessionID).text = output.text;
775
+ if (output.text.trim()) {
776
+ turn(input.sessionID).text = output.text;
777
+ await hook("checkpoint", {
778
+ ...base(input.sessionID, "experimental.text.complete"),
779
+ journal_events: [{ id: (input.messageID || input.sessionID) + ":text:" + output.text, kind: "assistant", text: output.text }],
780
+ });
781
+ }
402
782
  },
403
783
  event: async ({ event }) => {
404
784
  if (event.type === "session.idle") {
@@ -407,10 +787,12 @@ export const CodeBuddyPlugin = async ({ directory }) => {
407
787
  turns.delete(sessionID);
408
788
  await hook("capture", {
409
789
  ...base(sessionID, "session.idle"),
790
+ ...(current?.prompt ? { user_prompts: [current.prompt] } : {}),
410
791
  ...(current?.text ? { last_assistant_message: current.text } : {}),
411
792
  ...(current?.commands.length ? { shell_commands: current.commands.slice(-20) } : {}),
412
793
  });
413
794
  } else if (event.type === "session.compacted") {
795
+ compacted.add(event.properties.sessionID);
414
796
  await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
415
797
  }
416
798
  },
@@ -420,179 +802,105 @@ export const CodeBuddyPlugin = async ({ directory }) => {
420
802
  }
421
803
  var OPENCODE_PLUGIN_SOURCE = renderOpencodePlugin("full");
422
804
  function opencodePluginPath(repositoryRoot) {
423
- return join2(resolve2(repositoryRoot), ".opencode", "plugins", "codebuddy.js");
805
+ return join4(resolve3(repositoryRoot), ".opencode", "plugins", "codebuddy.js");
424
806
  }
425
- function plugin2(repositoryRoot, variant = "full") {
807
+ function plugin2(repositoryRoot, variant = "full", capture = defaultCapture(variant)) {
426
808
  const path = opencodePluginPath(repositoryRoot);
427
809
  return {
428
810
  marker: OPENCODE_PLUGIN_MARKER,
429
- files: [{ path, content: renderOpencodePlugin(variant) }],
430
- dirs: [dirname3(path), dirname3(dirname3(path))]
811
+ files: [{ path, content: renderOpencodePlugin(variant, capture) }],
812
+ dirs: [dirname4(path), dirname4(dirname4(path))]
431
813
  };
432
814
  }
433
815
  function opencodePluginInstalled(repositoryRoot) {
434
816
  return managedPluginInstalled(plugin2(repositoryRoot));
435
817
  }
436
- function setOpencodePlugin(repositoryRoot, enabled, variant = "full") {
437
- return setManagedPlugin(plugin2(repositoryRoot, variant), enabled);
818
+ function setOpencodePlugin(repositoryRoot, enabled, variant = "full", capture = defaultCapture(variant)) {
819
+ return setManagedPlugin(plugin2(repositoryRoot, variant, capture), enabled);
820
+ }
821
+ async function opencodePluginCapabilities(root) {
822
+ const injection = await opencodePluginVariant(root);
823
+ const source = injection ? await readFile4(opencodePluginPath(root), "utf8") : "";
824
+ const explicit = source.match(/^\/\/ codebuddy-capture: (on|off)$/m)?.[1];
825
+ return {
826
+ injection,
827
+ capture: explicit ? explicit === "on" : injection === "full",
828
+ recovery: (explicit ? explicit === "on" : injection === "full") && source.includes("codebuddy-recovery: v1"),
829
+ reset: injection !== null,
830
+ guard: false
831
+ };
438
832
  }
439
833
  async function opencodePluginVariant(repositoryRoot) {
440
834
  if (!await opencodePluginInstalled(repositoryRoot)) return null;
441
- const source = await readFile3(opencodePluginPath(repositoryRoot), "utf8");
835
+ const source = await readFile4(opencodePluginPath(repositoryRoot), "utf8");
442
836
  return source.startsWith(`${OPENCODE_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
443
837
  }
444
838
 
445
- // src/hooks/settings.ts
446
- import { mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
447
- import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
448
- var HOOK_COMMAND_PREFIX = "codebuddy hooks";
449
- var TOOL_GUARD_DEF = {
450
- event: "PreToolUse",
451
- matcher: "Read|Grep|Bash",
452
- command: `${HOOK_COMMAND_PREFIX} pretool`
453
- };
454
- var HOOK_DEFS = [
455
- { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} context` },
456
- {
457
- event: "PostToolUse",
458
- matcher: "Edit|Write|MultiEdit",
459
- command: `${HOOK_COMMAND_PREFIX} stage`
460
- },
461
- { event: "Stop", command: `${HOOK_COMMAND_PREFIX} capture` },
462
- // After compaction or /clear the model no longer holds earlier context, so
463
- // capsules the session ledger marked as sent must be sent again.
464
- { event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
465
- TOOL_GUARD_DEF
466
- ];
467
- var LEAN_HOOK_DEFS = [
468
- { event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} lean` },
469
- { event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
470
- TOOL_GUARD_DEF
471
- ];
472
- function claudeSettingsPath(repositoryRoot = process.cwd()) {
473
- return join3(resolve3(repositoryRoot), ".claude", "settings.json");
474
- }
475
- function upsertHooks(settings, variant = "full") {
476
- const defs = variant === "lean" ? LEAN_HOOK_DEFS : HOOK_DEFS;
477
- const wanted = new Set(defs.map((def) => def.command));
478
- const next = { ...settings, hooks: { ...settings.hooks ?? {} } };
479
- const hooks = next.hooks;
480
- let changed = false;
481
- for (const event of Object.keys(hooks)) {
482
- const groups = [];
483
- for (const group of hooks[event] ?? []) {
484
- const kept = (group.hooks ?? []).filter(
485
- (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX) || wanted.has(hook.command)
486
- );
487
- if (kept.length !== (group.hooks ?? []).length) changed = true;
488
- if (kept.length > 0) groups.push({ ...group, hooks: kept });
489
- }
490
- if (groups.length > 0) hooks[event] = groups;
491
- else delete hooks[event];
492
- }
493
- for (const def of defs) {
494
- const groups = [...hooks[def.event] ?? []];
495
- const existingIndex = groups.findIndex(
496
- (group) => group.hooks?.some((hook) => hook.command === def.command)
497
- );
498
- if (existingIndex === -1) {
499
- groups.push({
500
- ...def.matcher ? { matcher: def.matcher } : {},
501
- hooks: [{ type: "command", command: def.command }]
502
- });
503
- changed = true;
504
- } else {
505
- const existing = groups[existingIndex];
506
- if (existing && (def.matcher ?? void 0) !== (existing.matcher ?? void 0)) {
507
- const { matcher: _oldMatcher, ...rest } = existing;
508
- groups[existingIndex] = def.matcher ? { ...rest, matcher: def.matcher } : rest;
509
- changed = true;
510
- }
511
- }
512
- hooks[def.event] = groups;
513
- }
514
- return { settings: next, changed };
515
- }
516
- function removeHooks(settings) {
517
- if (!settings.hooks) return { settings, changed: false };
518
- const next = { ...settings, hooks: { ...settings.hooks } };
519
- const hooks = next.hooks;
520
- let changed = false;
521
- for (const event of Object.keys(hooks)) {
522
- const keptGroups = [];
523
- for (const group of hooks[event] ?? []) {
524
- const keptHooks = (group.hooks ?? []).filter(
525
- (hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX)
526
- );
527
- if (keptHooks.length !== (group.hooks ?? []).length) changed = true;
528
- if (keptHooks.length === 0) continue;
529
- keptGroups.push({ ...group, hooks: keptHooks });
530
- }
531
- if (keptGroups.length === 0) delete hooks[event];
532
- else hooks[event] = keptGroups;
533
- }
534
- return { settings: next, changed };
535
- }
536
- function hooksInstalled(settings) {
537
- const hooks = settings.hooks ?? {};
538
- return Object.values(hooks).some(
539
- (groups) => groups.some((group) => group.hooks?.some((h) => h.command?.startsWith(HOOK_COMMAND_PREFIX)))
540
- );
541
- }
542
- function hooksVariant(settings) {
543
- if (!hooksInstalled(settings)) return null;
544
- const leanCommand = `${HOOK_COMMAND_PREFIX} lean`;
545
- const lean = Object.values(settings.hooks ?? {}).some(
546
- (groups) => groups.some((group) => group.hooks?.some((hook) => hook.command === leanCommand))
547
- );
548
- return lean ? "lean" : "full";
549
- }
550
- async function readSettings(path) {
551
- try {
552
- return JSON.parse(await readFile4(path, "utf8"));
553
- } catch (error) {
554
- if (error.code === "ENOENT") return {};
555
- throw error;
556
- }
557
- }
558
- async function writeSettings(path, settings) {
559
- await mkdir2(dirname4(path), { recursive: true });
560
- await writeAtomic(path, `${JSON.stringify(settings, null, 2)}
561
- `, { mode: 420 });
562
- }
563
-
564
839
  // src/hooks/install.ts
565
840
  var SURFACES = {
566
841
  "claude-settings": {
567
842
  path: claudeSettingsPath,
568
843
  installed: async (root) => hooksInstalled(await readSettings(claudeSettingsPath(root))),
569
844
  variant: async (root) => hooksVariant(await readSettings(claudeSettingsPath(root))),
570
- set: async (root, enabled, variant) => {
845
+ set: async (root, enabled, variant, capture) => {
571
846
  const path = claudeSettingsPath(root);
572
847
  const current = await readSettings(path);
573
- const { settings, changed } = enabled ? upsertHooks(current, variant) : removeHooks(current);
848
+ const { settings, changed } = enabled ? upsertHooks(current, variant, capture) : removeHooks(current);
574
849
  if (changed) await writeSettings(path, settings);
575
850
  return changed;
576
- }
851
+ },
852
+ capabilities: async (root) => hookCapabilities(await readSettings(claudeSettingsPath(root)))
853
+ },
854
+ "codex-hooks": {
855
+ path: codexHooksPath,
856
+ installed: async (root) => (await codexHookCapabilities(root)).injection !== null,
857
+ variant: async (root) => (await codexHookCapabilities(root)).injection,
858
+ set: setCodexHooks,
859
+ capabilities: codexHookCapabilities
577
860
  },
578
861
  "opencode-plugin": {
579
862
  path: opencodePluginPath,
580
863
  installed: opencodePluginInstalled,
581
864
  variant: opencodePluginVariant,
582
- set: setOpencodePlugin
865
+ set: setOpencodePlugin,
866
+ capabilities: opencodePluginCapabilities
583
867
  },
584
868
  "hermes-plugin": {
585
869
  path: hermesPluginDir,
586
870
  installed: hermesPluginInstalled,
587
871
  variant: hermesPluginVariant,
588
- set: setHermesPlugin
872
+ set: setHermesPlugin,
873
+ capabilities: hermesPluginCapabilities
589
874
  }
590
875
  };
591
- async function setClientHooks(root, client, enabled, variant = "full") {
876
+ async function setClientHooks(root, client, enabled, variant = "full", capture = defaultCapture(variant)) {
877
+ const surface = CLIENT_ADAPTERS[client].hooks;
878
+ if (!surface) return null;
879
+ const { path, set, capabilities } = SURFACES[surface];
880
+ if (enabled && !capabilities && capture !== defaultCapture(variant)) {
881
+ throw new Error(
882
+ `${CLIENT_ADAPTERS[client].name} hooks cannot set capture separately: ${variant} hooks ${defaultCapture(variant) ? "always" : "never"} capture.`
883
+ );
884
+ }
885
+ return { target: path(root), changed: await set(root, enabled, variant, capture) };
886
+ }
887
+ function supportsCaptureOption(client) {
888
+ const surface = CLIENT_ADAPTERS[client].hooks;
889
+ return surface !== void 0 && SURFACES[surface].capabilities !== void 0;
890
+ }
891
+ async function clientHookCapabilities(root, client) {
592
892
  const surface = CLIENT_ADAPTERS[client].hooks;
593
893
  if (!surface) return null;
594
- const { path, set } = SURFACES[surface];
595
- return { target: path(root), changed: await set(root, enabled, variant) };
894
+ const { capabilities, variant } = SURFACES[surface];
895
+ if (capabilities) return capabilities(root);
896
+ const installed = await variant(root);
897
+ return {
898
+ injection: installed,
899
+ capture: installed ? defaultCapture(installed) : false,
900
+ recovery: false,
901
+ reset: installed !== null,
902
+ guard: false
903
+ };
596
904
  }
597
905
  async function clientHooksVariant(root, client) {
598
906
  const surface = CLIENT_ADAPTERS[client].hooks;
@@ -618,9 +926,9 @@ async function rulesFileHooked(root, client) {
618
926
 
619
927
  // src/integrations/project-mcp.ts
620
928
  import { access, mkdir as mkdir3, readFile as readFile5, rm as rm2 } from "fs/promises";
621
- import { dirname as dirname5, join as join4, resolve as resolve4 } from "path";
929
+ import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
622
930
  async function writeProjectMcpConfig(options) {
623
- const path = join4(options.repositoryRoot, ".mcp.json");
931
+ const path = join5(options.repositoryRoot, ".mcp.json");
624
932
  const current = await readProjectMcpConfig(path);
625
933
  current.mcpServers ??= {};
626
934
  current.mcpServers[`codebuddy-${options.namespace}`] = codebuddyMcpServer(
@@ -652,12 +960,12 @@ function codebuddyMcpServer(repositoryRoot, namespace) {
652
960
  env: {
653
961
  CODEBUDDY_PROJECT_ROOT: repositoryRoot,
654
962
  CODEBUDDY_NAMESPACE: namespace,
655
- CODEBUDDY_TOOL_PROFILE: "core"
963
+ CODEBUDDY_TOOL_PROFILE: "lean"
656
964
  }
657
965
  };
658
966
  }
659
967
  async function projectMcpPath(root, file, format) {
660
- const path = join4(root, file);
968
+ const path = join5(root, file);
661
969
  if (format !== "opencode" || await exists(path)) return path;
662
970
  return await exists(`${path}c`) ? `${path}c` : path;
663
971
  }
@@ -737,120 +1045,25 @@ async function readProjectMcpConfig(path) {
737
1045
 
738
1046
  // src/templates/install.ts
739
1047
  import { readFile as readFile6, rm as rm3 } from "fs/promises";
740
- import { join as join5, resolve as resolve5 } from "path";
1048
+ import { join as join6, resolve as resolve5 } from "path";
741
1049
 
742
1050
  // src/templates/workflow.ts
743
1051
  var WORKFLOW_CLIENTS = CLIENT_IDS;
744
- var INTRO = [
745
- "You have CodeBuddy available as a local, MCP-backed project-context engine.",
746
- "Use it to work like a careful senior engineer, not to dump broad context.",
747
- ""
748
- ];
749
- var START_AGENT = [
750
- "## At the start of a substantive coding turn",
751
- "",
752
- "1. When project context is needed, call `context_pack` with the user's task and a token budget (start",
753
- " with 4000). It automatically finds relevant files and returns bounded symbols,",
754
- " plan, policy, incidents, risks, architecture neighbours, and team facts.",
755
- " Reuse evidence already present. For a clearly identified small edit, read the",
756
- " needed source directly; fetch context for unknown dependencies or project constraints.",
757
- " Read missing function bodies before editing. If safety evidence was omitted,",
758
- " narrow the targets or increase the budget before proceeding.",
759
- "2. Use `context_bootstrap` only when you need broad project orientation beyond",
760
- " the task pack. Use `context_before_edit` when you already have exact paths",
761
- " or a plan and need a narrower follow-up.",
762
- "3. Prior work from earlier chats arrives as `handoffs` with freshness",
763
- " (`changedSince`, `missing`); re-check those files before relying on it.",
764
- "4. On repeated context calls in one conversation, pass a stable `sessionId` and",
765
- " the previous response's `delivery.deliveryId` as `ack`. Unchanged sections",
766
- " are then omitted and listed in `delivery.unchanged`. If earlier output is no",
767
- " longer in your context (e.g. after compaction), pass `full: true`.",
768
- ""
769
- ];
770
- var START_HOOKS = [
771
- "## At the start of a substantive coding turn",
772
- "",
773
- "CodeBuddy hooks inject a `CodeBuddy project context (auto-recalled)` block into",
774
- "each prompt: project, plan, policies, facts, incidents, handoffs, current",
775
- "changes, and risk. Agents without CodeBuddy hooks (no such block in the prompt)",
776
- "follow the agent-driven flow: `context_pack` first, `context_after_turn` last.",
777
- "",
778
- "1. Start from the injected block; do not call `context_bootstrap` for orientation",
779
- " it already covers. Its `task files` are CodeBuddy's best match; when it names",
780
- " none, call `context_pack` with the task before searching the repository yourself.",
781
- "2. Read ranges, not whole files: `Read` with offset/limit, or `sed -n 'A,Bp'`.",
782
- " Large files read whole (`Read` without a range, `cat`, `less`) are denied with an",
783
- " outline of their symbols and line ranges; read the range you need from it.",
784
- "3. Call `context_pack` (or `context_before_edit` with exact paths) when the",
785
- " task needs symbols, neighbours, or file-level evidence the block lacks. Pass the",
786
- " block's `session` value as `sessionId` and the previous `delivery.deliveryId`",
787
- " as `ack` so unchanged sections are not resent.",
788
- "4. Prior work from earlier chats arrives as handoffs with freshness; re-check",
789
- " those files before relying on it.",
790
- ""
1052
+ var HOOKS_RULES = [
1053
+ "- The prompt's `CodeBuddy` block names likely files and line ranges: read those ranges",
1054
+ " first (ranged `Read` or `sed -n`); search only if they do not fit.",
1055
+ "- Call `context_pack` only when it names no files. Hooks inject and capture: never call",
1056
+ " `context_bootstrap` or `context_after_turn`.",
1057
+ "- `recall` answers project-convention questions."
791
1058
  ];
792
- var MIDDLE = [
793
- "## While implementing",
794
- "",
795
- "- Prefer the smallest sufficient set of edits; preserve existing behavior.",
796
- "- Honor risk warnings and policy rules for the files you touch; call out risk",
797
- " before editing sensitive areas.",
798
- "- Do not re-request broad context if the task pack already suffices.",
799
- "",
800
- "## Before finalizing",
801
- "",
802
- "- Consider calling `code_suggestions` (with `paths`, `planId`, or `useGit`) for",
803
- " read-only, evidence-backed quality findings; resolve high-severity ones.",
804
- "- Run the smallest relevant verification (tests, lint, typecheck) and fix",
805
- " failures before presenting completion.",
806
- ""
807
- ];
808
- var AFTER_AGENT = [
809
- "## After a meaningful turn",
810
- "",
811
- "- Call `context_after_turn` with a concise `summary`, the `changedFiles`, and a",
812
- " `planId` if one is active, plus `verification` (commands run and results)",
813
- " so the turn's worklog handoff records it. CodeBuddy auto-saves confident facts, decisions,",
814
- " and incidents, queues uncertain or sensitive items for review, and drops",
815
- " chatter. Never paste secrets, tokens, or credentials into the summary.",
816
- ""
817
- ];
818
- var AFTER_HOOKS = [
819
- "## After a meaningful turn",
820
- "",
821
- "- Do not call `context_after_turn`: the turn-end hook captures the turn once, from",
822
- " the real change set and the verification commands you ran. A second capture",
823
- " would record a divergent worklog entry.",
824
- "- Never paste secrets, tokens, or credentials into your final message; it is the",
825
- " capture summary.",
826
- ""
827
- ];
828
- var TAIL = [
829
- "## If a tool or service is unavailable (degraded mode)",
830
- "",
831
- "- A CodeBuddy MCP tool errors or is missing: proceed using the repository",
832
- " itself as the source of truth; do not block on it.",
833
- "- Postgres is unreachable: the file-based tools (context, plan, risk, map,",
834
- " suggest) still work; recall and embeddings may be degraded \u2014 say so instead",
835
- " of inventing remembered facts.",
836
- "- Retrieval returns no context: read the relevant files directly rather than",
837
- " guessing; the working tree is authoritative.",
838
- "",
839
- "## Never",
840
- "",
841
- "- Invent behavior the codebase does not support.",
842
- "- Ignore failing tests or lint.",
843
- "- Make speculative, wide changes without evidence.",
844
- "- Save low-confidence project memory as a durable fact."
1059
+ var AGENT_RULES = [
1060
+ "- In unfamiliar code, call `context_pack` once with the task; read its line ranges first.",
1061
+ "- `recall` answers project-convention questions; `remember` durable facts (never secrets).",
1062
+ "- After changing code, call `context_after_turn` (summary, changed files) if listed."
845
1063
  ];
1064
+ var TAIL = "- CodeBuddy unavailable (degraded mode): use the repository directly.";
846
1065
  function renderBody(mode) {
847
- return [
848
- ...INTRO,
849
- ...mode === "hooks" ? START_HOOKS : START_AGENT,
850
- ...MIDDLE,
851
- ...mode === "hooks" ? AFTER_HOOKS : AFTER_AGENT,
852
- ...TAIL
853
- ].join("\n");
1066
+ return [...mode === "hooks" ? HOOKS_RULES : AGENT_RULES, TAIL].join("\n");
854
1067
  }
855
1068
  function rulesReaders(file) {
856
1069
  return CLIENT_IDS.filter((id) => CLIENT_ADAPTERS[id].rulesFile === file).map((id) => CLIENT_ADAPTERS[id].name).join(", ");
@@ -860,9 +1073,7 @@ function renderWorkflowTemplate(client, mode = "agent") {
860
1073
  return [
861
1074
  `# CodeBuddy workflow (${rulesReaders(file)})`,
862
1075
  "",
863
- "This section is managed by CodeBuddy (`codebuddy clients install <client> --mode <mode>`)",
864
- `and lives in \`${file}\`. Edit outside the managed markers to customize.`,
865
- `Automation mode: \`${mode}\` (${MODE_LABELS[mode]}).`,
1076
+ `Managed by CodeBuddy; edit outside the markers. Automation mode: \`${mode}\`.`,
866
1077
  "",
867
1078
  renderBody(mode)
868
1079
  ].join("\n");
@@ -888,7 +1099,7 @@ ${END_MARKER}
888
1099
  }
889
1100
  async function installWorkflowRules(options) {
890
1101
  const projectRoot = resolve5(options.projectRoot ?? process.cwd());
891
- const path = options.targetPath ?? join5(projectRoot, defaultRulesFile(options.client));
1102
+ const path = options.targetPath ?? join6(projectRoot, defaultRulesFile(options.client));
892
1103
  const existing = await readIfPresent(path);
893
1104
  const mode = options.mode ?? await defaultRulesMode(options.client, projectRoot);
894
1105
  if (options.onlyIfManaged && (existing === null || !hasManagedBlock(existing))) {
@@ -954,21 +1165,21 @@ async function readIfPresent(path) {
954
1165
  import { existsSync as existsSync2 } from "fs";
955
1166
  import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
956
1167
  import { homedir } from "os";
957
- import { dirname as dirname7, join as join7 } from "path";
1168
+ import { dirname as dirname7, join as join8 } from "path";
958
1169
 
959
1170
  // src/cli/commands/claude-desktop.ts
960
1171
  import { existsSync } from "fs";
961
1172
  import { mkdir as mkdir4, readFile as readFile7 } from "fs/promises";
962
- import { dirname as dirname6, join as join6, resolve as resolve6 } from "path";
1173
+ import { dirname as dirname6, join as join7, resolve as resolve6 } from "path";
963
1174
  import { fileURLToPath } from "url";
964
1175
  function claudeDesktopConfigPath(home = process.env.HOME ?? "") {
965
1176
  if (process.platform === "darwin") {
966
- return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1177
+ return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
967
1178
  }
968
1179
  if (process.platform === "win32") {
969
- return join6(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
1180
+ return join7(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
970
1181
  }
971
- return join6(home, ".config", "Claude", "claude_desktop_config.json");
1182
+ return join7(home, ".config", "Claude", "claude_desktop_config.json");
972
1183
  }
973
1184
  async function readClaudeConfig(path) {
974
1185
  if (!existsSync(path)) return {};
@@ -1051,7 +1262,7 @@ async function listClaudeEntries() {
1051
1262
 
1052
1263
  // src/cli/commands/codex.ts
1053
1264
  function codexConfigPath(home = homedir()) {
1054
- return join7(home, ".codex", "config.toml");
1265
+ return join8(home, ".codex", "config.toml");
1055
1266
  }
1056
1267
  async function readCodexConfig(path) {
1057
1268
  if (!existsSync2(path)) return "";
@@ -1163,11 +1374,11 @@ function escapeRegExp(value) {
1163
1374
  // src/integrations/hermes-config.ts
1164
1375
  import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
1165
1376
  import { homedir as homedir2 } from "os";
1166
- import { dirname as dirname8, join as join8 } from "path";
1377
+ import { dirname as dirname8, join as join9 } from "path";
1167
1378
  import { isDeepStrictEqual } from "util";
1168
1379
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1169
1380
  function hermesConfigPath(home = homedir2()) {
1170
- return join8(process.env.HERMES_HOME ?? join8(home, ".hermes"), "config.yaml");
1381
+ return join9(process.env.HERMES_HOME ?? join9(home, ".hermes"), "config.yaml");
1171
1382
  }
1172
1383
  async function readRaw(path) {
1173
1384
  try {
@@ -1273,7 +1484,7 @@ async function setGlobalMcp(target, options) {
1273
1484
  namespace,
1274
1485
  projectRoot: root,
1275
1486
  serverName: name,
1276
- toolProfile: "core"
1487
+ toolProfile: "lean"
1277
1488
  });
1278
1489
  return result.created ? "created" : result.changed ? "updated" : "unchanged";
1279
1490
  }
@@ -1290,10 +1501,16 @@ async function installClient(options) {
1290
1501
  `${adapter.name} does not support ${MODE_LABELS[options.mode]} mode: ${adapter.gap ?? "no lifecycle hooks"}.`
1291
1502
  );
1292
1503
  }
1293
- return apply(options, options.mode, options.mode !== "lean");
1504
+ const variant = options.mode === "lean" ? "lean" : "full";
1505
+ if (options.capture !== void 0 && options.capture !== defaultCapture(variant) && !supportsCaptureOption(options.client)) {
1506
+ throw new Error(
1507
+ `${adapter.name} hooks cannot set capture separately: ${variant} hooks ${defaultCapture(variant) ? "always" : "never"} capture.`
1508
+ );
1509
+ }
1510
+ return apply(options, options.mode, options.mode !== "lean", options.capture);
1294
1511
  }
1295
1512
  async function ensureIndex(root) {
1296
- const target = join9(root, ".codebuddy", "cache", "index.json");
1513
+ const target = join10(root, ".codebuddy", "cache", "index.json");
1297
1514
  const existed = await access2(target).then(
1298
1515
  () => true,
1299
1516
  () => false
@@ -1304,7 +1521,7 @@ async function ensureIndex(root) {
1304
1521
  async function uninstallClient(options) {
1305
1522
  return apply(options, "mcp-only", false);
1306
1523
  }
1307
- async function apply(options, mode, keepMcp) {
1524
+ async function apply(options, mode, keepMcp, capture) {
1308
1525
  const root = resolve7(options.projectRoot ?? process.cwd());
1309
1526
  const adapter = CLIENT_ADAPTERS[options.client];
1310
1527
  const steps = [];
@@ -1314,7 +1531,7 @@ async function apply(options, mode, keepMcp) {
1314
1531
  const shared = sharedRulesReaders(options.client);
1315
1532
  const liveSiblings = mode === "mcp-only" || mode === "lean" ? await installedWithMcp(root, shared, namespace) : [];
1316
1533
  if (liveSiblings.length > 0) {
1317
- const path = join9(root, adapter.rulesFile);
1534
+ const path = join10(root, adapter.rulesFile);
1318
1535
  steps.push({ surface: "rules", target: path, action: "kept" });
1319
1536
  notes.push(
1320
1537
  `Kept the CodeBuddy block in ${adapter.rulesFile}: still used by ${liveSiblings.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
@@ -1343,7 +1560,8 @@ async function apply(options, mode, keepMcp) {
1343
1560
  root,
1344
1561
  mode === "hooks" || mode === "lean",
1345
1562
  options.client,
1346
- mode === "lean" ? "lean" : "full"
1563
+ mode === "lean" ? "lean" : "full",
1564
+ capture
1347
1565
  );
1348
1566
  if (hooks) steps.push(hooks);
1349
1567
  if (mode === "lean") steps.push(await ensureIndex(root));
@@ -1385,8 +1603,8 @@ async function installedWithMcp(root, clients, namespace) {
1385
1603
  }
1386
1604
  return live;
1387
1605
  }
1388
- async function setHooks(root, enabled, client = "claude", variant = "full") {
1389
- const result = await setClientHooks(root, client, enabled, variant);
1606
+ async function setHooks(root, enabled, client = "claude", variant = "full", capture) {
1607
+ const result = await setClientHooks(root, client, enabled, variant, capture);
1390
1608
  if (!result) return null;
1391
1609
  const action = result.changed ? enabled ? "installed" : "removed" : "unchanged";
1392
1610
  return { surface: "hooks", target: result.target, action };
@@ -1394,7 +1612,7 @@ async function setHooks(root, enabled, client = "claude", variant = "full") {
1394
1612
  async function detectClient(client, projectRoot = process.cwd(), namespace) {
1395
1613
  const root = resolve7(projectRoot);
1396
1614
  const adapter = CLIENT_ADAPTERS[client];
1397
- const rulesMode = await readManagedMode(join9(root, adapter.rulesFile));
1615
+ const rulesMode = await readManagedMode(join10(root, adapter.rulesFile));
1398
1616
  const hooks = await clientHooksInstalled(root, client);
1399
1617
  const name = `codebuddy-${namespace ?? await resolveNamespaceFrom(root)}`;
1400
1618
  const mcp = "file" in adapter.mcp ? await hasMcpServer(
@@ -1413,13 +1631,16 @@ async function detectClient(client, projectRoot = process.cwd(), namespace) {
1413
1631
  `${adapter.rulesFile} is hook-driven but hooks are not installed: turns are not captured.`
1414
1632
  );
1415
1633
  }
1416
- const effectiveRules = hooks === null && rulesMode === "hooks" ? "agent" : rulesMode;
1634
+ const effectiveRules = hooks !== true && rulesMode === "hooks" ? "agent" : rulesMode;
1417
1635
  const mode = hooks ? await clientHooksVariant(root, client) === "lean" ? "lean" : "hooks" : effectiveRules ?? (mcp ? "mcp-only" : "none");
1418
1636
  return { client, mode, rulesMode, hooks, mcp, warnings };
1419
1637
  }
1420
1638
 
1421
1639
  export {
1640
+ runBridgeHook,
1641
+ runCodexHook,
1422
1642
  hermesPluginInstalled,
1643
+ clientHookCapabilities,
1423
1644
  clientHooksInstalled,
1424
1645
  writeProjectMcpConfig,
1425
1646
  WORKFLOW_CLIENTS,
@@ -1441,4 +1662,4 @@ export {
1441
1662
  setHooks,
1442
1663
  detectClient
1443
1664
  };
1444
- //# sourceMappingURL=chunk-J4T6HHI4.js.map
1665
+ //# sourceMappingURL=chunk-L6BRYZ6G.js.map