create-merlin-brain 3.22.0 → 4.0.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 +38 -4
- package/bin/merlin-ask.cjs +111 -0
- package/bin/merlin-cli.cjs +22 -0
- package/bin/runtime-adapters.cjs +709 -28
- package/dist/server/api/client.d.ts +2 -0
- package/dist/server/api/client.d.ts.map +1 -1
- package/dist/server/api/client.js +4 -0
- package/dist/server/api/client.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +56 -4
- package/dist/server/server.js.map +1 -1
- package/dist/server/tools/auto-mode.d.ts +9 -0
- package/dist/server/tools/auto-mode.d.ts.map +1 -0
- package/dist/server/tools/auto-mode.js +231 -0
- package/dist/server/tools/auto-mode.js.map +1 -0
- package/dist/server/tools/computer-use.d.ts +8 -0
- package/dist/server/tools/computer-use.d.ts.map +1 -0
- package/dist/server/tools/computer-use.js +355 -0
- package/dist/server/tools/computer-use.js.map +1 -0
- package/dist/server/tools/dream.d.ts +9 -0
- package/dist/server/tools/dream.d.ts.map +1 -0
- package/dist/server/tools/dream.js +246 -0
- package/dist/server/tools/dream.js.map +1 -0
- package/dist/server/tools/help.d.ts +3 -0
- package/dist/server/tools/help.d.ts.map +1 -0
- package/dist/server/tools/help.js +110 -0
- package/dist/server/tools/help.js.map +1 -0
- package/dist/server/tools/hud.d.ts +13 -0
- package/dist/server/tools/hud.d.ts.map +1 -0
- package/dist/server/tools/hud.js +295 -0
- package/dist/server/tools/hud.js.map +1 -0
- package/dist/server/tools/index.d.ts +5 -0
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +5 -0
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/provider-ask.d.ts +10 -0
- package/dist/server/tools/provider-ask.d.ts.map +1 -0
- package/dist/server/tools/provider-ask.js +234 -0
- package/dist/server/tools/provider-ask.js.map +1 -0
- package/dist/server/tools/rate-limit.d.ts +8 -0
- package/dist/server/tools/rate-limit.d.ts.map +1 -0
- package/dist/server/tools/rate-limit.js +184 -0
- package/dist/server/tools/rate-limit.js.map +1 -0
- package/dist/server/tools/skills.d.ts +16 -0
- package/dist/server/tools/skills.d.ts.map +1 -0
- package/dist/server/tools/skills.js +326 -0
- package/dist/server/tools/skills.js.map +1 -0
- package/dist/server/tools/team-workers.d.ts +7 -0
- package/dist/server/tools/team-workers.d.ts.map +1 -0
- package/dist/server/tools/team-workers.js +271 -0
- package/dist/server/tools/team-workers.js.map +1 -0
- package/dist/server/utils/merlin-manifest.d.ts +6 -1
- package/dist/server/utils/merlin-manifest.d.ts.map +1 -1
- package/dist/server/utils/merlin-manifest.js +34 -1
- package/dist/server/utils/merlin-manifest.js.map +1 -1
- package/files/CLAUDE.md +22 -0
- package/files/hooks/rate-limit-watch.sh +120 -0
- package/files/hooks/statusline.sh +148 -0
- package/files/merlin/skills/SKILLS-INDEX.md +82 -0
- package/files/merlin/skills/automation/payments.md +14 -0
- package/files/merlin/skills/automation/webhooks.md +14 -0
- package/files/merlin/skills/coding/accessibility.md +14 -0
- package/files/merlin/skills/coding/api-design.md +14 -0
- package/files/merlin/skills/coding/debug-mode.md +14 -0
- package/files/merlin/skills/coding/focus-mode.md +14 -0
- package/files/merlin/skills/coding/loop.md +14 -0
- package/files/merlin/skills/coding/performance.md +14 -0
- package/files/merlin/skills/coding/react-patterns.md +51 -0
- package/files/merlin/skills/coding/security-hardening.md +56 -0
- package/files/merlin/skills/coding/verify.md +14 -0
- package/files/merlin/skills/communication/dispatcher.md +40 -0
- package/files/merlin/skills/communication/email-gmail.md +31 -0
- package/files/merlin/skills/communication/telegram.md +50 -0
- package/files/merlin/skills/communication/whatsapp.md +47 -0
- package/files/merlin/skills/data/google-sheets.md +14 -0
- package/files/merlin/skills/design/animation.md +14 -0
- package/files/merlin/skills/devops/docker-containers.md +14 -0
- package/files/merlin/skills/research/brainstorm.md +14 -0
- package/files/merlin/skills/testing/tdd-workflow.md +58 -0
- package/package.json +4 -2
package/bin/runtime-adapters.cjs
CHANGED
|
@@ -8,7 +8,6 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const { execSync } = require('child_process');
|
|
11
|
-
|
|
12
11
|
// ---------------------------------------------------------------------------
|
|
13
12
|
// Runtime definitions
|
|
14
13
|
// ---------------------------------------------------------------------------
|
|
@@ -65,11 +64,178 @@ function buildMcpCommand(useGlobalBinary) {
|
|
|
65
64
|
return { command: 'node', args: [path.join(__dirname, 'serve.js')] };
|
|
66
65
|
}
|
|
67
66
|
|
|
67
|
+
function ensureDir(dir) {
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeFile(filePath, content) {
|
|
74
|
+
ensureDir(path.dirname(filePath));
|
|
75
|
+
fs.writeFileSync(filePath, content);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tomlMultiline(text) {
|
|
79
|
+
return "'''\n" + text.replace(/'''/g, "'''\" + \"'\" + '''") + "\n'''";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function escapeRegex(value) {
|
|
83
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ensureTomlSectionLine(content, sectionName, line) {
|
|
87
|
+
const key = line.split('=')[0].trim();
|
|
88
|
+
const sectionRegex = new RegExp(`(^\\[${escapeRegex(sectionName)}\\]\\n)([\\s\\S]*?)(?=^\\[|$)`, 'm');
|
|
89
|
+
if (sectionRegex.test(content)) {
|
|
90
|
+
return content.replace(sectionRegex, (match, start, body) => {
|
|
91
|
+
const keyRegex = new RegExp(`^${escapeRegex(key)}\\s*=.*$`, 'm');
|
|
92
|
+
let nextBody = body.replace(keyRegex, line);
|
|
93
|
+
if (!keyRegex.test(body)) {
|
|
94
|
+
const trimmedBody = body.replace(/\s+$/, '');
|
|
95
|
+
nextBody = `${trimmedBody ? `${trimmedBody}\n` : ''}${line}\n`;
|
|
96
|
+
}
|
|
97
|
+
const trimmedBody = nextBody.replace(/\s+$/, '');
|
|
98
|
+
return `${start}${trimmedBody ? `${trimmedBody}\n` : ''}`;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const prefix = content.trimEnd();
|
|
103
|
+
return `${prefix}${prefix ? '\n\n' : ''}[${sectionName}]\n${line}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function upsertTomlSection(content, sectionName, sectionBody) {
|
|
107
|
+
const sectionRegex = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n[\\s\\S]*?(?=^\\[|$)`, 'm');
|
|
108
|
+
const next = content.replace(sectionRegex, '').trimEnd();
|
|
109
|
+
return `${next}${next ? '\n\n' : ''}[${sectionName}]\n${sectionBody.trim()}\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function removeTomlSection(content, sectionName) {
|
|
113
|
+
const sectionRegex = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n[\\s\\S]*?(?=^\\[|$)`, 'm');
|
|
114
|
+
return content.replace(sectionRegex, '').trimEnd();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureTomlTopLevelLine(content, line) {
|
|
118
|
+
const key = line.split('=')[0].trim();
|
|
119
|
+
const lines = content.split('\n');
|
|
120
|
+
const keyRegex = new RegExp(`^${escapeRegex(key)}\\s*=.*$`);
|
|
121
|
+
const existingIndex = lines.findIndex((current) => keyRegex.test(current.trim()));
|
|
122
|
+
if (existingIndex !== -1) {
|
|
123
|
+
lines[existingIndex] = line;
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let insertAt = 0;
|
|
128
|
+
while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++;
|
|
129
|
+
if (insertAt < lines.length && !lines[insertAt].startsWith('[')) {
|
|
130
|
+
while (insertAt < lines.length && lines[insertAt].trim() !== '') insertAt++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const next = [...lines];
|
|
134
|
+
next.splice(insertAt, 0, line);
|
|
135
|
+
return next.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function mergeHooksJson(existingContent, hooksRoot) {
|
|
139
|
+
const nextConfig = JSON.parse(buildCodexHooksJson(hooksRoot));
|
|
140
|
+
let base = { hooks: {} };
|
|
141
|
+
|
|
142
|
+
if (existingContent && existingContent.trim()) {
|
|
143
|
+
try {
|
|
144
|
+
base = JSON.parse(existingContent);
|
|
145
|
+
} catch {
|
|
146
|
+
base = { hooks: {} };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
base.hooks = base.hooks || {};
|
|
151
|
+
for (const [eventName, entries] of Object.entries(nextConfig.hooks)) {
|
|
152
|
+
base.hooks[eventName] = base.hooks[eventName] || [];
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const cmd = entry.hooks?.[0]?.command;
|
|
155
|
+
const exists = base.hooks[eventName].some((existing) =>
|
|
156
|
+
existing?.hooks?.some((hook) => hook.command === cmd)
|
|
157
|
+
);
|
|
158
|
+
if (!exists) {
|
|
159
|
+
base.hooks[eventName].push(entry);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return JSON.stringify(base, null, 2) + '\n';
|
|
165
|
+
}
|
|
166
|
+
|
|
68
167
|
// ---------------------------------------------------------------------------
|
|
69
|
-
//
|
|
168
|
+
// Instruction file content for non-Claude runtimes
|
|
70
169
|
// ---------------------------------------------------------------------------
|
|
71
170
|
|
|
72
|
-
function
|
|
171
|
+
function buildInstructionMd(runtime = 'generic') {
|
|
172
|
+
if (runtime === 'codex') {
|
|
173
|
+
return `# Merlin Brain — Codex Operating Layer
|
|
174
|
+
|
|
175
|
+
> Installed by Merlin (https://merlin.build). Keep this file so Codex loads Merlin context automatically.
|
|
176
|
+
|
|
177
|
+
## Session Boot
|
|
178
|
+
|
|
179
|
+
Before doing real work in a repository:
|
|
180
|
+
|
|
181
|
+
1. Call \`merlin_get_selected_repo\`.
|
|
182
|
+
2. Call \`merlin_get_project_status\`.
|
|
183
|
+
3. Call \`merlin_get_rules\` and \`merlin_get_brief\`.
|
|
184
|
+
4. Summarize the current state, then proceed.
|
|
185
|
+
|
|
186
|
+
Do not skip boot. Do not start editing code without Merlin context.
|
|
187
|
+
|
|
188
|
+
## Discoverability
|
|
189
|
+
|
|
190
|
+
- If the user asks what Merlin can do, call \`merlin_help\`.
|
|
191
|
+
- If the best Merlin path is unclear, call \`merlin_help(task)\`.
|
|
192
|
+
- For new features or integrations, call \`merlin_recommend_for_task(task)\` before building from scratch.
|
|
193
|
+
- For agent selection, call \`merlin_smart_route(task)\`.
|
|
194
|
+
- For reusable prompt packs, call \`merlin_find_skill(query)\`.
|
|
195
|
+
|
|
196
|
+
## Codex + Merlin Workflow
|
|
197
|
+
|
|
198
|
+
- For codebase questions, start with \`merlin_search("query")\` or \`merlin_get_context("task")\`.
|
|
199
|
+
- Before every code edit, call \`merlin_get_context("your task")\`.
|
|
200
|
+
- Let Merlin choose the route first when helpful: tool vs skill vs custom agent vs direct execution.
|
|
201
|
+
- When local file search is still needed, prefer fast repo-native tools such as \`rg\`.
|
|
202
|
+
- Use Codex's normal execution style: inspect first, edit surgically, verify after changes.
|
|
203
|
+
- Keep progress updates concise and factual while work is in flight.
|
|
204
|
+
- Prefer Merlin skills and custom agents installed in this repo when the task matches them.
|
|
205
|
+
|
|
206
|
+
## Editing Discipline
|
|
207
|
+
|
|
208
|
+
- Prefer \`apply_patch\` for manual edits.
|
|
209
|
+
- Avoid broad rewrites when a narrow patch is sufficient.
|
|
210
|
+
- Preserve user changes you did not make.
|
|
211
|
+
- Verify behavior after implementation work before claiming completion.
|
|
212
|
+
|
|
213
|
+
## Delegation
|
|
214
|
+
|
|
215
|
+
- Use Merlin tools first for routing and context.
|
|
216
|
+
- Use Codex sub-agents only for clearly bounded side tasks that can run in parallel.
|
|
217
|
+
- Keep the critical path local when the next step depends on immediate code understanding.
|
|
218
|
+
|
|
219
|
+
## MCP Tools Available
|
|
220
|
+
|
|
221
|
+
- \`merlin_get_selected_repo\` — identify the active repository
|
|
222
|
+
- \`merlin_get_project_status\` — load project state and active tasks
|
|
223
|
+
- \`merlin_help(task?)\` — explain Merlin capabilities and recommend the best route
|
|
224
|
+
- \`merlin_get_context(task)\` — fetch targeted implementation context
|
|
225
|
+
- \`merlin_find_files(query)\` — locate files by purpose
|
|
226
|
+
- \`merlin_search(query)\` — semantic code search
|
|
227
|
+
- \`merlin_find_skill(query)\` — find reusable Merlin skills
|
|
228
|
+
- \`merlin_smart_route(task)\` — choose the best specialist agent
|
|
229
|
+
- \`merlin_recommend_for_task(task)\` — find agents and reference codebases before building
|
|
230
|
+
|
|
231
|
+
## Defaults
|
|
232
|
+
|
|
233
|
+
- Search before writing.
|
|
234
|
+
- Reuse existing patterns before introducing new ones.
|
|
235
|
+
- If Merlin Sights is unavailable, continue with disciplined local exploration instead of blocking.
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
|
|
73
239
|
return `# Merlin Brain — AI Development System
|
|
74
240
|
|
|
75
241
|
> Installed by Merlin (https://merlin.build). Keep this file to preserve Merlin context.
|
|
@@ -115,6 +281,443 @@ The Merlin MCP server exposes these tools:
|
|
|
115
281
|
`;
|
|
116
282
|
}
|
|
117
283
|
|
|
284
|
+
function buildCodexHooksJson(hooksRoot) {
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
hooks: {
|
|
287
|
+
SessionStart: [
|
|
288
|
+
{
|
|
289
|
+
matcher: 'startup|resume',
|
|
290
|
+
hooks: [
|
|
291
|
+
{
|
|
292
|
+
type: 'command',
|
|
293
|
+
command: `bash "${path.join(hooksRoot, 'session-start.sh')}"`,
|
|
294
|
+
statusMessage: 'Merlin booting',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
UserPromptSubmit: [
|
|
300
|
+
{
|
|
301
|
+
hooks: [
|
|
302
|
+
{
|
|
303
|
+
type: 'command',
|
|
304
|
+
command: `bash "${path.join(hooksRoot, 'user-prompt-router.sh')}"`,
|
|
305
|
+
statusMessage: 'Merlin routing',
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
PreToolUse: [
|
|
311
|
+
{
|
|
312
|
+
matcher: 'Bash',
|
|
313
|
+
hooks: [
|
|
314
|
+
{
|
|
315
|
+
type: 'command',
|
|
316
|
+
command: `bash "${path.join(hooksRoot, 'bash-pre-tool.sh')}"`,
|
|
317
|
+
statusMessage: 'Merlin command guard',
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
PostToolUse: [
|
|
323
|
+
{
|
|
324
|
+
matcher: 'Bash',
|
|
325
|
+
hooks: [
|
|
326
|
+
{
|
|
327
|
+
type: 'command',
|
|
328
|
+
command: `bash "${path.join(hooksRoot, 'bash-post-tool.sh')}"`,
|
|
329
|
+
statusMessage: 'Merlin command review',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
Stop: [
|
|
335
|
+
{
|
|
336
|
+
hooks: [
|
|
337
|
+
{
|
|
338
|
+
type: 'command',
|
|
339
|
+
command: `bash "${path.join(hooksRoot, 'stop.sh')}"`,
|
|
340
|
+
statusMessage: 'Merlin wrap-up',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
}, null, 2) + '\n';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildCodexSessionStartHook() {
|
|
350
|
+
return `#!/usr/bin/env bash
|
|
351
|
+
set -euo pipefail
|
|
352
|
+
trap 'echo "{}"; exit 0' ERR
|
|
353
|
+
|
|
354
|
+
MERLIN_HOME="\${HOME}/.codex/merlin"
|
|
355
|
+
mkdir -p "\${MERLIN_HOME}/analytics" "\${MERLIN_HOME}/sessions" 2>/dev/null || true
|
|
356
|
+
|
|
357
|
+
_context="MERLIN MODE ACTIVE. Before handling the user request, call merlin_get_selected_repo, then merlin_get_project_status, then merlin_get_rules and merlin_get_brief."
|
|
358
|
+
_context="\${_context} Use Merlin context before edits: call merlin_get_context(task)."
|
|
359
|
+
_context="\${_context} If the best Merlin path is unclear, call merlin_help(task) before acting."
|
|
360
|
+
_context="\${_context} For task routing, prefer merlin_smart_route(task), merlin_find_skill(query), and merlin_recommend_for_task(task) over improvising."
|
|
361
|
+
_context="\${_context} Prefer Merlin skills and custom agents installed in this repository when they match the task."
|
|
362
|
+
_context="\${_context} Keep the Codex experience pragmatic: inspect first, patch surgically, verify before claiming completion."
|
|
363
|
+
_context="\${_context} Prefix visible progress updates with the Merlin badge when practical: ⟡🔮 MERLIN ›"
|
|
364
|
+
|
|
365
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "\${_context//\"/\\\\\"}"
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildCodexUserPromptRouterHook() {
|
|
370
|
+
return `#!/usr/bin/env bash
|
|
371
|
+
set -euo pipefail
|
|
372
|
+
trap 'echo "{}"; exit 0' ERR
|
|
373
|
+
|
|
374
|
+
input=""
|
|
375
|
+
if [ ! -t 0 ]; then
|
|
376
|
+
input=$(cat 2>/dev/null || true)
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
[ -z "$input" ] && echo "{}" && exit 0
|
|
380
|
+
|
|
381
|
+
prompt=""
|
|
382
|
+
if command -v jq >/dev/null 2>&1; then
|
|
383
|
+
prompt=$(echo "$input" | jq -r '.prompt // .userPrompt // empty' 2>/dev/null || true)
|
|
384
|
+
else
|
|
385
|
+
prompt=$(echo "$input" | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/' 2>/dev/null || true)
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
[ -z "$prompt" ] && echo "{}" && exit 0
|
|
389
|
+
|
|
390
|
+
clean=$(printf '%s' "$prompt" | sed -E -e 's/<[^>]+>//g' -e 's|https?://[^ ]*||g' -e 's|/[a-zA-Z0-9._/-]+||g' -e 's/\`[^\`]*\`//g')
|
|
391
|
+
|
|
392
|
+
suggestion=""
|
|
393
|
+
if echo "$clean" | grep -qiE "what can merlin do|how do i use merlin|what is available|available skills|available agents|available tools|help me use merlin"; then
|
|
394
|
+
suggestion='Merlin routing: call merlin_help first, then pick the recommended tool, skill, or agent path.'
|
|
395
|
+
elif echo "$clean" | grep -qiE "resume|pick up|continue|where were we"; then
|
|
396
|
+
suggestion='Merlin routing: call merlin_get_project_status, then use the merlin-resume skill.'
|
|
397
|
+
elif echo "$clean" | grep -qiE "progress|status|where are we|how far"; then
|
|
398
|
+
suggestion='Merlin routing: call merlin_get_project_status, then use the merlin-progress skill.'
|
|
399
|
+
elif echo "$clean" | grep -qiE "map codebase|understand this repo|learn this codebase|explore the architecture"; then
|
|
400
|
+
suggestion='Merlin routing: call merlin_search and merlin_get_context, then use the merlin-map-codebase skill before implementation.'
|
|
401
|
+
elif echo "$clean" | grep -qiE "bug|crash|error|not working|fix|failing|exception"; then
|
|
402
|
+
suggestion='Merlin routing: call merlin_get_context for the bug, then merlin_smart_route(task), then use merlin-verify after the fix.'
|
|
403
|
+
elif echo "$clean" | grep -qiE "refactor|cleanup|clean up|dry|restructure"; then
|
|
404
|
+
suggestion='Merlin routing: call merlin_get_context first, keep scope narrow, and use merlin-workflow plus merlin-verify.'
|
|
405
|
+
elif echo "$clean" | grep -qiE "oauth|auth|stripe|prisma|sdk|integration|api|graphql|railway|deploy"; then
|
|
406
|
+
suggestion='Merlin routing: call merlin_recommend_for_task(task), merlin_find_skill(query), and merlin_smart_route(task) before building.'
|
|
407
|
+
elif echo "$clean" | grep -qiE "build|add|create|implement|new feature|develop"; then
|
|
408
|
+
suggestion='Merlin routing: call merlin_help(task) or merlin_smart_route(task), gather Merlin context, then execute with implementation-focused agents.'
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
[ -z "$suggestion" ] && echo "{}" && exit 0
|
|
412
|
+
|
|
413
|
+
printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\\n' "\${suggestion//\"/\\\\\"}"
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function buildCodexBashPreToolHook() {
|
|
418
|
+
return `#!/usr/bin/env bash
|
|
419
|
+
set -euo pipefail
|
|
420
|
+
trap 'echo "{}"; exit 0' ERR
|
|
421
|
+
|
|
422
|
+
input=""
|
|
423
|
+
if [ ! -t 0 ]; then
|
|
424
|
+
input=$(cat 2>/dev/null || true)
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
[ -z "$input" ] && echo "{}" && exit 0
|
|
428
|
+
|
|
429
|
+
command_str=""
|
|
430
|
+
if command -v jq >/dev/null 2>&1; then
|
|
431
|
+
command_str=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
|
|
432
|
+
else
|
|
433
|
+
command_str=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
[ -z "$command_str" ] && echo "{}" && exit 0
|
|
437
|
+
|
|
438
|
+
block() {
|
|
439
|
+
printf '{"decision":"block","reason":"%s"}\\n' "$1"
|
|
440
|
+
exit 2
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if echo "$command_str" | grep -qE '(curl|wget)\\s[^|]*\\|\\s*(bash|sh|zsh|python|ruby|perl)\\b' 2>/dev/null; then
|
|
444
|
+
block "Merlin blocked pipe-to-shell execution."
|
|
445
|
+
fi
|
|
446
|
+
if echo "$command_str" | grep -qE '(^|\\s|;|&&|\\|\\|)(sudo|su)\\s' 2>/dev/null; then
|
|
447
|
+
block "Merlin blocked privilege escalation."
|
|
448
|
+
fi
|
|
449
|
+
if echo "$command_str" | grep -qE 'git\\s+reset\\s+.*--hard|git\\s+clean\\s+.*-[a-z]*f|git\\s+push\\s+.*--force' 2>/dev/null; then
|
|
450
|
+
block "Merlin blocked a destructive git command."
|
|
451
|
+
fi
|
|
452
|
+
if echo "$command_str" | grep -qE '\\brm\\s+(-[a-z]*r[a-z]*f|-rf|-fr|--recursive)' 2>/dev/null; then
|
|
453
|
+
block "Merlin blocked a destructive rm command."
|
|
454
|
+
fi
|
|
455
|
+
if echo "$command_str" | grep -qE 'AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{40,}|-----BEGIN (RSA|EC|DSA|OPENSSH|PRIVATE) KEY-----' 2>/dev/null; then
|
|
456
|
+
block "Merlin blocked a command containing a secret or private key."
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
echo '{}'
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function buildCodexBashPostToolHook() {
|
|
464
|
+
return `#!/usr/bin/env bash
|
|
465
|
+
set -euo pipefail
|
|
466
|
+
trap 'echo "{}"; exit 0' ERR
|
|
467
|
+
|
|
468
|
+
input=""
|
|
469
|
+
if [ ! -t 0 ]; then
|
|
470
|
+
input=$(cat 2>/dev/null || true)
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
[ -z "$input" ] && echo "{}" && exit 0
|
|
474
|
+
|
|
475
|
+
if command -v jq >/dev/null 2>&1; then
|
|
476
|
+
cmd=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
|
|
477
|
+
else
|
|
478
|
+
cmd=""
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
if echo "$cmd" | grep -qiE 'npm test|pnpm test|yarn test|vitest|pytest|cargo test|go test'; then
|
|
482
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"Merlin note: summarize whether verification passed before moving on."}}\\n'
|
|
483
|
+
exit 0
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
echo '{}'
|
|
487
|
+
`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function buildCodexStopHook() {
|
|
491
|
+
return `#!/usr/bin/env bash
|
|
492
|
+
set -euo pipefail
|
|
493
|
+
trap 'echo "{}"; exit 0' ERR
|
|
494
|
+
|
|
495
|
+
MERLIN_HOME="\${HOME}/.codex/merlin"
|
|
496
|
+
mkdir -p "\${MERLIN_HOME}/sessions" 2>/dev/null || true
|
|
497
|
+
|
|
498
|
+
if command -v jq >/dev/null 2>&1; then
|
|
499
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
500
|
+
jq -n --arg ts "$ts" '{ timestamp: $ts }' > "\${MERLIN_HOME}/sessions/session-$(date +%s).json" 2>/dev/null || true
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
echo '{}'
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function installCodexHookScripts(codexDir) {
|
|
508
|
+
const hooksRoot = path.join(codexDir, 'merlin', 'hooks');
|
|
509
|
+
const scripts = {
|
|
510
|
+
'session-start.sh': buildCodexSessionStartHook(),
|
|
511
|
+
'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
|
|
512
|
+
'bash-pre-tool.sh': buildCodexBashPreToolHook(),
|
|
513
|
+
'bash-post-tool.sh': buildCodexBashPostToolHook(),
|
|
514
|
+
'stop.sh': buildCodexStopHook(),
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
ensureDir(hooksRoot);
|
|
518
|
+
for (const [name, content] of Object.entries(scripts)) {
|
|
519
|
+
const target = path.join(hooksRoot, name);
|
|
520
|
+
writeFile(target, content);
|
|
521
|
+
fs.chmodSync(target, '755');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return hooksRoot;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function buildCodexAgentSpecs() {
|
|
528
|
+
return [
|
|
529
|
+
{
|
|
530
|
+
filename: 'implementation-dev.toml',
|
|
531
|
+
name: 'implementation_dev',
|
|
532
|
+
description: 'Implementation specialist for bounded code changes with verification.',
|
|
533
|
+
model: 'gpt-5.4',
|
|
534
|
+
effort: 'medium',
|
|
535
|
+
sandbox: 'workspace-write',
|
|
536
|
+
nicknames: ['Forge', 'Patch', 'Build'],
|
|
537
|
+
instructions: 'Implement the requested change with minimal surface area. Inspect existing patterns first, preserve unrelated user edits, and verify behavior before handing back results.',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
filename: 'tests-qa.toml',
|
|
541
|
+
name: 'tests_qa',
|
|
542
|
+
description: 'Testing specialist focused on regressions, missing cases, and verification.',
|
|
543
|
+
model: 'gpt-5.4-mini',
|
|
544
|
+
effort: 'medium',
|
|
545
|
+
sandbox: 'workspace-write',
|
|
546
|
+
nicknames: ['Spec', 'Check', 'Proof'],
|
|
547
|
+
instructions: 'Focus on tests, reproducibility, and verification evidence. Add or improve tests when justified, and report concrete coverage gaps when testing is blocked.',
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
filename: 'system-architect.toml',
|
|
551
|
+
name: 'system_architect',
|
|
552
|
+
description: 'Architecture specialist for design tradeoffs and decomposition.',
|
|
553
|
+
model: 'gpt-5.4',
|
|
554
|
+
effort: 'high',
|
|
555
|
+
sandbox: 'read-only',
|
|
556
|
+
nicknames: ['Northstar', 'Grid', 'Frame'],
|
|
557
|
+
instructions: 'Stay at the design layer unless explicitly asked to patch code. Map dependencies, tradeoffs, and sequencing clearly. Prefer the simplest architecture that satisfies the real constraints.',
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
filename: 'docs-keeper.toml',
|
|
561
|
+
name: 'docs_keeper',
|
|
562
|
+
description: 'Documentation specialist for READMEs, usage guides, and project docs.',
|
|
563
|
+
model: 'gpt-5.4-mini',
|
|
564
|
+
effort: 'medium',
|
|
565
|
+
sandbox: 'workspace-write',
|
|
566
|
+
nicknames: ['Ledger', 'Script', 'Index'],
|
|
567
|
+
instructions: 'Update documentation to match the actual implementation. Prefer concise, accurate edits tied to real behavior and verified paths.',
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
filename: 'hardening-guard.toml',
|
|
571
|
+
name: 'hardening_guard',
|
|
572
|
+
description: 'Security and resilience reviewer for edge cases and failure handling.',
|
|
573
|
+
model: 'gpt-5.4',
|
|
574
|
+
effort: 'high',
|
|
575
|
+
sandbox: 'read-only',
|
|
576
|
+
nicknames: ['Shield', 'Gate', 'Sentinel'],
|
|
577
|
+
instructions: 'Review for security issues, data loss, unsafe command usage, auth gaps, and weak failure handling. Lead with concrete risks and mitigations.',
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
filename: 'merlin-codebase-mapper.toml',
|
|
581
|
+
name: 'merlin_codebase_mapper',
|
|
582
|
+
description: 'Read-only codebase mapper for architecture, flows, and key files.',
|
|
583
|
+
model: 'gpt-5.4-mini',
|
|
584
|
+
effort: 'medium',
|
|
585
|
+
sandbox: 'read-only',
|
|
586
|
+
nicknames: ['Scout', 'Atlas', 'Survey'],
|
|
587
|
+
instructions: 'Map the relevant code paths, file ownership, and architectural seams before implementation. Prefer precise citations over broad summaries.',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
filename: 'merlin-researcher.toml',
|
|
591
|
+
name: 'merlin_researcher',
|
|
592
|
+
description: 'Research specialist for external docs and fast-moving technical questions.',
|
|
593
|
+
model: 'gpt-5.4-mini',
|
|
594
|
+
effort: 'medium',
|
|
595
|
+
sandbox: 'read-only',
|
|
596
|
+
nicknames: ['Query', 'Lens', 'Signal'],
|
|
597
|
+
instructions: 'Use primary sources when research is needed. Distinguish verified facts from inference and return concise findings with citations.',
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
filename: 'merlin-verifier.toml',
|
|
601
|
+
name: 'merlin_verifier',
|
|
602
|
+
description: 'Verification specialist for validation, behavior checks, and release confidence.',
|
|
603
|
+
model: 'gpt-5.4-mini',
|
|
604
|
+
effort: 'medium',
|
|
605
|
+
sandbox: 'workspace-write',
|
|
606
|
+
nicknames: ['Audit', 'Pulse', 'Trace'],
|
|
607
|
+
instructions: 'Verify the requested outcome rather than merely restating changes. Prefer direct evidence from tests, builds, or code paths, and call out residual risks clearly.',
|
|
608
|
+
},
|
|
609
|
+
];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function installCodexAgents(projectDir) {
|
|
613
|
+
const agentsDir = path.join(projectDir, '.codex', 'agents');
|
|
614
|
+
ensureDir(agentsDir);
|
|
615
|
+
const created = [];
|
|
616
|
+
|
|
617
|
+
for (const spec of buildCodexAgentSpecs()) {
|
|
618
|
+
const content = [
|
|
619
|
+
`name = "${spec.name}"`,
|
|
620
|
+
`description = "${spec.description}"`,
|
|
621
|
+
`model = "${spec.model}"`,
|
|
622
|
+
`model_reasoning_effort = "${spec.effort}"`,
|
|
623
|
+
`sandbox_mode = "${spec.sandbox}"`,
|
|
624
|
+
`nickname_candidates = [${spec.nicknames.map((n) => `"${n}"`).join(', ')}]`,
|
|
625
|
+
`developer_instructions = ${tomlMultiline(spec.instructions)}`,
|
|
626
|
+
'',
|
|
627
|
+
].join('\n');
|
|
628
|
+
writeFile(path.join(agentsDir, spec.filename), content);
|
|
629
|
+
created.push(path.join(agentsDir, spec.filename));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return created;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function buildCodexSkillSpecs() {
|
|
636
|
+
return [
|
|
637
|
+
{
|
|
638
|
+
dir: 'merlin-discover',
|
|
639
|
+
content: `---
|
|
640
|
+
name: merlin-discover
|
|
641
|
+
description: Use when the user asks what Merlin can do, which Merlin features are available, or which Merlin path should be used for a task in Codex.
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
Start with \`merlin_help\`.
|
|
645
|
+
If the user also has a concrete task, call \`merlin_help(task)\`.
|
|
646
|
+
Then choose the recommended route: direct Merlin tools, a Merlin skill, a custom Codex agent, or local execution.
|
|
647
|
+
Do not assume the user already knows internal Merlin names.`,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
dir: 'merlin-map-codebase',
|
|
651
|
+
content: `---
|
|
652
|
+
name: merlin-map-codebase
|
|
653
|
+
description: Use when the user wants to understand a repository, onboard to a codebase, or map architecture before implementing changes.
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
Before making architectural claims, call \`merlin_get_selected_repo\` and \`merlin_get_project_status\`.
|
|
657
|
+
Then use \`merlin_search\` and \`merlin_get_context\` to identify the core files, flows, and conventions relevant to the request.
|
|
658
|
+
When the task is large, prefer the \`merlin_codebase_mapper\` custom agent for bounded exploration.`,
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
dir: 'merlin-progress',
|
|
662
|
+
content: `---
|
|
663
|
+
name: merlin-progress
|
|
664
|
+
description: Use when the user asks for current status, progress, next steps, or where work left off in the repository.
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
Call \`merlin_get_project_status\` first.
|
|
668
|
+
Summarize the current state, active tasks, and recommended next step.
|
|
669
|
+
If local planning files exist, reconcile them with Merlin status instead of trusting either source blindly.`,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
dir: 'merlin-resume',
|
|
673
|
+
content: `---
|
|
674
|
+
name: merlin-resume
|
|
675
|
+
description: Use when the user is returning to a project and wants to continue, resume, or recover context quickly.
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
Boot with Merlin tools first, then reconstruct the current state from project status, local planning files, and recent git changes.
|
|
679
|
+
Prefer concise orientation: what was in progress, what is blocked, and the most reasonable next action.`,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
dir: 'merlin-workflow',
|
|
683
|
+
content: `---
|
|
684
|
+
name: merlin-workflow
|
|
685
|
+
description: Use when the task should be decomposed into architecture, implementation, testing, docs, or verification specialists instead of one monolithic pass.
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
Use Merlin context before decomposition.
|
|
689
|
+
For bounded parallel work, use the custom Codex agents installed in \`.codex/agents\`.
|
|
690
|
+
Keep the critical path local when a side task would block the very next action.`,
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
dir: 'merlin-verify',
|
|
694
|
+
content: `---
|
|
695
|
+
name: merlin-verify
|
|
696
|
+
description: Use when implementation work is complete and you need focused validation, test review, or release confidence.
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
Prefer direct evidence over narration.
|
|
700
|
+
Run or inspect the relevant verification commands, summarize what passed, what failed, and any residual risk.
|
|
701
|
+
Use the \`merlin_verifier\` or \`tests_qa\` agent for bounded validation passes when parallel work helps.`,
|
|
702
|
+
},
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function installCodexSkills(projectDir) {
|
|
707
|
+
const skillsRoot = path.join(projectDir, '.agents', 'skills');
|
|
708
|
+
ensureDir(skillsRoot);
|
|
709
|
+
const created = [];
|
|
710
|
+
|
|
711
|
+
for (const spec of buildCodexSkillSpecs()) {
|
|
712
|
+
const skillDir = path.join(skillsRoot, spec.dir);
|
|
713
|
+
ensureDir(skillDir);
|
|
714
|
+
writeFile(path.join(skillDir, 'SKILL.md'), spec.content.trim() + '\n');
|
|
715
|
+
created.push(path.join(skillDir, 'SKILL.md'));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return created;
|
|
719
|
+
}
|
|
720
|
+
|
|
118
721
|
// ---------------------------------------------------------------------------
|
|
119
722
|
// Codex CLI adapter
|
|
120
723
|
// Writes: ~/.codex/AGENTS.md + ~/.codex/config.toml (MCP section)
|
|
@@ -130,7 +733,7 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
|
|
|
130
733
|
|
|
131
734
|
// Write AGENTS.md
|
|
132
735
|
const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
|
|
133
|
-
const agentsMd =
|
|
736
|
+
const agentsMd = buildInstructionMd('codex');
|
|
134
737
|
const existingAgentsMd = fs.existsSync(agentsMdPath)
|
|
135
738
|
? fs.readFileSync(agentsMdPath, 'utf8')
|
|
136
739
|
: '';
|
|
@@ -145,29 +748,62 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
|
|
|
145
748
|
results.push('Wrote ~/.codex/AGENTS.md');
|
|
146
749
|
}
|
|
147
750
|
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
?
|
|
751
|
+
// Install Codex hook scripts + hooks.json
|
|
752
|
+
const hooksRoot = installCodexHookScripts(rt.configDir);
|
|
753
|
+
const hooksJsonPath = path.join(rt.configDir, 'hooks.json');
|
|
754
|
+
const existingHooks = fs.existsSync(hooksJsonPath)
|
|
755
|
+
? fs.readFileSync(hooksJsonPath, 'utf8')
|
|
153
756
|
: '';
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
[mcp.merlin]
|
|
157
|
-
command = "${command}"
|
|
158
|
-
${argsToml ? argsToml + '\n' : ''}${apiKey ? `[mcp.merlin.env]${envToml}\n` : ''}`;
|
|
757
|
+
writeFile(hooksJsonPath, mergeHooksJson(existingHooks, hooksRoot));
|
|
758
|
+
results.push('Installed ~/.codex/hooks.json');
|
|
159
759
|
|
|
760
|
+
// Write / update config.toml with MCP section and Codex features
|
|
761
|
+
const configTomlPath = path.join(rt.configDir, 'config.toml');
|
|
762
|
+
const { command, args } = buildMcpCommand(useGlobalBinary);
|
|
160
763
|
let tomlContent = fs.existsSync(configTomlPath)
|
|
161
764
|
? fs.readFileSync(configTomlPath, 'utf8')
|
|
162
765
|
: '';
|
|
163
766
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
767
|
+
tomlContent = removeTomlSection(removeTomlSection(tomlContent, 'mcp.merlin'), 'mcp_servers.merlin');
|
|
768
|
+
tomlContent = removeTomlSection(tomlContent, 'features');
|
|
769
|
+
tomlContent = removeTomlSection(tomlContent, 'tui');
|
|
770
|
+
tomlContent = removeTomlSection(tomlContent, 'agents');
|
|
771
|
+
tomlContent = removeTomlSection(tomlContent, 'shell_environment_policy');
|
|
772
|
+
tomlContent = tomlContent
|
|
773
|
+
.replace(/^project_doc_fallback_filenames = .*$/gm, '')
|
|
774
|
+
.replace(/^codex_hooks = .*$/gm, '')
|
|
775
|
+
.replace(/^notifications = .*$/gm, '')
|
|
776
|
+
.replace(/^max_threads = .*$/gm, '')
|
|
777
|
+
.replace(/^max_depth = .*$/gm, '')
|
|
778
|
+
.replace(/^inherit = .*$/gm, '')
|
|
779
|
+
.replace(/^startup_timeout_sec = .*$/gm, '')
|
|
780
|
+
.replace(/^tool_timeout_sec = .*$/gm, '')
|
|
781
|
+
.replace(/^command = "merlin-brain"\n?/m, '')
|
|
782
|
+
.replace(/^args = \[.*create-merlin-brain.*\]\n?/m, '')
|
|
783
|
+
.replace(/^args = \[.*packages\/create-merlin-brain\/bin\/serve\.js.*\]\n?/m, '')
|
|
784
|
+
.trimEnd();
|
|
785
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'features', 'codex_hooks = true');
|
|
786
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'tui', 'notifications = true');
|
|
787
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_threads = 8');
|
|
788
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_depth = 2');
|
|
789
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'shell_environment_policy', 'inherit = "all"');
|
|
790
|
+
tomlContent = ensureTomlTopLevelLine(tomlContent, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
|
|
791
|
+
|
|
792
|
+
const mcpLines = [
|
|
793
|
+
`command = "${command}"`,
|
|
794
|
+
args ? `args = [${args.map((a) => `"${a}"`).join(', ')}]` : null,
|
|
795
|
+
'startup_timeout_sec = 20',
|
|
796
|
+
'tool_timeout_sec = 120',
|
|
797
|
+
].filter(Boolean).join('\n');
|
|
798
|
+
|
|
799
|
+
tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin', mcpLines);
|
|
800
|
+
if (apiKey) {
|
|
801
|
+
tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin.env', `MERLIN_API_KEY = "${apiKey}"`);
|
|
169
802
|
}
|
|
170
803
|
|
|
804
|
+
writeFile(configTomlPath, tomlContent.trimEnd() + '\n');
|
|
805
|
+
results.push('Configured Merlin MCP and Codex features in ~/.codex/config.toml');
|
|
806
|
+
|
|
171
807
|
return results;
|
|
172
808
|
}
|
|
173
809
|
|
|
@@ -216,8 +852,8 @@ function configureOpenCode(rt, useGlobalBinary, apiKey) {
|
|
|
216
852
|
results.push('AGENTS.md already has Merlin (skipped)');
|
|
217
853
|
} else {
|
|
218
854
|
const combined = existingAgentsMd
|
|
219
|
-
? existingAgentsMd.trimEnd() + '\n\n---\n\n' +
|
|
220
|
-
:
|
|
855
|
+
? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('opencode')
|
|
856
|
+
: buildInstructionMd('opencode');
|
|
221
857
|
fs.writeFileSync(agentsMdPath, combined);
|
|
222
858
|
results.push('Wrote ~/.opencode/AGENTS.md');
|
|
223
859
|
}
|
|
@@ -246,8 +882,8 @@ function configureGemini(rt, useGlobalBinary, apiKey) {
|
|
|
246
882
|
results.push('GEMINI.md already has Merlin (skipped)');
|
|
247
883
|
} else {
|
|
248
884
|
const combined = existingGeminiMd
|
|
249
|
-
? existingGeminiMd.trimEnd() + '\n\n---\n\n' +
|
|
250
|
-
:
|
|
885
|
+
? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('gemini')
|
|
886
|
+
: buildInstructionMd('gemini');
|
|
251
887
|
fs.writeFileSync(geminiMdPath, combined);
|
|
252
888
|
results.push('Wrote ~/.gemini/GEMINI.md');
|
|
253
889
|
}
|
|
@@ -297,22 +933,67 @@ function generateProjectRuntimeFiles(projectDir, runtimeIds = ['all']) {
|
|
|
297
933
|
? ['codex', 'opencode', 'gemini']
|
|
298
934
|
: runtimeIds;
|
|
299
935
|
|
|
300
|
-
const agentsMd = buildAgentsMd();
|
|
301
|
-
|
|
302
936
|
// AGENTS.md — used by Codex and OpenCode
|
|
303
937
|
if (targets.includes('codex') || targets.includes('opencode')) {
|
|
304
938
|
const agentsMdPath = path.join(projectDir, 'AGENTS.md');
|
|
305
939
|
if (!fs.existsSync(agentsMdPath)) {
|
|
306
|
-
|
|
940
|
+
const instructionMd = targets.includes('codex')
|
|
941
|
+
? buildInstructionMd('codex')
|
|
942
|
+
: buildInstructionMd('opencode');
|
|
943
|
+
fs.writeFileSync(agentsMdPath, instructionMd);
|
|
307
944
|
results.push(`Created ${agentsMdPath}`);
|
|
308
945
|
}
|
|
309
946
|
}
|
|
310
947
|
|
|
948
|
+
if (targets.includes('codex')) {
|
|
949
|
+
const projectConfigPath = path.join(projectDir, '.codex', 'config.toml');
|
|
950
|
+
let projectConfig = fs.existsSync(projectConfigPath)
|
|
951
|
+
? fs.readFileSync(projectConfigPath, 'utf8')
|
|
952
|
+
: '';
|
|
953
|
+
projectConfig = ensureTomlTopLevelLine(projectConfig, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
|
|
954
|
+
projectConfig = ensureTomlSectionLine(projectConfig, 'features', 'codex_hooks = true');
|
|
955
|
+
projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_threads = 8');
|
|
956
|
+
projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_depth = 2');
|
|
957
|
+
writeFile(projectConfigPath, projectConfig.trimEnd() + '\n');
|
|
958
|
+
results.push(`Created ${projectConfigPath}`);
|
|
959
|
+
|
|
960
|
+
const projectHooksRoot = path.join(projectDir, '.codex', 'merlin', 'hooks');
|
|
961
|
+
ensureDir(projectHooksRoot);
|
|
962
|
+
const hookSpecs = {
|
|
963
|
+
'session-start.sh': buildCodexSessionStartHook(),
|
|
964
|
+
'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
|
|
965
|
+
'bash-pre-tool.sh': buildCodexBashPreToolHook(),
|
|
966
|
+
'bash-post-tool.sh': buildCodexBashPostToolHook(),
|
|
967
|
+
'stop.sh': buildCodexStopHook(),
|
|
968
|
+
};
|
|
969
|
+
for (const [name, content] of Object.entries(hookSpecs)) {
|
|
970
|
+
const hookPath = path.join(projectHooksRoot, name);
|
|
971
|
+
writeFile(hookPath, content);
|
|
972
|
+
fs.chmodSync(hookPath, '755');
|
|
973
|
+
}
|
|
974
|
+
const projectHooksPath = path.join(projectDir, '.codex', 'hooks.json');
|
|
975
|
+
const existingProjectHooks = fs.existsSync(projectHooksPath)
|
|
976
|
+
? fs.readFileSync(projectHooksPath, 'utf8')
|
|
977
|
+
: '';
|
|
978
|
+
writeFile(projectHooksPath, mergeHooksJson(existingProjectHooks, projectHooksRoot));
|
|
979
|
+
results.push(`Created ${path.join(projectDir, '.codex', 'hooks.json')}`);
|
|
980
|
+
|
|
981
|
+
const createdAgents = installCodexAgents(projectDir);
|
|
982
|
+
if (createdAgents.length) {
|
|
983
|
+
results.push(`Installed ${createdAgents.length} Codex agents`);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const createdSkills = installCodexSkills(projectDir);
|
|
987
|
+
if (createdSkills.length) {
|
|
988
|
+
results.push(`Installed ${createdSkills.length} Codex skills`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
311
992
|
// GEMINI.md — used by Gemini CLI
|
|
312
993
|
if (targets.includes('gemini')) {
|
|
313
994
|
const geminiMdPath = path.join(projectDir, 'GEMINI.md');
|
|
314
995
|
if (!fs.existsSync(geminiMdPath)) {
|
|
315
|
-
fs.writeFileSync(geminiMdPath,
|
|
996
|
+
fs.writeFileSync(geminiMdPath, buildInstructionMd('gemini'));
|
|
316
997
|
results.push(`Created ${geminiMdPath}`);
|
|
317
998
|
}
|
|
318
999
|
}
|
|
@@ -392,5 +1073,5 @@ module.exports = {
|
|
|
392
1073
|
detectRuntimes,
|
|
393
1074
|
configureRuntimes,
|
|
394
1075
|
generateProjectRuntimeFiles,
|
|
395
|
-
|
|
1076
|
+
buildInstructionMd,
|
|
396
1077
|
};
|