@orquentra/codebuddy 2.6.2 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/adapters-S4HHTWJW.js +24 -0
- package/dist/{chunk-IBLJOHJL.js → chunk-3QGW53GD.js} +2 -2
- package/dist/{chunk-IMFUYEZA.js → chunk-6DONJW3W.js} +232 -64
- package/dist/chunk-6DONJW3W.js.map +1 -0
- package/dist/{chunk-JFDRBPYF.js → chunk-6JCKMIQY.js} +2 -2
- package/dist/{chunk-S6VEG4S2.js → chunk-6KGYAFG7.js} +28 -5
- package/dist/chunk-6KGYAFG7.js.map +1 -0
- package/dist/{chunk-OWWCDRXW.js → chunk-6NZEZJYD.js} +9 -7
- package/dist/chunk-6NZEZJYD.js.map +1 -0
- package/dist/{chunk-GDJH7XJC.js → chunk-AL6VBTNA.js} +77 -3
- package/dist/chunk-AL6VBTNA.js.map +1 -0
- package/dist/{chunk-XBJQKCLU.js → chunk-CAEPHLHC.js} +13 -9
- package/dist/chunk-CAEPHLHC.js.map +1 -0
- package/dist/chunk-CG5B3LYG.js +66 -0
- package/dist/chunk-CG5B3LYG.js.map +1 -0
- package/dist/{chunk-IHQM2ZDV.js → chunk-DFV2SK6C.js} +1 -1
- package/dist/{chunk-IHQM2ZDV.js.map → chunk-DFV2SK6C.js.map} +1 -1
- package/dist/{chunk-QBDJXGG5.js → chunk-EJQVGFH2.js} +93 -22
- package/dist/chunk-EJQVGFH2.js.map +1 -0
- package/dist/{chunk-5MQOE4J6.js → chunk-ESZXCZIN.js} +3 -3
- package/dist/{chunk-J2EAZ4MA.js → chunk-GKNYMNFM.js} +22 -25
- package/dist/chunk-GKNYMNFM.js.map +1 -0
- package/dist/{chunk-5FW3PMNP.js → chunk-GXOML4UZ.js} +76 -11
- package/dist/chunk-GXOML4UZ.js.map +1 -0
- package/dist/{chunk-JPHJBCZB.js → chunk-HPOR6YXA.js} +3 -2
- package/dist/chunk-HPOR6YXA.js.map +1 -0
- package/dist/chunk-ITQO632W.js +673 -0
- package/dist/chunk-ITQO632W.js.map +1 -0
- package/dist/chunk-J4T6HHI4.js +1444 -0
- package/dist/chunk-J4T6HHI4.js.map +1 -0
- package/dist/{chunk-5KZY6OTG.js → chunk-KNR2P2UY.js} +33 -83
- package/dist/chunk-KNR2P2UY.js.map +1 -0
- package/dist/{chunk-2XDPKFUW.js → chunk-NY2YXP3R.js} +46 -20
- package/dist/chunk-NY2YXP3R.js.map +1 -0
- package/dist/{chunk-RAJJSCIP.js → chunk-SFMIMLXR.js} +2 -2
- package/dist/chunk-TGFTKWOR.js +16 -0
- package/dist/chunk-TGFTKWOR.js.map +1 -0
- package/dist/{chunk-NERKZSSE.js → chunk-TLWGZVPC.js} +395 -686
- package/dist/chunk-TLWGZVPC.js.map +1 -0
- package/dist/{chunk-B6CO63SQ.js → chunk-UK6XTS2Y.js} +24 -10
- package/dist/chunk-UK6XTS2Y.js.map +1 -0
- package/dist/chunk-W7GAXL6C.js +104 -0
- package/dist/chunk-W7GAXL6C.js.map +1 -0
- package/dist/chunk-XL2HLACE.js +55 -0
- package/dist/chunk-XL2HLACE.js.map +1 -0
- package/dist/chunk-Y7D7PUBE.js +321 -0
- package/dist/chunk-Y7D7PUBE.js.map +1 -0
- package/dist/{chunk-R7CD6EXS.js → chunk-ZNLSCV3J.js} +578 -381
- package/dist/chunk-ZNLSCV3J.js.map +1 -0
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/{cloud-7WM3YLI4.js → cloud-LXZUR3U2.js} +3 -3
- package/dist/{codebuddy-B_pCaQrV.d.ts → codebuddy-DO7ZhoWn.d.ts} +15 -1
- package/dist/config-GEZFLXK6.js +12 -0
- package/dist/index.d.ts +46 -3
- package/dist/index.js +23 -19
- package/dist/install-BXM56QCD.js +19 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-command-7DFW32MA.js +19 -0
- package/dist/{local-5MZURLCZ.js → local-ST4WMRCG.js} +3 -3
- package/dist/{main-MTBQZGJV.js → main-QUASSFP7.js} +987 -1741
- package/dist/main-QUASSFP7.js.map +1 -0
- package/dist/{memory-KIC5FJVW.js → memory-TGHVTBME.js} +9 -8
- package/dist/memory-TGHVTBME.js.map +1 -0
- package/dist/operations-WJR7AZZF.js +26 -0
- package/dist/pretool-command-JTKX6G3M.js +13 -0
- package/dist/{rerank-5QZ3PNUW.js → rerank-IQAEWLQR.js} +6 -5
- package/dist/rerank-IQAEWLQR.js.map +1 -0
- package/dist/{runtime-CUBOOPZO.js → runtime-OQQ22VJF.js} +15 -11
- package/dist/summaries-ML53R7EL.js +21 -0
- package/dist/summaries-ML53R7EL.js.map +1 -0
- package/dist/{zvec-MSRKT7IV.js → zvec-XDZNZ2OL.js} +11 -3
- package/dist/zvec-XDZNZ2OL.js.map +1 -0
- package/package.json +1 -1
- package/src/db/migrations/0006_fact_idempotency_key.sql +2 -0
- package/src/db/migrations/meta/0006_snapshot.json +1258 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/dist/chunk-2XDPKFUW.js.map +0 -1
- package/dist/chunk-5FW3PMNP.js.map +0 -1
- package/dist/chunk-5KZY6OTG.js.map +0 -1
- package/dist/chunk-B6CO63SQ.js.map +0 -1
- package/dist/chunk-GDJH7XJC.js.map +0 -1
- package/dist/chunk-IMFUYEZA.js.map +0 -1
- package/dist/chunk-J2EAZ4MA.js.map +0 -1
- package/dist/chunk-JPHJBCZB.js.map +0 -1
- package/dist/chunk-NERKZSSE.js.map +0 -1
- package/dist/chunk-OWWCDRXW.js.map +0 -1
- package/dist/chunk-PQK3GKZH.js +0 -98
- package/dist/chunk-PQK3GKZH.js.map +0 -1
- package/dist/chunk-QBDJXGG5.js.map +0 -1
- package/dist/chunk-R7CD6EXS.js.map +0 -1
- package/dist/chunk-S6VEG4S2.js.map +0 -1
- package/dist/chunk-XBJQKCLU.js.map +0 -1
- package/dist/config-DEVHUDIU.js +0 -11
- package/dist/lean-command-HTQCPH76.js +0 -15
- package/dist/main-MTBQZGJV.js.map +0 -1
- package/dist/memory-KIC5FJVW.js.map +0 -1
- package/dist/operations-POCHY3WR.js +0 -25
- package/dist/pretool-command-AX6JXP25.js +0 -10
- package/dist/rerank-5QZ3PNUW.js.map +0 -1
- package/dist/summaries-SB5L2TSW.js +0 -18
- /package/dist/{cloud-7WM3YLI4.js.map → adapters-S4HHTWJW.js.map} +0 -0
- /package/dist/{chunk-IBLJOHJL.js.map → chunk-3QGW53GD.js.map} +0 -0
- /package/dist/{chunk-JFDRBPYF.js.map → chunk-6JCKMIQY.js.map} +0 -0
- /package/dist/{chunk-5MQOE4J6.js.map → chunk-ESZXCZIN.js.map} +0 -0
- /package/dist/{chunk-RAJJSCIP.js.map → chunk-SFMIMLXR.js.map} +0 -0
- /package/dist/{config-DEVHUDIU.js.map → cloud-LXZUR3U2.js.map} +0 -0
- /package/dist/{lean-command-HTQCPH76.js.map → config-GEZFLXK6.js.map} +0 -0
- /package/dist/{local-5MZURLCZ.js.map → install-BXM56QCD.js.map} +0 -0
- /package/dist/{operations-POCHY3WR.js.map → lean-command-7DFW32MA.js.map} +0 -0
- /package/dist/{pretool-command-AX6JXP25.js.map → local-ST4WMRCG.js.map} +0 -0
- /package/dist/{runtime-CUBOOPZO.js.map → operations-WJR7AZZF.js.map} +0 -0
- /package/dist/{summaries-SB5L2TSW.js.map → pretool-command-JTKX6G3M.js.map} +0 -0
- /package/dist/{zvec-MSRKT7IV.js.map → runtime-OQQ22VJF.js.map} +0 -0
|
@@ -0,0 +1,1444 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLIENT_ADAPTERS,
|
|
3
|
+
CLIENT_IDS,
|
|
4
|
+
MODE_LABELS,
|
|
5
|
+
sharedRulesReaders,
|
|
6
|
+
supportedModes
|
|
7
|
+
} from "./chunk-W7GAXL6C.js";
|
|
8
|
+
import {
|
|
9
|
+
indexRepository
|
|
10
|
+
} from "./chunk-ITQO632W.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveNamespaceFrom
|
|
13
|
+
} from "./chunk-KNR2P2UY.js";
|
|
14
|
+
import {
|
|
15
|
+
writeAtomic
|
|
16
|
+
} from "./chunk-CG5B3LYG.js";
|
|
17
|
+
|
|
18
|
+
// src/clients/install.ts
|
|
19
|
+
import { access as access2 } from "fs/promises";
|
|
20
|
+
import { join as join9, resolve as resolve7 } from "path";
|
|
21
|
+
|
|
22
|
+
// src/hooks/hermes-plugin.ts
|
|
23
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
24
|
+
import { dirname as dirname2, join, resolve } from "path";
|
|
25
|
+
|
|
26
|
+
// src/hooks/managed-plugin.ts
|
|
27
|
+
import { mkdir, readFile, rm, rmdir } from "fs/promises";
|
|
28
|
+
import { dirname } from "path";
|
|
29
|
+
async function read(path) {
|
|
30
|
+
try {
|
|
31
|
+
return await readFile(path, "utf8");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === "ENOENT") return null;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function managedPluginInstalled(plugin3) {
|
|
38
|
+
for (const file of plugin3.files) {
|
|
39
|
+
if (!(await read(file.path))?.startsWith(plugin3.marker)) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
async function setManagedPlugin(plugin3, enabled) {
|
|
44
|
+
const current = await Promise.all(plugin3.files.map((file) => read(file.path)));
|
|
45
|
+
const foreign = plugin3.files.find((_, i) => {
|
|
46
|
+
const text = current[i];
|
|
47
|
+
return text != null && !text.startsWith(plugin3.marker);
|
|
48
|
+
});
|
|
49
|
+
if (foreign) {
|
|
50
|
+
if (!enabled) return false;
|
|
51
|
+
throw new Error(
|
|
52
|
+
`${foreign.path} exists and is not managed by CodeBuddy; move it aside and re-run.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
let changed = false;
|
|
56
|
+
for (const [i, file] of plugin3.files.entries()) {
|
|
57
|
+
if (enabled && current[i] !== file.content) {
|
|
58
|
+
await mkdir(dirname(file.path), { recursive: true });
|
|
59
|
+
await writeAtomic(file.path, file.content, { mode: 420 });
|
|
60
|
+
changed = true;
|
|
61
|
+
} else if (!enabled && current[i] != null) {
|
|
62
|
+
await rm(file.path, { force: true });
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!enabled && changed) {
|
|
67
|
+
for (const dir of plugin3.dirs) await rmdir(dir).catch(() => {
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return changed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/hooks/hermes-plugin.ts
|
|
74
|
+
var HERMES_PLUGIN_MARKER = "# codebuddy-managed: hermes-plugin";
|
|
75
|
+
var HERMES_PLUGIN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1
|
|
76
|
+
name: codebuddy
|
|
77
|
+
version: 1.0.0
|
|
78
|
+
kind: standalone
|
|
79
|
+
description: CodeBuddy recall and capture bridge (codebuddy clients install hermes --mode hooks).
|
|
80
|
+
provides_hooks:
|
|
81
|
+
- pre_llm_call
|
|
82
|
+
- post_tool_call
|
|
83
|
+
- post_llm_call
|
|
84
|
+
- on_session_end
|
|
85
|
+
`;
|
|
86
|
+
var HERMES_LEAN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1 lean
|
|
87
|
+
name: codebuddy
|
|
88
|
+
version: 1.0.0
|
|
89
|
+
kind: standalone
|
|
90
|
+
description: CodeBuddy lean bridge (codebuddy clients install hermes --mode lean).
|
|
91
|
+
provides_hooks:
|
|
92
|
+
- pre_llm_call
|
|
93
|
+
`;
|
|
94
|
+
var HERMES_HOOK_FN = `def _hook(command, payload):
|
|
95
|
+
"""Run one fail-soft CodeBuddy hook; its stdout, or "" on any failure."""
|
|
96
|
+
try:
|
|
97
|
+
done = subprocess.run(
|
|
98
|
+
["codebuddy", "hooks", command],
|
|
99
|
+
input=json.dumps({"cwd": ROOT, **payload}),
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
timeout=TIMEOUT_S[command],
|
|
103
|
+
cwd=ROOT,
|
|
104
|
+
)
|
|
105
|
+
except Exception:
|
|
106
|
+
return ""
|
|
107
|
+
return done.stdout if done.returncode == 0 else ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
`;
|
|
111
|
+
function renderHermesPlugin(variant) {
|
|
112
|
+
return variant === "lean" ? `${HERMES_PLUGIN_MARKER} v1 lean
|
|
113
|
+
# Generated by "codebuddy clients install hermes --mode lean"; removed by
|
|
114
|
+
# "codebuddy clients uninstall hermes". Local edits are overwritten.
|
|
115
|
+
"""CodeBuddy lean bridge: forwards each prompt to "codebuddy hooks lean"."""
|
|
116
|
+
|
|
117
|
+
import json
|
|
118
|
+
import subprocess
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
|
|
121
|
+
# <root>/.hermes/plugins/codebuddy/__init__.py
|
|
122
|
+
ROOT = str(Path(__file__).absolute().parents[3])
|
|
123
|
+
TIMEOUT_S = {"lean": 2}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
${HERMES_HOOK_FN}def _pre_llm_call(session_id="", user_message="", **_):
|
|
127
|
+
if not isinstance(user_message, str) or not user_message.strip():
|
|
128
|
+
return None
|
|
129
|
+
payload = {
|
|
130
|
+
"session_id": session_id,
|
|
131
|
+
"hook_event_name": "pre_llm_call",
|
|
132
|
+
"ephemeral_context": True,
|
|
133
|
+
"prompt": user_message,
|
|
134
|
+
}
|
|
135
|
+
block = _hook("lean", payload).strip()
|
|
136
|
+
return {"context": block} if block else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def register(ctx):
|
|
140
|
+
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
|
|
143
|
+
# "codebuddy clients uninstall hermes". Local edits are overwritten.
|
|
144
|
+
"""CodeBuddy lifecycle bridge: forwards Hermes hooks to "codebuddy hooks <command>"."""
|
|
145
|
+
|
|
146
|
+
import json
|
|
147
|
+
import subprocess
|
|
148
|
+
import threading
|
|
149
|
+
from pathlib import Path
|
|
150
|
+
|
|
151
|
+
# <root>/.hermes/plugins/codebuddy/__init__.py
|
|
152
|
+
ROOT = str(Path(__file__).absolute().parents[3])
|
|
153
|
+
EDIT_TOOLS = {"write_file", "patch"}
|
|
154
|
+
TIMEOUT_S = {"context": 20, "stage": 10, "capture": 60}
|
|
155
|
+
_lock = threading.Lock()
|
|
156
|
+
_turns = {}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
${HERMES_HOOK_FN}def _turn(turn_id, session_id):
|
|
160
|
+
# Compression can rotate the session id mid-turn; a turn keeps its first id.
|
|
161
|
+
with _lock:
|
|
162
|
+
return _turns.setdefault(
|
|
163
|
+
turn_id or session_id, {"session": session_id, "text": "", "commands": []}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
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}
|
|
170
|
+
if isinstance(user_message, str) and user_message.strip():
|
|
171
|
+
payload["prompt"] = user_message
|
|
172
|
+
block = _hook("context", payload).strip()
|
|
173
|
+
return {"context": block} if block else None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id="", **_):
|
|
177
|
+
turn = _turn(turn_id, session_id)
|
|
178
|
+
args = args if isinstance(args, dict) else {}
|
|
179
|
+
if tool_name in EDIT_TOOLS:
|
|
180
|
+
# Paths only: write_file content never needs to cross the pipe.
|
|
181
|
+
tool_input = {k: args[k] for k in ("path", "mode", "patch") if k in args}
|
|
182
|
+
_hook("stage", {
|
|
183
|
+
"session_id": turn["session"],
|
|
184
|
+
"hook_event_name": "post_tool_call",
|
|
185
|
+
"tool_name": tool_name,
|
|
186
|
+
"tool_input": tool_input,
|
|
187
|
+
})
|
|
188
|
+
elif tool_name == "terminal" and isinstance(args.get("command"), str):
|
|
189
|
+
exit_code = None
|
|
190
|
+
if not args.get("background"): # a background process has not finished
|
|
191
|
+
try:
|
|
192
|
+
exit_code = json.loads(result).get("exit_code")
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
turn["commands"].append({
|
|
196
|
+
"command": args["command"],
|
|
197
|
+
"exit_code": exit_code if type(exit_code) is int else None,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _post_llm_call(session_id="", turn_id="", assistant_response="", **_):
|
|
202
|
+
if isinstance(assistant_response, str) and assistant_response.strip():
|
|
203
|
+
_turn(turn_id, session_id)["text"] = assistant_response
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _on_session_end(session_id="", turn_id="", **_):
|
|
207
|
+
with _lock:
|
|
208
|
+
turn = _turns.pop(turn_id or session_id, None)
|
|
209
|
+
turn = turn or {"session": session_id, "text": "", "commands": []}
|
|
210
|
+
payload = {"session_id": turn["session"], "hook_event_name": "on_session_end"}
|
|
211
|
+
if turn["text"]:
|
|
212
|
+
payload["last_assistant_message"] = turn["text"]
|
|
213
|
+
if turn["commands"]:
|
|
214
|
+
payload["shell_commands"] = turn["commands"][-20:]
|
|
215
|
+
_hook("capture", payload)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def register(ctx):
|
|
219
|
+
ctx.register_hook("pre_llm_call", _pre_llm_call)
|
|
220
|
+
ctx.register_hook("post_tool_call", _post_tool_call)
|
|
221
|
+
ctx.register_hook("post_llm_call", _post_llm_call)
|
|
222
|
+
ctx.register_hook("on_session_end", _on_session_end)
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
var HERMES_PLUGIN_SOURCE = renderHermesPlugin("full");
|
|
226
|
+
function hermesPluginDir(repositoryRoot) {
|
|
227
|
+
return join(resolve(repositoryRoot), ".hermes", "plugins", "codebuddy");
|
|
228
|
+
}
|
|
229
|
+
function plugin(repositoryRoot, variant = "full") {
|
|
230
|
+
const dir = hermesPluginDir(repositoryRoot);
|
|
231
|
+
return {
|
|
232
|
+
marker: HERMES_PLUGIN_MARKER,
|
|
233
|
+
files: [
|
|
234
|
+
{
|
|
235
|
+
path: join(dir, "plugin.yaml"),
|
|
236
|
+
content: variant === "lean" ? HERMES_LEAN_MANIFEST : HERMES_PLUGIN_MANIFEST
|
|
237
|
+
},
|
|
238
|
+
{ path: join(dir, "__init__.py"), content: renderHermesPlugin(variant) }
|
|
239
|
+
],
|
|
240
|
+
dirs: [dir, dirname2(dir), dirname2(dirname2(dir))]
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function hermesPluginInstalled(repositoryRoot) {
|
|
244
|
+
return managedPluginInstalled(plugin(repositoryRoot));
|
|
245
|
+
}
|
|
246
|
+
function setHermesPlugin(repositoryRoot, enabled, variant = "full") {
|
|
247
|
+
return setManagedPlugin(plugin(repositoryRoot, variant), enabled);
|
|
248
|
+
}
|
|
249
|
+
async function hermesPluginVariant(repositoryRoot) {
|
|
250
|
+
if (!await hermesPluginInstalled(repositoryRoot)) return null;
|
|
251
|
+
const source = await readFile2(join(hermesPluginDir(repositoryRoot), "__init__.py"), "utf8");
|
|
252
|
+
return source.startsWith(`${HERMES_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 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";
|
|
258
|
+
var OPENCODE_PLUGIN_MARKER = "// codebuddy-managed: opencode-plugin";
|
|
259
|
+
var OPENCODE_RUNTIME = `const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
260
|
+
let lastMs = 0;
|
|
261
|
+
let counter = 0;
|
|
262
|
+
|
|
263
|
+
/** Run one fail-soft CodeBuddy hook; resolves to its stdout, "" on any failure. */
|
|
264
|
+
function hook(command, payload) {
|
|
265
|
+
return new Promise((done) => {
|
|
266
|
+
let out = "";
|
|
267
|
+
let child;
|
|
268
|
+
try {
|
|
269
|
+
child = spawn("codebuddy", ["hooks", command], {
|
|
270
|
+
cwd: payload.cwd,
|
|
271
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
272
|
+
// npm installs a codebuddy.cmd shim on Windows, which spawn only runs through a shell.
|
|
273
|
+
shell: process.platform === "win32",
|
|
274
|
+
});
|
|
275
|
+
} catch {
|
|
276
|
+
done("");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const timer = setTimeout(() => child.kill(), TIMEOUT_MS[command]);
|
|
280
|
+
child.stdout.setEncoding("utf8");
|
|
281
|
+
child.stdout.on("data", (chunk) => {
|
|
282
|
+
out += chunk;
|
|
283
|
+
});
|
|
284
|
+
child.on("error", () => {
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
done("");
|
|
287
|
+
});
|
|
288
|
+
child.on("close", (code) => {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
done(code === 0 ? out : "");
|
|
291
|
+
});
|
|
292
|
+
child.stdin.on("error", () => {});
|
|
293
|
+
child.stdin.end(JSON.stringify(payload));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** An ascending OpenCode part id (prt_ + 48-bit time/counter hex + 14 base62). */
|
|
298
|
+
function partId() {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
if (now !== lastMs) {
|
|
301
|
+
lastMs = now;
|
|
302
|
+
counter = 0;
|
|
303
|
+
}
|
|
304
|
+
counter += 1;
|
|
305
|
+
const time = (BigInt(now) * 4096n + BigInt(counter)) & 0xffffffffffffn;
|
|
306
|
+
let random = "";
|
|
307
|
+
for (const byte of crypto.getRandomValues(new Uint8Array(14))) random += ID_CHARS[byte % 62];
|
|
308
|
+
return "prt_" + time.toString(16).padStart(12, "0") + random;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
`;
|
|
312
|
+
function renderOpencodePlugin(variant) {
|
|
313
|
+
return variant === "lean" ? `${OPENCODE_PLUGIN_MARKER} v1 lean
|
|
314
|
+
// Generated by \`codebuddy clients install opencode --mode lean\`; removed by
|
|
315
|
+
// \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
|
|
316
|
+
import { spawn } from "node:child_process";
|
|
317
|
+
|
|
318
|
+
const TIMEOUT_MS = { lean: 2000, reset: 10000 };
|
|
319
|
+
${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
|
|
320
|
+
export const CodeBuddyPlugin = async ({ directory }) => {
|
|
321
|
+
const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
"chat.message": async (input, output) => {
|
|
325
|
+
const prompt = output.parts
|
|
326
|
+
.flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
|
|
327
|
+
.join("\\n")
|
|
328
|
+
.trim();
|
|
329
|
+
if (!prompt) return;
|
|
330
|
+
const block = (await hook("lean", { ...base(input.sessionID, "chat.message"), prompt })).trim();
|
|
331
|
+
if (!block) return;
|
|
332
|
+
output.parts.push({
|
|
333
|
+
id: partId(),
|
|
334
|
+
sessionID: input.sessionID,
|
|
335
|
+
messageID: output.message.id,
|
|
336
|
+
type: "text",
|
|
337
|
+
text: block,
|
|
338
|
+
synthetic: true,
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
event: async ({ event }) => {
|
|
342
|
+
if (event.type === "session.compacted") {
|
|
343
|
+
await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
` : `${OPENCODE_PLUGIN_MARKER} v1
|
|
349
|
+
// Generated by \`codebuddy clients install opencode --mode hooks\`; removed by
|
|
350
|
+
// \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
|
|
351
|
+
import { spawn } from "node:child_process";
|
|
352
|
+
|
|
353
|
+
const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]);
|
|
354
|
+
const TIMEOUT_MS = { context: 20000, stage: 10000, capture: 60000, reset: 10000 };
|
|
355
|
+
${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
|
|
356
|
+
export const CodeBuddyPlugin = async ({ directory }) => {
|
|
357
|
+
/** @type {Map<string, { text: string, commands: Array<{ command: string, exit_code: number | null }> }>} */
|
|
358
|
+
const turns = new Map();
|
|
359
|
+
const turn = (sessionID) => {
|
|
360
|
+
if (!turns.has(sessionID)) turns.set(sessionID, { text: "", commands: [] });
|
|
361
|
+
return turns.get(sessionID);
|
|
362
|
+
};
|
|
363
|
+
const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
"chat.message": async (input, output) => {
|
|
367
|
+
turns.delete(input.sessionID);
|
|
368
|
+
const prompt = output.parts
|
|
369
|
+
.flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
|
|
370
|
+
.join("\\n")
|
|
371
|
+
.trim();
|
|
372
|
+
const block = (
|
|
373
|
+
await hook("context", { ...base(input.sessionID, "chat.message"), ...(prompt ? { prompt } : {}) })
|
|
374
|
+
).trim();
|
|
375
|
+
if (!block) return;
|
|
376
|
+
output.parts.push({
|
|
377
|
+
id: partId(),
|
|
378
|
+
sessionID: input.sessionID,
|
|
379
|
+
messageID: output.message.id,
|
|
380
|
+
type: "text",
|
|
381
|
+
text: block,
|
|
382
|
+
synthetic: true,
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
"tool.execute.after": async (input, output) => {
|
|
386
|
+
if (EDIT_TOOLS.has(input.tool)) {
|
|
387
|
+
await hook("stage", {
|
|
388
|
+
...base(input.sessionID, "tool.execute.after"),
|
|
389
|
+
tool_name: input.tool,
|
|
390
|
+
tool_input: input.args,
|
|
391
|
+
});
|
|
392
|
+
} else if (input.tool === "bash" && typeof input.args?.command === "string") {
|
|
393
|
+
const exit = output.metadata?.exit;
|
|
394
|
+
turn(input.sessionID).commands.push({
|
|
395
|
+
command: input.args.command,
|
|
396
|
+
exit_code: typeof exit === "number" ? exit : null,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
"experimental.text.complete": async (input, output) => {
|
|
401
|
+
if (output.text.trim()) turn(input.sessionID).text = output.text;
|
|
402
|
+
},
|
|
403
|
+
event: async ({ event }) => {
|
|
404
|
+
if (event.type === "session.idle") {
|
|
405
|
+
const sessionID = event.properties.sessionID;
|
|
406
|
+
const current = turns.get(sessionID);
|
|
407
|
+
turns.delete(sessionID);
|
|
408
|
+
await hook("capture", {
|
|
409
|
+
...base(sessionID, "session.idle"),
|
|
410
|
+
...(current?.text ? { last_assistant_message: current.text } : {}),
|
|
411
|
+
...(current?.commands.length ? { shell_commands: current.commands.slice(-20) } : {}),
|
|
412
|
+
});
|
|
413
|
+
} else if (event.type === "session.compacted") {
|
|
414
|
+
await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
var OPENCODE_PLUGIN_SOURCE = renderOpencodePlugin("full");
|
|
422
|
+
function opencodePluginPath(repositoryRoot) {
|
|
423
|
+
return join2(resolve2(repositoryRoot), ".opencode", "plugins", "codebuddy.js");
|
|
424
|
+
}
|
|
425
|
+
function plugin2(repositoryRoot, variant = "full") {
|
|
426
|
+
const path = opencodePluginPath(repositoryRoot);
|
|
427
|
+
return {
|
|
428
|
+
marker: OPENCODE_PLUGIN_MARKER,
|
|
429
|
+
files: [{ path, content: renderOpencodePlugin(variant) }],
|
|
430
|
+
dirs: [dirname3(path), dirname3(dirname3(path))]
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function opencodePluginInstalled(repositoryRoot) {
|
|
434
|
+
return managedPluginInstalled(plugin2(repositoryRoot));
|
|
435
|
+
}
|
|
436
|
+
function setOpencodePlugin(repositoryRoot, enabled, variant = "full") {
|
|
437
|
+
return setManagedPlugin(plugin2(repositoryRoot, variant), enabled);
|
|
438
|
+
}
|
|
439
|
+
async function opencodePluginVariant(repositoryRoot) {
|
|
440
|
+
if (!await opencodePluginInstalled(repositoryRoot)) return null;
|
|
441
|
+
const source = await readFile3(opencodePluginPath(repositoryRoot), "utf8");
|
|
442
|
+
return source.startsWith(`${OPENCODE_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
|
|
443
|
+
}
|
|
444
|
+
|
|
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
|
+
// src/hooks/install.ts
|
|
565
|
+
var SURFACES = {
|
|
566
|
+
"claude-settings": {
|
|
567
|
+
path: claudeSettingsPath,
|
|
568
|
+
installed: async (root) => hooksInstalled(await readSettings(claudeSettingsPath(root))),
|
|
569
|
+
variant: async (root) => hooksVariant(await readSettings(claudeSettingsPath(root))),
|
|
570
|
+
set: async (root, enabled, variant) => {
|
|
571
|
+
const path = claudeSettingsPath(root);
|
|
572
|
+
const current = await readSettings(path);
|
|
573
|
+
const { settings, changed } = enabled ? upsertHooks(current, variant) : removeHooks(current);
|
|
574
|
+
if (changed) await writeSettings(path, settings);
|
|
575
|
+
return changed;
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
"opencode-plugin": {
|
|
579
|
+
path: opencodePluginPath,
|
|
580
|
+
installed: opencodePluginInstalled,
|
|
581
|
+
variant: opencodePluginVariant,
|
|
582
|
+
set: setOpencodePlugin
|
|
583
|
+
},
|
|
584
|
+
"hermes-plugin": {
|
|
585
|
+
path: hermesPluginDir,
|
|
586
|
+
installed: hermesPluginInstalled,
|
|
587
|
+
variant: hermesPluginVariant,
|
|
588
|
+
set: setHermesPlugin
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
async function setClientHooks(root, client, enabled, variant = "full") {
|
|
592
|
+
const surface = CLIENT_ADAPTERS[client].hooks;
|
|
593
|
+
if (!surface) return null;
|
|
594
|
+
const { path, set } = SURFACES[surface];
|
|
595
|
+
return { target: path(root), changed: await set(root, enabled, variant) };
|
|
596
|
+
}
|
|
597
|
+
async function clientHooksVariant(root, client) {
|
|
598
|
+
const surface = CLIENT_ADAPTERS[client].hooks;
|
|
599
|
+
if (!surface) return void 0;
|
|
600
|
+
return SURFACES[surface].variant(root);
|
|
601
|
+
}
|
|
602
|
+
async function clientHooksInstalled(root, client) {
|
|
603
|
+
const surface = CLIENT_ADAPTERS[client].hooks;
|
|
604
|
+
return surface ? SURFACES[surface].installed(root) : null;
|
|
605
|
+
}
|
|
606
|
+
async function hookedRulesSiblings(root, client) {
|
|
607
|
+
const file = CLIENT_ADAPTERS[client].rulesFile;
|
|
608
|
+
const hooked = [];
|
|
609
|
+
for (const id of CLIENT_IDS) {
|
|
610
|
+
if (id !== client && CLIENT_ADAPTERS[id].rulesFile === file && await clientHooksInstalled(root, id))
|
|
611
|
+
hooked.push(id);
|
|
612
|
+
}
|
|
613
|
+
return hooked;
|
|
614
|
+
}
|
|
615
|
+
async function rulesFileHooked(root, client) {
|
|
616
|
+
return await clientHooksInstalled(root, client) === true || (await hookedRulesSiblings(root, client)).length > 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/integrations/project-mcp.ts
|
|
620
|
+
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";
|
|
622
|
+
async function writeProjectMcpConfig(options) {
|
|
623
|
+
const path = join4(options.repositoryRoot, ".mcp.json");
|
|
624
|
+
const current = await readProjectMcpConfig(path);
|
|
625
|
+
current.mcpServers ??= {};
|
|
626
|
+
current.mcpServers[`codebuddy-${options.namespace}`] = codebuddyMcpServer(
|
|
627
|
+
options.repositoryRoot,
|
|
628
|
+
options.namespace
|
|
629
|
+
);
|
|
630
|
+
if (options.graphify) {
|
|
631
|
+
current.mcpServers.graphify = {
|
|
632
|
+
command: "python",
|
|
633
|
+
args: [
|
|
634
|
+
"-m",
|
|
635
|
+
"graphify.serve",
|
|
636
|
+
resolve4(options.repositoryRoot, options.graphPath ?? "graphify-out/graph.json")
|
|
637
|
+
],
|
|
638
|
+
env: { CODEBUDDY_PROJECT_ROOT: options.repositoryRoot }
|
|
639
|
+
};
|
|
640
|
+
} else if (current.mcpServers.graphify) {
|
|
641
|
+
delete current.mcpServers.graphify;
|
|
642
|
+
}
|
|
643
|
+
await writeAtomic(path, `${JSON.stringify(current, null, 2)}
|
|
644
|
+
`, { mode: 420 });
|
|
645
|
+
return { path, servers: Object.keys(current.mcpServers).sort() };
|
|
646
|
+
}
|
|
647
|
+
var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
|
|
648
|
+
function codebuddyMcpServer(repositoryRoot, namespace) {
|
|
649
|
+
return {
|
|
650
|
+
command: "codebuddy",
|
|
651
|
+
args: ["serve"],
|
|
652
|
+
env: {
|
|
653
|
+
CODEBUDDY_PROJECT_ROOT: repositoryRoot,
|
|
654
|
+
CODEBUDDY_NAMESPACE: namespace,
|
|
655
|
+
CODEBUDDY_TOOL_PROFILE: "core"
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
async function projectMcpPath(root, file, format) {
|
|
660
|
+
const path = join4(root, file);
|
|
661
|
+
if (format !== "opencode" || await exists(path)) return path;
|
|
662
|
+
return await exists(`${path}c`) ? `${path}c` : path;
|
|
663
|
+
}
|
|
664
|
+
async function exists(path) {
|
|
665
|
+
return access(path).then(
|
|
666
|
+
() => true,
|
|
667
|
+
() => false
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
function formatEntry(format, server) {
|
|
671
|
+
if (format === "mcpServers") return server;
|
|
672
|
+
return {
|
|
673
|
+
type: "local",
|
|
674
|
+
command: [server.command, ...server.args],
|
|
675
|
+
...server.env ? { environment: server.env } : {},
|
|
676
|
+
enabled: true
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function serversKey(format) {
|
|
680
|
+
return format === "opencode" ? "mcp" : "mcpServers";
|
|
681
|
+
}
|
|
682
|
+
async function upsertMcpServer(path, format, name, server) {
|
|
683
|
+
const existed = await exists(path);
|
|
684
|
+
const { config, comments } = await readConfig(path);
|
|
685
|
+
const key = serversKey(format);
|
|
686
|
+
const servers2 = { ...config[key] ?? {} };
|
|
687
|
+
const entry = formatEntry(format, server);
|
|
688
|
+
if (JSON.stringify(servers2[name]) === JSON.stringify(entry)) return "unchanged";
|
|
689
|
+
if (comments) return "manual";
|
|
690
|
+
servers2[name] = entry;
|
|
691
|
+
const next = format === "opencode" && !existed ? { $schema: OPENCODE_SCHEMA, ...config, [key]: servers2 } : { ...config, [key]: servers2 };
|
|
692
|
+
await mkdir3(dirname5(path), { recursive: true });
|
|
693
|
+
await writeAtomic(path, `${JSON.stringify(next, null, 2)}
|
|
694
|
+
`, { mode: 420 });
|
|
695
|
+
return existed ? "updated" : "created";
|
|
696
|
+
}
|
|
697
|
+
async function removeMcpServer(path, format, name) {
|
|
698
|
+
const { config, comments } = await readConfig(path);
|
|
699
|
+
const key = serversKey(format);
|
|
700
|
+
const servers2 = { ...config[key] ?? {} };
|
|
701
|
+
if (!(name in servers2)) return "unchanged";
|
|
702
|
+
if (comments) return "manual";
|
|
703
|
+
delete servers2[name];
|
|
704
|
+
const next = { ...config, [key]: servers2 };
|
|
705
|
+
const leftover = Object.keys(next).filter((k) => k !== key && k !== "$schema");
|
|
706
|
+
if (Object.keys(servers2).length === 0 && leftover.length === 0) await rm2(path, { force: true });
|
|
707
|
+
else await writeAtomic(path, `${JSON.stringify(next, null, 2)}
|
|
708
|
+
`, { mode: 420 });
|
|
709
|
+
return "removed";
|
|
710
|
+
}
|
|
711
|
+
async function hasMcpServer(path, format, name) {
|
|
712
|
+
const { config } = await readConfig(path);
|
|
713
|
+
return name in (config[serversKey(format)] ?? {});
|
|
714
|
+
}
|
|
715
|
+
function parseJsonc(raw) {
|
|
716
|
+
let comments = false;
|
|
717
|
+
const json = raw.replace(/"(?:[^"\\]|\\.)*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (match) => {
|
|
718
|
+
if (match.startsWith('"')) return match;
|
|
719
|
+
comments = true;
|
|
720
|
+
return " ";
|
|
721
|
+
}).replace(/"(?:[^"\\]|\\.)*"|,(\s*[}\]])/g, (match, tail) => tail ?? match);
|
|
722
|
+
return { value: JSON.parse(json), comments };
|
|
723
|
+
}
|
|
724
|
+
async function readConfig(path) {
|
|
725
|
+
try {
|
|
726
|
+
const { value, comments } = parseJsonc(await readFile5(path, "utf8"));
|
|
727
|
+
const config = value && typeof value === "object" ? value : {};
|
|
728
|
+
return { config, comments };
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (error.code === "ENOENT") return { config: {}, comments: false };
|
|
731
|
+
throw new Error(`Could not read ${path}: ${error.message}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function readProjectMcpConfig(path) {
|
|
735
|
+
return (await readConfig(path)).config;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/templates/install.ts
|
|
739
|
+
import { readFile as readFile6, rm as rm3 } from "fs/promises";
|
|
740
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
741
|
+
|
|
742
|
+
// src/templates/workflow.ts
|
|
743
|
+
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
|
+
""
|
|
791
|
+
];
|
|
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."
|
|
845
|
+
];
|
|
846
|
+
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");
|
|
854
|
+
}
|
|
855
|
+
function rulesReaders(file) {
|
|
856
|
+
return CLIENT_IDS.filter((id) => CLIENT_ADAPTERS[id].rulesFile === file).map((id) => CLIENT_ADAPTERS[id].name).join(", ");
|
|
857
|
+
}
|
|
858
|
+
function renderWorkflowTemplate(client, mode = "agent") {
|
|
859
|
+
const file = CLIENT_ADAPTERS[client].rulesFile;
|
|
860
|
+
return [
|
|
861
|
+
`# CodeBuddy workflow (${rulesReaders(file)})`,
|
|
862
|
+
"",
|
|
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]}).`,
|
|
866
|
+
"",
|
|
867
|
+
renderBody(mode)
|
|
868
|
+
].join("\n");
|
|
869
|
+
}
|
|
870
|
+
function parseWorkflowMode(block) {
|
|
871
|
+
return block.match(/Automation mode: `(hooks|agent)`/)?.[1] === "hooks" ? "hooks" : "agent";
|
|
872
|
+
}
|
|
873
|
+
function defaultRulesFile(client) {
|
|
874
|
+
return CLIENT_ADAPTERS[client].rulesFile;
|
|
875
|
+
}
|
|
876
|
+
function clientDisplayName(client) {
|
|
877
|
+
return CLIENT_ADAPTERS[client].name;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/templates/install.ts
|
|
881
|
+
var START_MARKER = "<!-- codebuddy:workflow:start -->";
|
|
882
|
+
var END_MARKER = "<!-- codebuddy:workflow:end -->";
|
|
883
|
+
function renderManagedBlock(client, mode = "agent") {
|
|
884
|
+
return `${START_MARKER}
|
|
885
|
+
${renderWorkflowTemplate(client, mode)}
|
|
886
|
+
${END_MARKER}
|
|
887
|
+
`;
|
|
888
|
+
}
|
|
889
|
+
async function installWorkflowRules(options) {
|
|
890
|
+
const projectRoot = resolve5(options.projectRoot ?? process.cwd());
|
|
891
|
+
const path = options.targetPath ?? join5(projectRoot, defaultRulesFile(options.client));
|
|
892
|
+
const existing = await readIfPresent(path);
|
|
893
|
+
const mode = options.mode ?? await defaultRulesMode(options.client, projectRoot);
|
|
894
|
+
if (options.onlyIfManaged && (existing === null || !hasManagedBlock(existing))) {
|
|
895
|
+
return { path, client: options.client, action: "unchanged" };
|
|
896
|
+
}
|
|
897
|
+
if (mode === "mcp-only") {
|
|
898
|
+
const content2 = existing === null ? null : removeBlock(existing);
|
|
899
|
+
if (content2 === null || content2 === existing) {
|
|
900
|
+
return { path, client: options.client, action: "unchanged" };
|
|
901
|
+
}
|
|
902
|
+
if (content2.trim() === "") await rm3(path, { force: true });
|
|
903
|
+
else await writeAtomic(path, content2, { mode: 420 });
|
|
904
|
+
return { path, client: options.client, action: "removed" };
|
|
905
|
+
}
|
|
906
|
+
const { content, action } = upsertBlock(existing, renderManagedBlock(options.client, mode));
|
|
907
|
+
if (action === "unchanged") return { path, client: options.client, action };
|
|
908
|
+
await writeAtomic(path, content, { mode: 420 });
|
|
909
|
+
return { path, client: options.client, action };
|
|
910
|
+
}
|
|
911
|
+
function upsertBlock(existing, block) {
|
|
912
|
+
if (existing === null) return { content: block, action: "created" };
|
|
913
|
+
const start = existing.indexOf(START_MARKER);
|
|
914
|
+
const end = existing.indexOf(END_MARKER);
|
|
915
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
916
|
+
const before = existing.slice(0, start);
|
|
917
|
+
const after = existing.slice(end + END_MARKER.length).replace(/^\n/, "");
|
|
918
|
+
const replaced = `${before}${block}${after}`;
|
|
919
|
+
return { content: replaced, action: replaced === existing ? "unchanged" : "updated" };
|
|
920
|
+
}
|
|
921
|
+
const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
922
|
+
return { content: `${existing}${separator}${block}`, action: "added" };
|
|
923
|
+
}
|
|
924
|
+
async function defaultRulesMode(client, projectRoot = process.cwd()) {
|
|
925
|
+
return await rulesFileHooked(projectRoot, client) ? "hooks" : "agent";
|
|
926
|
+
}
|
|
927
|
+
async function readManagedMode(path) {
|
|
928
|
+
const existing = await readIfPresent(path);
|
|
929
|
+
if (existing === null || !hasManagedBlock(existing)) return null;
|
|
930
|
+
return parseWorkflowMode(existing.slice(existing.indexOf(START_MARKER)));
|
|
931
|
+
}
|
|
932
|
+
function hasManagedBlock(content) {
|
|
933
|
+
const start = content.indexOf(START_MARKER);
|
|
934
|
+
return start !== -1 && content.indexOf(END_MARKER) > start;
|
|
935
|
+
}
|
|
936
|
+
function removeBlock(existing) {
|
|
937
|
+
if (!hasManagedBlock(existing)) return existing;
|
|
938
|
+
const start = existing.indexOf(START_MARKER);
|
|
939
|
+
const end = existing.indexOf(END_MARKER) + END_MARKER.length;
|
|
940
|
+
const before = existing.slice(0, start).replace(/\n\n$/, "\n");
|
|
941
|
+
const after = existing.slice(end).replace(/^\n/, "");
|
|
942
|
+
return `${before}${after}`;
|
|
943
|
+
}
|
|
944
|
+
async function readIfPresent(path) {
|
|
945
|
+
try {
|
|
946
|
+
return await readFile6(path, "utf8");
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (error.code === "ENOENT") return null;
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/cli/commands/codex.ts
|
|
954
|
+
import { existsSync as existsSync2 } from "fs";
|
|
955
|
+
import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
|
|
956
|
+
import { homedir } from "os";
|
|
957
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
958
|
+
|
|
959
|
+
// src/cli/commands/claude-desktop.ts
|
|
960
|
+
import { existsSync } from "fs";
|
|
961
|
+
import { mkdir as mkdir4, readFile as readFile7 } from "fs/promises";
|
|
962
|
+
import { dirname as dirname6, join as join6, resolve as resolve6 } from "path";
|
|
963
|
+
import { fileURLToPath } from "url";
|
|
964
|
+
function claudeDesktopConfigPath(home = process.env.HOME ?? "") {
|
|
965
|
+
if (process.platform === "darwin") {
|
|
966
|
+
return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
967
|
+
}
|
|
968
|
+
if (process.platform === "win32") {
|
|
969
|
+
return join6(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
|
|
970
|
+
}
|
|
971
|
+
return join6(home, ".config", "Claude", "claude_desktop_config.json");
|
|
972
|
+
}
|
|
973
|
+
async function readClaudeConfig(path) {
|
|
974
|
+
if (!existsSync(path)) return {};
|
|
975
|
+
try {
|
|
976
|
+
return JSON.parse(await readFile7(path, "utf8"));
|
|
977
|
+
} catch (error) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Failed to parse Claude Desktop config at ${path}: ${error.message}`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async function writeClaudeConfig(path, config) {
|
|
984
|
+
await mkdir4(dirname6(path), { recursive: true });
|
|
985
|
+
await writeAtomic(path, `${JSON.stringify(config, null, 2)}
|
|
986
|
+
`, { mode: 384 });
|
|
987
|
+
}
|
|
988
|
+
function resolveCliEntrypoint() {
|
|
989
|
+
const fromArgv = process.argv[1];
|
|
990
|
+
if (fromArgv && existsSync(fromArgv)) return fromArgv;
|
|
991
|
+
const here = dirname6(fileURLToPath(import.meta.url));
|
|
992
|
+
const candidates = [
|
|
993
|
+
resolve6(here, "index.js"),
|
|
994
|
+
// bundled: here = dist/cli/
|
|
995
|
+
resolve6(here, "..", "cli", "index.js"),
|
|
996
|
+
// source: here = src/cli/commands/
|
|
997
|
+
resolve6(here, "..", "index.js")
|
|
998
|
+
];
|
|
999
|
+
for (const candidate of candidates) {
|
|
1000
|
+
if (existsSync(candidate)) return candidate;
|
|
1001
|
+
}
|
|
1002
|
+
return fromArgv ?? candidates[0] ?? "";
|
|
1003
|
+
}
|
|
1004
|
+
function buildEntry(options) {
|
|
1005
|
+
const env = {
|
|
1006
|
+
CODEBUDDY_NAMESPACE: options.namespace,
|
|
1007
|
+
CODEBUDDY_PROJECT_ROOT: resolve6(options.projectRoot ?? process.cwd())
|
|
1008
|
+
};
|
|
1009
|
+
if (options.hfToken) env.HF_TOKEN = options.hfToken;
|
|
1010
|
+
if (options.groqApiKey) env.GROQ_API_KEY = options.groqApiKey;
|
|
1011
|
+
if (options.databaseUrl) env.DATABASE_URL = options.databaseUrl;
|
|
1012
|
+
return {
|
|
1013
|
+
// Absolute: Claude Desktop is a GUI app and does not inherit the shell's
|
|
1014
|
+
// PATH (nvm, Homebrew), so a bare "node" may not resolve or may be another
|
|
1015
|
+
// version. Re-run `codebuddy claude install` after switching Node.
|
|
1016
|
+
command: process.execPath,
|
|
1017
|
+
args: [resolveCliEntrypoint(), "serve"],
|
|
1018
|
+
env
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
async function installClaudeEntry(options) {
|
|
1022
|
+
const path = claudeDesktopConfigPath();
|
|
1023
|
+
const config = await readClaudeConfig(path);
|
|
1024
|
+
config.mcpServers ??= {};
|
|
1025
|
+
const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
|
|
1026
|
+
const existed = config.mcpServers[serverKey] !== void 0;
|
|
1027
|
+
config.mcpServers[serverKey] = buildEntry(options);
|
|
1028
|
+
await writeClaudeConfig(path, config);
|
|
1029
|
+
return { path, serverKey, created: !existed };
|
|
1030
|
+
}
|
|
1031
|
+
async function removeClaudeEntry(serverKey) {
|
|
1032
|
+
const path = claudeDesktopConfigPath();
|
|
1033
|
+
const config = await readClaudeConfig(path);
|
|
1034
|
+
if (!config.mcpServers || !(serverKey in config.mcpServers)) {
|
|
1035
|
+
return { removed: false, path };
|
|
1036
|
+
}
|
|
1037
|
+
delete config.mcpServers[serverKey];
|
|
1038
|
+
await writeClaudeConfig(path, config);
|
|
1039
|
+
return { removed: true, path };
|
|
1040
|
+
}
|
|
1041
|
+
async function listClaudeEntries() {
|
|
1042
|
+
const path = claudeDesktopConfigPath();
|
|
1043
|
+
const config = await readClaudeConfig(path);
|
|
1044
|
+
const entries = Object.entries(config.mcpServers ?? {}).filter(([key]) => key === "codebuddy" || key.startsWith("codebuddy-")).map(([key, entry]) => ({
|
|
1045
|
+
key,
|
|
1046
|
+
namespace: entry.env?.CODEBUDDY_NAMESPACE ?? null,
|
|
1047
|
+
databaseUrl: entry.env?.DATABASE_URL ?? null
|
|
1048
|
+
}));
|
|
1049
|
+
return { path, entries };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/cli/commands/codex.ts
|
|
1053
|
+
function codexConfigPath(home = homedir()) {
|
|
1054
|
+
return join7(home, ".codex", "config.toml");
|
|
1055
|
+
}
|
|
1056
|
+
async function readCodexConfig(path) {
|
|
1057
|
+
if (!existsSync2(path)) return "";
|
|
1058
|
+
return readFile8(path, "utf8");
|
|
1059
|
+
}
|
|
1060
|
+
async function writeCodexConfig(path, content) {
|
|
1061
|
+
await mkdir5(dirname7(path), { recursive: true });
|
|
1062
|
+
await writeAtomic(path, content, { mode: 384 });
|
|
1063
|
+
}
|
|
1064
|
+
async function installCodexEntry(options) {
|
|
1065
|
+
const path = codexConfigPath();
|
|
1066
|
+
const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
|
|
1067
|
+
const current = await readCodexConfig(path);
|
|
1068
|
+
const existed = hasServerBlock(current, serverKey);
|
|
1069
|
+
const entry = buildEntry(options);
|
|
1070
|
+
if (options.toolProfile)
|
|
1071
|
+
entry.env = { ...entry.env, CODEBUDDY_TOOL_PROFILE: options.toolProfile };
|
|
1072
|
+
const next = upsertServerBlock(current, serverKey, entry);
|
|
1073
|
+
if (next !== current) await writeCodexConfig(path, next);
|
|
1074
|
+
return { path, serverKey, created: !existed, changed: next !== current };
|
|
1075
|
+
}
|
|
1076
|
+
async function removeCodexEntry(serverKey) {
|
|
1077
|
+
const path = codexConfigPath();
|
|
1078
|
+
const current = await readCodexConfig(path);
|
|
1079
|
+
const next = removeServerBlock(current, serverKey);
|
|
1080
|
+
const removed = next !== current;
|
|
1081
|
+
const trimmed = next.trimEnd();
|
|
1082
|
+
if (removed) await writeCodexConfig(path, trimmed ? `${trimmed}
|
|
1083
|
+
` : "");
|
|
1084
|
+
return { removed, path };
|
|
1085
|
+
}
|
|
1086
|
+
async function listCodexEntries() {
|
|
1087
|
+
const path = codexConfigPath();
|
|
1088
|
+
const config = await readCodexConfig(path);
|
|
1089
|
+
const entries = Array.from(config.matchAll(/^\[mcp_servers\."([^"]+)"\]\s*$/gm)).flatMap((match) => match[1] ? [match[1]] : []).filter((key) => key === "codebuddy" || key.startsWith("codebuddy-")).map((key) => {
|
|
1090
|
+
const block = getServerBlock(config, key);
|
|
1091
|
+
return {
|
|
1092
|
+
key,
|
|
1093
|
+
namespace: readEnvValue(block, "CODEBUDDY_NAMESPACE"),
|
|
1094
|
+
databaseUrl: readEnvValue(block, "DATABASE_URL")
|
|
1095
|
+
};
|
|
1096
|
+
});
|
|
1097
|
+
return { path, entries };
|
|
1098
|
+
}
|
|
1099
|
+
function upsertServerBlock(config, serverKey, entry) {
|
|
1100
|
+
const trimmed = removeServerBlock(config, serverKey).trimEnd();
|
|
1101
|
+
const block = formatServerBlock(serverKey, entry);
|
|
1102
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${block}
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
function removeServerBlock(config, serverKey) {
|
|
1106
|
+
const escaped = escapeRegExp(serverKey);
|
|
1107
|
+
const end = String.raw`(?![\s\S])`;
|
|
1108
|
+
const sectionPattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
|
|
1109
|
+
const envPattern = String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
|
|
1110
|
+
return config.replace(new RegExp(sectionPattern, "gm"), "").replace(new RegExp(envPattern, "gm"), "");
|
|
1111
|
+
}
|
|
1112
|
+
function getServerBlock(config, serverKey) {
|
|
1113
|
+
const escaped = escapeRegExp(serverKey);
|
|
1114
|
+
const end = String.raw`(?![\s\S])`;
|
|
1115
|
+
const pattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})|^\[[^\]]+\]\s*$|${end})`;
|
|
1116
|
+
const server = config.match(new RegExp(pattern, "m"))?.[0] ?? "";
|
|
1117
|
+
const env = config.match(
|
|
1118
|
+
new RegExp(
|
|
1119
|
+
String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`,
|
|
1120
|
+
"m"
|
|
1121
|
+
)
|
|
1122
|
+
)?.[0] ?? "";
|
|
1123
|
+
return `${server}
|
|
1124
|
+
${env}`;
|
|
1125
|
+
}
|
|
1126
|
+
function hasServerBlock(config, serverKey) {
|
|
1127
|
+
return new RegExp(String.raw`^\[mcp_servers\."${escapeRegExp(serverKey)}"\]\s*$`, "m").test(
|
|
1128
|
+
config
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
function formatServerBlock(serverKey, entry) {
|
|
1132
|
+
const lines = [
|
|
1133
|
+
`[mcp_servers.${tomlKey(serverKey)}]`,
|
|
1134
|
+
`command = ${tomlString(entry.command)}`,
|
|
1135
|
+
`args = ${tomlArray(entry.args)}`
|
|
1136
|
+
];
|
|
1137
|
+
if (entry.env && Object.keys(entry.env).length > 0) {
|
|
1138
|
+
lines.push("", `[mcp_servers.${tomlKey(serverKey)}.env]`);
|
|
1139
|
+
for (const [key, value] of Object.entries(entry.env).sort(
|
|
1140
|
+
([left], [right]) => left.localeCompare(right)
|
|
1141
|
+
)) {
|
|
1142
|
+
lines.push(`${key} = ${tomlString(value)}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return lines.join("\n");
|
|
1146
|
+
}
|
|
1147
|
+
function readEnvValue(block, key) {
|
|
1148
|
+
return block.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
|
|
1149
|
+
}
|
|
1150
|
+
function tomlKey(key) {
|
|
1151
|
+
return `"${key.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
1152
|
+
}
|
|
1153
|
+
function tomlString(value) {
|
|
1154
|
+
return JSON.stringify(value);
|
|
1155
|
+
}
|
|
1156
|
+
function tomlArray(values) {
|
|
1157
|
+
return `[${values.map(tomlString).join(", ")}]`;
|
|
1158
|
+
}
|
|
1159
|
+
function escapeRegExp(value) {
|
|
1160
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/integrations/hermes-config.ts
|
|
1164
|
+
import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
|
|
1165
|
+
import { homedir as homedir2 } from "os";
|
|
1166
|
+
import { dirname as dirname8, join as join8 } from "path";
|
|
1167
|
+
import { isDeepStrictEqual } from "util";
|
|
1168
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1169
|
+
function hermesConfigPath(home = homedir2()) {
|
|
1170
|
+
return join8(process.env.HERMES_HOME ?? join8(home, ".hermes"), "config.yaml");
|
|
1171
|
+
}
|
|
1172
|
+
async function readRaw(path) {
|
|
1173
|
+
try {
|
|
1174
|
+
return await readFile9(path, "utf8");
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
if (error.code === "ENOENT") return "";
|
|
1177
|
+
throw error;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function parseConfig(raw) {
|
|
1181
|
+
try {
|
|
1182
|
+
const value = parseYaml(raw) ?? {};
|
|
1183
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1184
|
+
} catch {
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function servers(config) {
|
|
1189
|
+
const value = config.mcp_servers;
|
|
1190
|
+
return value && typeof value === "object" ? value : {};
|
|
1191
|
+
}
|
|
1192
|
+
var isItem = (line) => line.trim() !== "" && !line.trim().startsWith("#");
|
|
1193
|
+
var indentOf = (line) => line.length - line.trimStart().length;
|
|
1194
|
+
function editMcpServers(raw, name, entry) {
|
|
1195
|
+
const lines = raw.split("\n");
|
|
1196
|
+
const header = lines.findIndex((line) => /^mcp_servers:\s*(#.*)?$/.test(line));
|
|
1197
|
+
const render = (indent2) => stringifyYaml({ [name]: entry }).trimEnd().split("\n").map((line) => `${" ".repeat(indent2)}${line}`);
|
|
1198
|
+
if (header === -1) {
|
|
1199
|
+
if (entry === null) return raw;
|
|
1200
|
+
const body = raw === "" || raw.endsWith("\n") ? raw : `${raw}
|
|
1201
|
+
`;
|
|
1202
|
+
return `${body}mcp_servers:
|
|
1203
|
+
${render(2).join("\n")}
|
|
1204
|
+
`;
|
|
1205
|
+
}
|
|
1206
|
+
let end = header + 1;
|
|
1207
|
+
while (end < lines.length && !/^\S/.test(lines[end] ?? "")) end++;
|
|
1208
|
+
const block = Array.from({ length: end - header - 1 }, (_, i) => header + 1 + i);
|
|
1209
|
+
const items = block.filter((i) => isItem(lines[i] ?? ""));
|
|
1210
|
+
const indent = items.length > 0 ? indentOf(lines[items[0]] ?? "") : 2;
|
|
1211
|
+
const quoted = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1212
|
+
const keyLine = new RegExp(`^ {${indent}}(?:${quoted}|"${quoted}"|'${quoted}'):\\s*(#.*)?$`);
|
|
1213
|
+
const start = items.find((i) => keyLine.test(lines[i] ?? ""));
|
|
1214
|
+
if (start === void 0) {
|
|
1215
|
+
if (entry === null) return raw;
|
|
1216
|
+
const at = (items.at(-1) ?? header) + 1;
|
|
1217
|
+
lines.splice(at, 0, ...render(indent));
|
|
1218
|
+
return lines.join("\n");
|
|
1219
|
+
}
|
|
1220
|
+
const next = block.find(
|
|
1221
|
+
(i) => i > start && (lines[i] ?? "").trim() !== "" && indentOf(lines[i] ?? "") <= indent
|
|
1222
|
+
);
|
|
1223
|
+
let stop = next ?? end;
|
|
1224
|
+
while (stop > start + 1 && (lines[stop - 1] ?? "").trim() === "") stop--;
|
|
1225
|
+
lines.splice(start, stop - start, ...entry === null ? [] : render(indent));
|
|
1226
|
+
if (entry === null && items.length > 0 && items.every((i) => i >= start && i < stop)) {
|
|
1227
|
+
lines.splice(header, 1);
|
|
1228
|
+
}
|
|
1229
|
+
return lines.join("\n");
|
|
1230
|
+
}
|
|
1231
|
+
async function setHermesMcpServer(path, name, entry) {
|
|
1232
|
+
const raw = await readRaw(path);
|
|
1233
|
+
const before = parseConfig(raw);
|
|
1234
|
+
if (!before) return "manual";
|
|
1235
|
+
const current = servers(before)[name];
|
|
1236
|
+
if (entry === null ? current === void 0 : isDeepStrictEqual(current, entry)) {
|
|
1237
|
+
return "unchanged";
|
|
1238
|
+
}
|
|
1239
|
+
const nextServers = { ...servers(before) };
|
|
1240
|
+
if (entry === null) delete nextServers[name];
|
|
1241
|
+
else nextServers[name] = entry;
|
|
1242
|
+
const { mcp_servers: _, ...rest } = before;
|
|
1243
|
+
const expected = Object.keys(nextServers).length > 0 ? { ...rest, mcp_servers: nextServers } : rest;
|
|
1244
|
+
const next = editMcpServers(raw, name, entry);
|
|
1245
|
+
if (!isDeepStrictEqual(parseConfig(next), expected)) return "manual";
|
|
1246
|
+
await mkdir6(dirname8(path), { recursive: true });
|
|
1247
|
+
await writeAtomic(path, next, { mode: 384 });
|
|
1248
|
+
return entry === null ? "removed" : current === void 0 ? "created" : "updated";
|
|
1249
|
+
}
|
|
1250
|
+
async function readHermesConfig(path) {
|
|
1251
|
+
return parseConfig(await readRaw(path));
|
|
1252
|
+
}
|
|
1253
|
+
async function hasHermesMcpServer(path, name) {
|
|
1254
|
+
const config = await readHermesConfig(path);
|
|
1255
|
+
return config !== null && name in servers(config);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/clients/global-mcp.ts
|
|
1259
|
+
function globalMcpPath(target) {
|
|
1260
|
+
return target === "codex" ? codexConfigPath() : hermesConfigPath();
|
|
1261
|
+
}
|
|
1262
|
+
async function setGlobalMcp(target, options) {
|
|
1263
|
+
const { name, root, namespace, enabled } = options;
|
|
1264
|
+
if (target === "hermes") {
|
|
1265
|
+
return setHermesMcpServer(
|
|
1266
|
+
hermesConfigPath(),
|
|
1267
|
+
name,
|
|
1268
|
+
enabled ? codebuddyMcpServer(root, namespace) : null
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (!enabled) return (await removeCodexEntry(name)).removed ? "removed" : "unchanged";
|
|
1272
|
+
const result = await installCodexEntry({
|
|
1273
|
+
namespace,
|
|
1274
|
+
projectRoot: root,
|
|
1275
|
+
serverName: name,
|
|
1276
|
+
toolProfile: "core"
|
|
1277
|
+
});
|
|
1278
|
+
return result.created ? "created" : result.changed ? "updated" : "unchanged";
|
|
1279
|
+
}
|
|
1280
|
+
async function hasGlobalMcp(target, name) {
|
|
1281
|
+
if (target === "hermes") return hasHermesMcpServer(hermesConfigPath(), name);
|
|
1282
|
+
return (await listCodexEntries()).entries.some((entry) => entry.key === name);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/clients/install.ts
|
|
1286
|
+
async function installClient(options) {
|
|
1287
|
+
const adapter = CLIENT_ADAPTERS[options.client];
|
|
1288
|
+
if (!supportedModes(adapter).includes(options.mode)) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`${adapter.name} does not support ${MODE_LABELS[options.mode]} mode: ${adapter.gap ?? "no lifecycle hooks"}.`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
return apply(options, options.mode, options.mode !== "lean");
|
|
1294
|
+
}
|
|
1295
|
+
async function ensureIndex(root) {
|
|
1296
|
+
const target = join9(root, ".codebuddy", "cache", "index.json");
|
|
1297
|
+
const existed = await access2(target).then(
|
|
1298
|
+
() => true,
|
|
1299
|
+
() => false
|
|
1300
|
+
);
|
|
1301
|
+
if (!existed) await indexRepository({ repositoryRoot: root });
|
|
1302
|
+
return { surface: "index", target, action: existed ? "unchanged" : "built" };
|
|
1303
|
+
}
|
|
1304
|
+
async function uninstallClient(options) {
|
|
1305
|
+
return apply(options, "mcp-only", false);
|
|
1306
|
+
}
|
|
1307
|
+
async function apply(options, mode, keepMcp) {
|
|
1308
|
+
const root = resolve7(options.projectRoot ?? process.cwd());
|
|
1309
|
+
const adapter = CLIENT_ADAPTERS[options.client];
|
|
1310
|
+
const steps = [];
|
|
1311
|
+
const notes = [];
|
|
1312
|
+
const namespace = options.namespace ?? await resolveNamespaceFrom(root);
|
|
1313
|
+
const name = `codebuddy-${namespace}`;
|
|
1314
|
+
const shared = sharedRulesReaders(options.client);
|
|
1315
|
+
const liveSiblings = mode === "mcp-only" || mode === "lean" ? await installedWithMcp(root, shared, namespace) : [];
|
|
1316
|
+
if (liveSiblings.length > 0) {
|
|
1317
|
+
const path = join9(root, adapter.rulesFile);
|
|
1318
|
+
steps.push({ surface: "rules", target: path, action: "kept" });
|
|
1319
|
+
notes.push(
|
|
1320
|
+
`Kept the CodeBuddy block in ${adapter.rulesFile}: still used by ${liveSiblings.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
|
|
1321
|
+
);
|
|
1322
|
+
} else {
|
|
1323
|
+
const hooked = mode === "agent" ? await hookedRulesSiblings(root, options.client) : [];
|
|
1324
|
+
const rules = await installWorkflowRules({
|
|
1325
|
+
client: options.client,
|
|
1326
|
+
projectRoot: root,
|
|
1327
|
+
// Lean sends no rules: the block would cost tokens every turn and shadow AGENTS.md.
|
|
1328
|
+
mode: mode === "lean" ? "mcp-only" : hooked.length > 0 ? "hooks" : mode
|
|
1329
|
+
});
|
|
1330
|
+
steps.push({ surface: "rules", target: rules.path, action: rules.action });
|
|
1331
|
+
if (hooked.length > 0) {
|
|
1332
|
+
notes.push(
|
|
1333
|
+
`Kept ${adapter.rulesFile} hook-driven: ${hooked.map((id) => CLIENT_ADAPTERS[id].name).join(", ")} hooks are installed; agents without hooks follow its agent-driven fallback.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (shared.length > 0) {
|
|
1338
|
+
notes.push(
|
|
1339
|
+
`${adapter.rulesFile} is also read by ${shared.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
const hooks = await setHooks(
|
|
1343
|
+
root,
|
|
1344
|
+
mode === "hooks" || mode === "lean",
|
|
1345
|
+
options.client,
|
|
1346
|
+
mode === "lean" ? "lean" : "full"
|
|
1347
|
+
);
|
|
1348
|
+
if (hooks) steps.push(hooks);
|
|
1349
|
+
if (mode === "lean") steps.push(await ensureIndex(root));
|
|
1350
|
+
if ("file" in adapter.mcp) {
|
|
1351
|
+
const path = await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format);
|
|
1352
|
+
const action = keepMcp ? await upsertMcpServer(path, adapter.mcp.format, name, codebuddyMcpServer(root, namespace)) : await removeMcpServer(path, adapter.mcp.format, name);
|
|
1353
|
+
steps.push({ surface: "mcp", target: path, action });
|
|
1354
|
+
if (action === "manual") {
|
|
1355
|
+
notes.push(
|
|
1356
|
+
`${path} has comments CodeBuddy will not rewrite; ${keepMcp ? "add" : "remove"} the "${name}" entry under "mcp" by hand${keepMcp ? `: ${JSON.stringify(formatEntry(adapter.mcp.format, codebuddyMcpServer(root, namespace)))}` : ""}.`
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
} else if (options.global) {
|
|
1360
|
+
const target = globalMcpPath(adapter.mcp.global);
|
|
1361
|
+
const action = await setGlobalMcp(adapter.mcp.global, {
|
|
1362
|
+
name,
|
|
1363
|
+
root,
|
|
1364
|
+
namespace,
|
|
1365
|
+
enabled: keepMcp
|
|
1366
|
+
});
|
|
1367
|
+
steps.push({ surface: "mcp", target, action });
|
|
1368
|
+
if (action === "manual") {
|
|
1369
|
+
notes.push(
|
|
1370
|
+
`${target} is not a YAML map CodeBuddy can edit safely; ${keepMcp ? "add" : "remove"} the "${name}" entry under mcp_servers by hand.`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
} else {
|
|
1374
|
+
notes.push(
|
|
1375
|
+
`MCP config is global; ${keepMcp ? "add" : "remove"} it with --global, or: ${adapter.mcp.manual}.`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
if (adapter.note && mode !== "mcp-only") notes.push(`${adapter.name}: ${adapter.note}.`);
|
|
1379
|
+
return { client: options.client, steps, notes };
|
|
1380
|
+
}
|
|
1381
|
+
async function installedWithMcp(root, clients, namespace) {
|
|
1382
|
+
const live = [];
|
|
1383
|
+
for (const id of clients) {
|
|
1384
|
+
if ((await detectClient(id, root, namespace)).mcp) live.push(id);
|
|
1385
|
+
}
|
|
1386
|
+
return live;
|
|
1387
|
+
}
|
|
1388
|
+
async function setHooks(root, enabled, client = "claude", variant = "full") {
|
|
1389
|
+
const result = await setClientHooks(root, client, enabled, variant);
|
|
1390
|
+
if (!result) return null;
|
|
1391
|
+
const action = result.changed ? enabled ? "installed" : "removed" : "unchanged";
|
|
1392
|
+
return { surface: "hooks", target: result.target, action };
|
|
1393
|
+
}
|
|
1394
|
+
async function detectClient(client, projectRoot = process.cwd(), namespace) {
|
|
1395
|
+
const root = resolve7(projectRoot);
|
|
1396
|
+
const adapter = CLIENT_ADAPTERS[client];
|
|
1397
|
+
const rulesMode = await readManagedMode(join9(root, adapter.rulesFile));
|
|
1398
|
+
const hooks = await clientHooksInstalled(root, client);
|
|
1399
|
+
const name = `codebuddy-${namespace ?? await resolveNamespaceFrom(root)}`;
|
|
1400
|
+
const mcp = "file" in adapter.mcp ? await hasMcpServer(
|
|
1401
|
+
await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format),
|
|
1402
|
+
adapter.mcp.format,
|
|
1403
|
+
name
|
|
1404
|
+
) : await hasGlobalMcp(adapter.mcp.global, name).catch(() => false);
|
|
1405
|
+
const warnings = [];
|
|
1406
|
+
if (hooks && rulesMode === "agent") {
|
|
1407
|
+
warnings.push(
|
|
1408
|
+
`hooks are installed but ${adapter.rulesFile} is agent-driven: retrieval and capture run twice. Run \`codebuddy clients install ${client} --mode hooks\`.`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (rulesMode === "hooks" && hooks === false) {
|
|
1412
|
+
warnings.push(
|
|
1413
|
+
`${adapter.rulesFile} is hook-driven but hooks are not installed: turns are not captured.`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
const effectiveRules = hooks === null && rulesMode === "hooks" ? "agent" : rulesMode;
|
|
1417
|
+
const mode = hooks ? await clientHooksVariant(root, client) === "lean" ? "lean" : "hooks" : effectiveRules ?? (mcp ? "mcp-only" : "none");
|
|
1418
|
+
return { client, mode, rulesMode, hooks, mcp, warnings };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
export {
|
|
1422
|
+
hermesPluginInstalled,
|
|
1423
|
+
clientHooksInstalled,
|
|
1424
|
+
writeProjectMcpConfig,
|
|
1425
|
+
WORKFLOW_CLIENTS,
|
|
1426
|
+
renderWorkflowTemplate,
|
|
1427
|
+
clientDisplayName,
|
|
1428
|
+
renderManagedBlock,
|
|
1429
|
+
installWorkflowRules,
|
|
1430
|
+
defaultRulesMode,
|
|
1431
|
+
installClaudeEntry,
|
|
1432
|
+
removeClaudeEntry,
|
|
1433
|
+
listClaudeEntries,
|
|
1434
|
+
installCodexEntry,
|
|
1435
|
+
removeCodexEntry,
|
|
1436
|
+
listCodexEntries,
|
|
1437
|
+
hermesConfigPath,
|
|
1438
|
+
readHermesConfig,
|
|
1439
|
+
installClient,
|
|
1440
|
+
uninstallClient,
|
|
1441
|
+
setHooks,
|
|
1442
|
+
detectClient
|
|
1443
|
+
};
|
|
1444
|
+
//# sourceMappingURL=chunk-J4T6HHI4.js.map
|