@orquentra/codebuddy 2.8.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE_CODEX.md +33 -0
- package/README.md +27 -3
- package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
- package/dist/{chunk-GKNYMNFM.js → chunk-4DKYKDHI.js} +6 -1
- package/dist/chunk-4DKYKDHI.js.map +1 -0
- package/dist/{chunk-6KGYAFG7.js → chunk-7QMHIJBA.js} +30 -3
- package/dist/chunk-7QMHIJBA.js.map +1 -0
- package/dist/{chunk-6NZEZJYD.js → chunk-ESC4Y3L7.js} +3 -5
- package/dist/{chunk-6NZEZJYD.js.map → chunk-ESC4Y3L7.js.map} +1 -1
- package/dist/chunk-GQKWUHMA.js +20 -0
- package/dist/chunk-GQKWUHMA.js.map +1 -0
- package/dist/{chunk-J4T6HHI4.js → chunk-L6BRYZ6G.js} +537 -316
- package/dist/chunk-L6BRYZ6G.js.map +1 -0
- package/dist/{chunk-UK6XTS2Y.js → chunk-LRQNPYCK.js} +13 -19
- package/dist/chunk-LRQNPYCK.js.map +1 -0
- package/dist/{chunk-GXOML4UZ.js → chunk-LW75SKZR.js} +2 -2
- package/dist/{chunk-EJQVGFH2.js → chunk-UOTKHSQ5.js} +26 -9
- package/dist/chunk-UOTKHSQ5.js.map +1 -0
- package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
- package/dist/chunk-V7PT6ER5.js.map +1 -0
- package/dist/{chunk-ZNLSCV3J.js → chunk-XSNW6Y3X.js} +1253 -315
- package/dist/chunk-XSNW6Y3X.js.map +1 -0
- package/dist/{chunk-6DONJW3W.js → chunk-YJPCS2GP.js} +274 -69
- package/dist/chunk-YJPCS2GP.js.map +1 -0
- package/dist/{chunk-XL2HLACE.js → chunk-YN7TEIUH.js} +1 -1
- package/dist/chunk-YN7TEIUH.js.map +1 -0
- package/dist/{chunk-TLWGZVPC.js → chunk-YNSTIMCF.js} +746 -66
- package/dist/chunk-YNSTIMCF.js.map +1 -0
- package/dist/{chunk-NY2YXP3R.js → chunk-YWDH6DQ6.js} +70 -4
- package/dist/chunk-YWDH6DQ6.js.map +1 -0
- package/dist/cli/index.js +23 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +9 -9
- package/dist/install-EU35ERK5.js +30 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-43ADYTBW.js +28 -0
- package/dist/{main-QUASSFP7.js → main-SFC5LV4E.js} +1333 -437
- package/dist/main-SFC5LV4E.js.map +1 -0
- package/dist/{operations-WJR7AZZF.js → operations-6Z6ZDVBP.js} +3 -3
- package/dist/{pretool-command-JTKX6G3M.js → pretool-command-B5GY5T6E.js} +3 -3
- package/dist/prompt-gate-TDTFFQW2.js +8 -0
- package/dist/{runtime-OQQ22VJF.js → runtime-W5AW32SD.js} +15 -7
- package/dist/{summaries-ML53R7EL.js → summaries-32PHOB6L.js} +3 -4
- package/dist/{zvec-XDZNZ2OL.js → zvec-24HLAFGN.js} +3 -4
- package/dist/zvec-24HLAFGN.js.map +1 -0
- package/package.json +11 -2
- package/dist/chunk-6DONJW3W.js.map +0 -1
- package/dist/chunk-6KGYAFG7.js.map +0 -1
- package/dist/chunk-EJQVGFH2.js.map +0 -1
- package/dist/chunk-GKNYMNFM.js.map +0 -1
- package/dist/chunk-ITQO632W.js +0 -673
- package/dist/chunk-ITQO632W.js.map +0 -1
- package/dist/chunk-J4T6HHI4.js.map +0 -1
- package/dist/chunk-NY2YXP3R.js.map +0 -1
- package/dist/chunk-TLWGZVPC.js.map +0 -1
- package/dist/chunk-UK6XTS2Y.js.map +0 -1
- package/dist/chunk-W7GAXL6C.js.map +0 -1
- package/dist/chunk-XL2HLACE.js.map +0 -1
- package/dist/chunk-ZNLSCV3J.js.map +0 -1
- package/dist/install-BXM56QCD.js +0 -19
- package/dist/lean-command-7DFW32MA.js +0 -19
- package/dist/main-QUASSFP7.js.map +0 -1
- /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
- /package/dist/{chunk-GXOML4UZ.js.map → chunk-LW75SKZR.js.map} +0 -0
- /package/dist/{install-BXM56QCD.js.map → install-EU35ERK5.js.map} +0 -0
- /package/dist/{lean-command-7DFW32MA.js.map → lean-43ADYTBW.js.map} +0 -0
- /package/dist/{operations-WJR7AZZF.js.map → operations-6Z6ZDVBP.js.map} +0 -0
- /package/dist/{pretool-command-JTKX6G3M.js.map → pretool-command-B5GY5T6E.js.map} +0 -0
- /package/dist/{runtime-OQQ22VJF.js.map → prompt-gate-TDTFFQW2.js.map} +0 -0
- /package/dist/{summaries-ML53R7EL.js.map → runtime-W5AW32SD.js.map} +0 -0
- /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-
|
|
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-
|
|
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
|
|
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
|
|
24
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
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":
|
|
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("
|
|
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
|
-
` : `${
|
|
142
|
-
# Generated by "codebuddy clients install hermes --mode
|
|
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 = {"
|
|
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":
|
|
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
|
-
|
|
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)
|
|
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
|
|
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:
|
|
236
|
-
content:
|
|
575
|
+
path: join3(dir, "plugin.yaml"),
|
|
576
|
+
content: capture ? HERMES_PLUGIN_MANIFEST : HERMES_LEAN_MANIFEST
|
|
237
577
|
},
|
|
238
|
-
{ path:
|
|
578
|
+
{ path: join3(dir, "__init__.py"), content: renderHermesPlugin(variant, capture) }
|
|
239
579
|
],
|
|
240
|
-
dirs: [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
|
|
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
|
|
257
|
-
import { dirname as
|
|
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
|
-
|
|
314
|
-
|
|
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:
|
|
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("
|
|
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
|
-
` : `${
|
|
349
|
-
// Generated by \`codebuddy clients install opencode --mode
|
|
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 = {
|
|
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("
|
|
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())
|
|
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
|
|
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: [
|
|
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
|
|
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 {
|
|
595
|
-
|
|
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
|
|
929
|
+
import { dirname as dirname5, join as join5, resolve as resolve4 } from "path";
|
|
622
930
|
async function writeProjectMcpConfig(options) {
|
|
623
|
-
const path =
|
|
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: "
|
|
963
|
+
CODEBUDDY_TOOL_PROFILE: "lean"
|
|
656
964
|
}
|
|
657
965
|
};
|
|
658
966
|
}
|
|
659
967
|
async function projectMcpPath(root, file, format) {
|
|
660
|
-
const path =
|
|
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
|
|
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
|
|
745
|
-
"
|
|
746
|
-
"
|
|
747
|
-
""
|
|
748
|
-
|
|
749
|
-
|
|
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
|
|
793
|
-
"
|
|
794
|
-
"",
|
|
795
|
-
"-
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
1177
|
+
return join7(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
967
1178
|
}
|
|
968
1179
|
if (process.platform === "win32") {
|
|
969
|
-
return
|
|
1180
|
+
return join7(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
|
|
970
1181
|
}
|
|
971
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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-
|
|
1665
|
+
//# sourceMappingURL=chunk-L6BRYZ6G.js.map
|