create-merlin-brain 3.22.0 → 3.23.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 +22 -4
- package/bin/merlin-ask.cjs +111 -0
- package/bin/merlin-cli.cjs +22 -0
- package/bin/runtime-adapters.cjs +678 -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 +45 -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/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 +4 -0
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +4 -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,165 @@ 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
|
+
## Codex + Merlin Workflow
|
|
189
|
+
|
|
190
|
+
- For codebase questions, start with \`merlin_search("query")\` or \`merlin_get_context("task")\`.
|
|
191
|
+
- Before every code edit, call \`merlin_get_context("your task")\`.
|
|
192
|
+
- When local file search is still needed, prefer fast repo-native tools such as \`rg\`.
|
|
193
|
+
- Use Codex's normal execution style: inspect first, edit surgically, verify after changes.
|
|
194
|
+
- Keep progress updates concise and factual while work is in flight.
|
|
195
|
+
- Prefer Merlin skills and custom agents installed in this repo when the task matches them.
|
|
196
|
+
|
|
197
|
+
## Editing Discipline
|
|
198
|
+
|
|
199
|
+
- Prefer \`apply_patch\` for manual edits.
|
|
200
|
+
- Avoid broad rewrites when a narrow patch is sufficient.
|
|
201
|
+
- Preserve user changes you did not make.
|
|
202
|
+
- Verify behavior after implementation work before claiming completion.
|
|
203
|
+
|
|
204
|
+
## Delegation
|
|
205
|
+
|
|
206
|
+
- Use Merlin tools first for routing and context.
|
|
207
|
+
- Use Codex sub-agents only for clearly bounded side tasks that can run in parallel.
|
|
208
|
+
- Keep the critical path local when the next step depends on immediate code understanding.
|
|
209
|
+
|
|
210
|
+
## MCP Tools Available
|
|
211
|
+
|
|
212
|
+
- \`merlin_get_selected_repo\` — identify the active repository
|
|
213
|
+
- \`merlin_get_project_status\` — load project state and active tasks
|
|
214
|
+
- \`merlin_get_context(task)\` — fetch targeted implementation context
|
|
215
|
+
- \`merlin_find_files(query)\` — locate files by purpose
|
|
216
|
+
- \`merlin_search(query)\` — semantic code search
|
|
217
|
+
|
|
218
|
+
## Defaults
|
|
219
|
+
|
|
220
|
+
- Search before writing.
|
|
221
|
+
- Reuse existing patterns before introducing new ones.
|
|
222
|
+
- If Merlin Sights is unavailable, continue with disciplined local exploration instead of blocking.
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
73
226
|
return `# Merlin Brain — AI Development System
|
|
74
227
|
|
|
75
228
|
> Installed by Merlin (https://merlin.build). Keep this file to preserve Merlin context.
|
|
@@ -115,6 +268,425 @@ The Merlin MCP server exposes these tools:
|
|
|
115
268
|
`;
|
|
116
269
|
}
|
|
117
270
|
|
|
271
|
+
function buildCodexHooksJson(hooksRoot) {
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
hooks: {
|
|
274
|
+
SessionStart: [
|
|
275
|
+
{
|
|
276
|
+
matcher: 'startup|resume',
|
|
277
|
+
hooks: [
|
|
278
|
+
{
|
|
279
|
+
type: 'command',
|
|
280
|
+
command: `bash "${path.join(hooksRoot, 'session-start.sh')}"`,
|
|
281
|
+
statusMessage: 'Merlin booting',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
UserPromptSubmit: [
|
|
287
|
+
{
|
|
288
|
+
hooks: [
|
|
289
|
+
{
|
|
290
|
+
type: 'command',
|
|
291
|
+
command: `bash "${path.join(hooksRoot, 'user-prompt-router.sh')}"`,
|
|
292
|
+
statusMessage: 'Merlin routing',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
PreToolUse: [
|
|
298
|
+
{
|
|
299
|
+
matcher: 'Bash',
|
|
300
|
+
hooks: [
|
|
301
|
+
{
|
|
302
|
+
type: 'command',
|
|
303
|
+
command: `bash "${path.join(hooksRoot, 'bash-pre-tool.sh')}"`,
|
|
304
|
+
statusMessage: 'Merlin command guard',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
PostToolUse: [
|
|
310
|
+
{
|
|
311
|
+
matcher: 'Bash',
|
|
312
|
+
hooks: [
|
|
313
|
+
{
|
|
314
|
+
type: 'command',
|
|
315
|
+
command: `bash "${path.join(hooksRoot, 'bash-post-tool.sh')}"`,
|
|
316
|
+
statusMessage: 'Merlin command review',
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
Stop: [
|
|
322
|
+
{
|
|
323
|
+
hooks: [
|
|
324
|
+
{
|
|
325
|
+
type: 'command',
|
|
326
|
+
command: `bash "${path.join(hooksRoot, 'stop.sh')}"`,
|
|
327
|
+
statusMessage: 'Merlin wrap-up',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
}, null, 2) + '\n';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function buildCodexSessionStartHook() {
|
|
337
|
+
return `#!/usr/bin/env bash
|
|
338
|
+
set -euo pipefail
|
|
339
|
+
trap 'echo "{}"; exit 0' ERR
|
|
340
|
+
|
|
341
|
+
MERLIN_HOME="\${HOME}/.codex/merlin"
|
|
342
|
+
mkdir -p "\${MERLIN_HOME}/analytics" "\${MERLIN_HOME}/sessions" 2>/dev/null || true
|
|
343
|
+
|
|
344
|
+
_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."
|
|
345
|
+
_context="\${_context} Use Merlin context before edits: call merlin_get_context(task)."
|
|
346
|
+
_context="\${_context} Prefer Merlin skills and custom agents installed in this repository when they match the task."
|
|
347
|
+
_context="\${_context} Keep the Codex experience pragmatic: inspect first, patch surgically, verify before claiming completion."
|
|
348
|
+
_context="\${_context} Prefix visible progress updates with the Merlin badge when practical: ⟡🔮 MERLIN ›"
|
|
349
|
+
|
|
350
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "\${_context//\"/\\\\\"}"
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildCodexUserPromptRouterHook() {
|
|
355
|
+
return `#!/usr/bin/env bash
|
|
356
|
+
set -euo pipefail
|
|
357
|
+
trap 'echo "{}"; exit 0' ERR
|
|
358
|
+
|
|
359
|
+
input=""
|
|
360
|
+
if [ ! -t 0 ]; then
|
|
361
|
+
input=$(cat 2>/dev/null || true)
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
[ -z "$input" ] && echo "{}" && exit 0
|
|
365
|
+
|
|
366
|
+
prompt=""
|
|
367
|
+
if command -v jq >/dev/null 2>&1; then
|
|
368
|
+
prompt=$(echo "$input" | jq -r '.prompt // .userPrompt // empty' 2>/dev/null || true)
|
|
369
|
+
else
|
|
370
|
+
prompt=$(echo "$input" | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/' 2>/dev/null || true)
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
[ -z "$prompt" ] && echo "{}" && exit 0
|
|
374
|
+
|
|
375
|
+
clean=$(printf '%s' "$prompt" | sed -E -e 's/<[^>]+>//g' -e 's|https?://[^ ]*||g' -e 's|/[a-zA-Z0-9._/-]+||g' -e 's/\`[^\`]*\`//g')
|
|
376
|
+
|
|
377
|
+
suggestion=""
|
|
378
|
+
if echo "$clean" | grep -qiE "resume|pick up|continue|where were we"; then
|
|
379
|
+
suggestion='Merlin routing: resume context and use the merlin-resume skill.'
|
|
380
|
+
elif echo "$clean" | grep -qiE "progress|status|where are we|how far"; then
|
|
381
|
+
suggestion='Merlin routing: use the merlin-progress skill.'
|
|
382
|
+
elif echo "$clean" | grep -qiE "map codebase|understand this repo|learn this codebase|explore the architecture"; then
|
|
383
|
+
suggestion='Merlin routing: use the merlin-map-codebase skill before implementation.'
|
|
384
|
+
elif echo "$clean" | grep -qiE "bug|crash|error|not working|fix|failing|exception"; then
|
|
385
|
+
suggestion='Merlin routing: debug first, then route bounded work to implementation and verification agents.'
|
|
386
|
+
elif echo "$clean" | grep -qiE "refactor|cleanup|clean up|dry|restructure"; then
|
|
387
|
+
suggestion='Merlin routing: keep scope narrow, preserve behavior, and use implementation plus verification agents.'
|
|
388
|
+
elif echo "$clean" | grep -qiE "build|add|create|implement|new feature|develop"; then
|
|
389
|
+
suggestion='Merlin routing: gather Merlin context first, then execute with implementation-focused agents.'
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
[ -z "$suggestion" ] && echo "{}" && exit 0
|
|
393
|
+
|
|
394
|
+
printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\\n' "\${suggestion//\"/\\\\\"}"
|
|
395
|
+
`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildCodexBashPreToolHook() {
|
|
399
|
+
return `#!/usr/bin/env bash
|
|
400
|
+
set -euo pipefail
|
|
401
|
+
trap 'echo "{}"; exit 0' ERR
|
|
402
|
+
|
|
403
|
+
input=""
|
|
404
|
+
if [ ! -t 0 ]; then
|
|
405
|
+
input=$(cat 2>/dev/null || true)
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
[ -z "$input" ] && echo "{}" && exit 0
|
|
409
|
+
|
|
410
|
+
command_str=""
|
|
411
|
+
if command -v jq >/dev/null 2>&1; then
|
|
412
|
+
command_str=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
|
|
413
|
+
else
|
|
414
|
+
command_str=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
[ -z "$command_str" ] && echo "{}" && exit 0
|
|
418
|
+
|
|
419
|
+
block() {
|
|
420
|
+
printf '{"decision":"block","reason":"%s"}\\n' "$1"
|
|
421
|
+
exit 2
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if echo "$command_str" | grep -qE '(curl|wget)\\s[^|]*\\|\\s*(bash|sh|zsh|python|ruby|perl)\\b' 2>/dev/null; then
|
|
425
|
+
block "Merlin blocked pipe-to-shell execution."
|
|
426
|
+
fi
|
|
427
|
+
if echo "$command_str" | grep -qE '(^|\\s|;|&&|\\|\\|)(sudo|su)\\s' 2>/dev/null; then
|
|
428
|
+
block "Merlin blocked privilege escalation."
|
|
429
|
+
fi
|
|
430
|
+
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
|
|
431
|
+
block "Merlin blocked a destructive git command."
|
|
432
|
+
fi
|
|
433
|
+
if echo "$command_str" | grep -qE '\\brm\\s+(-[a-z]*r[a-z]*f|-rf|-fr|--recursive)' 2>/dev/null; then
|
|
434
|
+
block "Merlin blocked a destructive rm command."
|
|
435
|
+
fi
|
|
436
|
+
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
|
|
437
|
+
block "Merlin blocked a command containing a secret or private key."
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
echo '{}'
|
|
441
|
+
`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function buildCodexBashPostToolHook() {
|
|
445
|
+
return `#!/usr/bin/env bash
|
|
446
|
+
set -euo pipefail
|
|
447
|
+
trap 'echo "{}"; exit 0' ERR
|
|
448
|
+
|
|
449
|
+
input=""
|
|
450
|
+
if [ ! -t 0 ]; then
|
|
451
|
+
input=$(cat 2>/dev/null || true)
|
|
452
|
+
fi
|
|
453
|
+
|
|
454
|
+
[ -z "$input" ] && echo "{}" && exit 0
|
|
455
|
+
|
|
456
|
+
if command -v jq >/dev/null 2>&1; then
|
|
457
|
+
cmd=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
|
|
458
|
+
else
|
|
459
|
+
cmd=""
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
if echo "$cmd" | grep -qiE 'npm test|pnpm test|yarn test|vitest|pytest|cargo test|go test'; then
|
|
463
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"Merlin note: summarize whether verification passed before moving on."}}\\n'
|
|
464
|
+
exit 0
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
echo '{}'
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function buildCodexStopHook() {
|
|
472
|
+
return `#!/usr/bin/env bash
|
|
473
|
+
set -euo pipefail
|
|
474
|
+
trap 'echo "{}"; exit 0' ERR
|
|
475
|
+
|
|
476
|
+
MERLIN_HOME="\${HOME}/.codex/merlin"
|
|
477
|
+
mkdir -p "\${MERLIN_HOME}/sessions" 2>/dev/null || true
|
|
478
|
+
|
|
479
|
+
if command -v jq >/dev/null 2>&1; then
|
|
480
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
481
|
+
jq -n --arg ts "$ts" '{ timestamp: $ts }' > "\${MERLIN_HOME}/sessions/session-$(date +%s).json" 2>/dev/null || true
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
echo '{}'
|
|
485
|
+
`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function installCodexHookScripts(codexDir) {
|
|
489
|
+
const hooksRoot = path.join(codexDir, 'merlin', 'hooks');
|
|
490
|
+
const scripts = {
|
|
491
|
+
'session-start.sh': buildCodexSessionStartHook(),
|
|
492
|
+
'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
|
|
493
|
+
'bash-pre-tool.sh': buildCodexBashPreToolHook(),
|
|
494
|
+
'bash-post-tool.sh': buildCodexBashPostToolHook(),
|
|
495
|
+
'stop.sh': buildCodexStopHook(),
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
ensureDir(hooksRoot);
|
|
499
|
+
for (const [name, content] of Object.entries(scripts)) {
|
|
500
|
+
const target = path.join(hooksRoot, name);
|
|
501
|
+
writeFile(target, content);
|
|
502
|
+
fs.chmodSync(target, '755');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return hooksRoot;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildCodexAgentSpecs() {
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
filename: 'implementation-dev.toml',
|
|
512
|
+
name: 'implementation_dev',
|
|
513
|
+
description: 'Implementation specialist for bounded code changes with verification.',
|
|
514
|
+
model: 'gpt-5.4',
|
|
515
|
+
effort: 'medium',
|
|
516
|
+
sandbox: 'workspace-write',
|
|
517
|
+
nicknames: ['Forge', 'Patch', 'Build'],
|
|
518
|
+
instructions: 'Implement the requested change with minimal surface area. Inspect existing patterns first, preserve unrelated user edits, and verify behavior before handing back results.',
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
filename: 'tests-qa.toml',
|
|
522
|
+
name: 'tests_qa',
|
|
523
|
+
description: 'Testing specialist focused on regressions, missing cases, and verification.',
|
|
524
|
+
model: 'gpt-5.4-mini',
|
|
525
|
+
effort: 'medium',
|
|
526
|
+
sandbox: 'workspace-write',
|
|
527
|
+
nicknames: ['Spec', 'Check', 'Proof'],
|
|
528
|
+
instructions: 'Focus on tests, reproducibility, and verification evidence. Add or improve tests when justified, and report concrete coverage gaps when testing is blocked.',
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
filename: 'system-architect.toml',
|
|
532
|
+
name: 'system_architect',
|
|
533
|
+
description: 'Architecture specialist for design tradeoffs and decomposition.',
|
|
534
|
+
model: 'gpt-5.4',
|
|
535
|
+
effort: 'high',
|
|
536
|
+
sandbox: 'read-only',
|
|
537
|
+
nicknames: ['Northstar', 'Grid', 'Frame'],
|
|
538
|
+
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.',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
filename: 'docs-keeper.toml',
|
|
542
|
+
name: 'docs_keeper',
|
|
543
|
+
description: 'Documentation specialist for READMEs, usage guides, and project docs.',
|
|
544
|
+
model: 'gpt-5.4-mini',
|
|
545
|
+
effort: 'medium',
|
|
546
|
+
sandbox: 'workspace-write',
|
|
547
|
+
nicknames: ['Ledger', 'Script', 'Index'],
|
|
548
|
+
instructions: 'Update documentation to match the actual implementation. Prefer concise, accurate edits tied to real behavior and verified paths.',
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
filename: 'hardening-guard.toml',
|
|
552
|
+
name: 'hardening_guard',
|
|
553
|
+
description: 'Security and resilience reviewer for edge cases and failure handling.',
|
|
554
|
+
model: 'gpt-5.4',
|
|
555
|
+
effort: 'high',
|
|
556
|
+
sandbox: 'read-only',
|
|
557
|
+
nicknames: ['Shield', 'Gate', 'Sentinel'],
|
|
558
|
+
instructions: 'Review for security issues, data loss, unsafe command usage, auth gaps, and weak failure handling. Lead with concrete risks and mitigations.',
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
filename: 'merlin-codebase-mapper.toml',
|
|
562
|
+
name: 'merlin_codebase_mapper',
|
|
563
|
+
description: 'Read-only codebase mapper for architecture, flows, and key files.',
|
|
564
|
+
model: 'gpt-5.4-mini',
|
|
565
|
+
effort: 'medium',
|
|
566
|
+
sandbox: 'read-only',
|
|
567
|
+
nicknames: ['Scout', 'Atlas', 'Survey'],
|
|
568
|
+
instructions: 'Map the relevant code paths, file ownership, and architectural seams before implementation. Prefer precise citations over broad summaries.',
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
filename: 'merlin-researcher.toml',
|
|
572
|
+
name: 'merlin_researcher',
|
|
573
|
+
description: 'Research specialist for external docs and fast-moving technical questions.',
|
|
574
|
+
model: 'gpt-5.4-mini',
|
|
575
|
+
effort: 'medium',
|
|
576
|
+
sandbox: 'read-only',
|
|
577
|
+
nicknames: ['Query', 'Lens', 'Signal'],
|
|
578
|
+
instructions: 'Use primary sources when research is needed. Distinguish verified facts from inference and return concise findings with citations.',
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
filename: 'merlin-verifier.toml',
|
|
582
|
+
name: 'merlin_verifier',
|
|
583
|
+
description: 'Verification specialist for validation, behavior checks, and release confidence.',
|
|
584
|
+
model: 'gpt-5.4-mini',
|
|
585
|
+
effort: 'medium',
|
|
586
|
+
sandbox: 'workspace-write',
|
|
587
|
+
nicknames: ['Audit', 'Pulse', 'Trace'],
|
|
588
|
+
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.',
|
|
589
|
+
},
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function installCodexAgents(projectDir) {
|
|
594
|
+
const agentsDir = path.join(projectDir, '.codex', 'agents');
|
|
595
|
+
ensureDir(agentsDir);
|
|
596
|
+
const created = [];
|
|
597
|
+
|
|
598
|
+
for (const spec of buildCodexAgentSpecs()) {
|
|
599
|
+
const content = [
|
|
600
|
+
`name = "${spec.name}"`,
|
|
601
|
+
`description = "${spec.description}"`,
|
|
602
|
+
`model = "${spec.model}"`,
|
|
603
|
+
`model_reasoning_effort = "${spec.effort}"`,
|
|
604
|
+
`sandbox_mode = "${spec.sandbox}"`,
|
|
605
|
+
`nickname_candidates = [${spec.nicknames.map((n) => `"${n}"`).join(', ')}]`,
|
|
606
|
+
`developer_instructions = ${tomlMultiline(spec.instructions)}`,
|
|
607
|
+
'',
|
|
608
|
+
].join('\n');
|
|
609
|
+
writeFile(path.join(agentsDir, spec.filename), content);
|
|
610
|
+
created.push(path.join(agentsDir, spec.filename));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return created;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function buildCodexSkillSpecs() {
|
|
617
|
+
return [
|
|
618
|
+
{
|
|
619
|
+
dir: 'merlin-map-codebase',
|
|
620
|
+
content: `---
|
|
621
|
+
name: merlin-map-codebase
|
|
622
|
+
description: Use when the user wants to understand a repository, onboard to a codebase, or map architecture before implementing changes.
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
Before making architectural claims, call \`merlin_get_selected_repo\` and \`merlin_get_project_status\`.
|
|
626
|
+
Then use \`merlin_search\` and \`merlin_get_context\` to identify the core files, flows, and conventions relevant to the request.
|
|
627
|
+
When the task is large, prefer the \`merlin_codebase_mapper\` custom agent for bounded exploration.`,
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
dir: 'merlin-progress',
|
|
631
|
+
content: `---
|
|
632
|
+
name: merlin-progress
|
|
633
|
+
description: Use when the user asks for current status, progress, next steps, or where work left off in the repository.
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
Call \`merlin_get_project_status\` first.
|
|
637
|
+
Summarize the current state, active tasks, and recommended next step.
|
|
638
|
+
If local planning files exist, reconcile them with Merlin status instead of trusting either source blindly.`,
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
dir: 'merlin-resume',
|
|
642
|
+
content: `---
|
|
643
|
+
name: merlin-resume
|
|
644
|
+
description: Use when the user is returning to a project and wants to continue, resume, or recover context quickly.
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
Boot with Merlin tools first, then reconstruct the current state from project status, local planning files, and recent git changes.
|
|
648
|
+
Prefer concise orientation: what was in progress, what is blocked, and the most reasonable next action.`,
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
dir: 'merlin-workflow',
|
|
652
|
+
content: `---
|
|
653
|
+
name: merlin-workflow
|
|
654
|
+
description: Use when the task should be decomposed into architecture, implementation, testing, docs, or verification specialists instead of one monolithic pass.
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
Use Merlin context before decomposition.
|
|
658
|
+
For bounded parallel work, use the custom Codex agents installed in \`.codex/agents\`.
|
|
659
|
+
Keep the critical path local when a side task would block the very next action.`,
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
dir: 'merlin-verify',
|
|
663
|
+
content: `---
|
|
664
|
+
name: merlin-verify
|
|
665
|
+
description: Use when implementation work is complete and you need focused validation, test review, or release confidence.
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
Prefer direct evidence over narration.
|
|
669
|
+
Run or inspect the relevant verification commands, summarize what passed, what failed, and any residual risk.
|
|
670
|
+
Use the \`merlin_verifier\` or \`tests_qa\` agent for bounded validation passes when parallel work helps.`,
|
|
671
|
+
},
|
|
672
|
+
];
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function installCodexSkills(projectDir) {
|
|
676
|
+
const skillsRoot = path.join(projectDir, '.agents', 'skills');
|
|
677
|
+
ensureDir(skillsRoot);
|
|
678
|
+
const created = [];
|
|
679
|
+
|
|
680
|
+
for (const spec of buildCodexSkillSpecs()) {
|
|
681
|
+
const skillDir = path.join(skillsRoot, spec.dir);
|
|
682
|
+
ensureDir(skillDir);
|
|
683
|
+
writeFile(path.join(skillDir, 'SKILL.md'), spec.content.trim() + '\n');
|
|
684
|
+
created.push(path.join(skillDir, 'SKILL.md'));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return created;
|
|
688
|
+
}
|
|
689
|
+
|
|
118
690
|
// ---------------------------------------------------------------------------
|
|
119
691
|
// Codex CLI adapter
|
|
120
692
|
// Writes: ~/.codex/AGENTS.md + ~/.codex/config.toml (MCP section)
|
|
@@ -130,7 +702,7 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
|
|
|
130
702
|
|
|
131
703
|
// Write AGENTS.md
|
|
132
704
|
const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
|
|
133
|
-
const agentsMd =
|
|
705
|
+
const agentsMd = buildInstructionMd('codex');
|
|
134
706
|
const existingAgentsMd = fs.existsSync(agentsMdPath)
|
|
135
707
|
? fs.readFileSync(agentsMdPath, 'utf8')
|
|
136
708
|
: '';
|
|
@@ -145,29 +717,62 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
|
|
|
145
717
|
results.push('Wrote ~/.codex/AGENTS.md');
|
|
146
718
|
}
|
|
147
719
|
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
?
|
|
720
|
+
// Install Codex hook scripts + hooks.json
|
|
721
|
+
const hooksRoot = installCodexHookScripts(rt.configDir);
|
|
722
|
+
const hooksJsonPath = path.join(rt.configDir, 'hooks.json');
|
|
723
|
+
const existingHooks = fs.existsSync(hooksJsonPath)
|
|
724
|
+
? fs.readFileSync(hooksJsonPath, 'utf8')
|
|
153
725
|
: '';
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
[mcp.merlin]
|
|
157
|
-
command = "${command}"
|
|
158
|
-
${argsToml ? argsToml + '\n' : ''}${apiKey ? `[mcp.merlin.env]${envToml}\n` : ''}`;
|
|
726
|
+
writeFile(hooksJsonPath, mergeHooksJson(existingHooks, hooksRoot));
|
|
727
|
+
results.push('Installed ~/.codex/hooks.json');
|
|
159
728
|
|
|
729
|
+
// Write / update config.toml with MCP section and Codex features
|
|
730
|
+
const configTomlPath = path.join(rt.configDir, 'config.toml');
|
|
731
|
+
const { command, args } = buildMcpCommand(useGlobalBinary);
|
|
160
732
|
let tomlContent = fs.existsSync(configTomlPath)
|
|
161
733
|
? fs.readFileSync(configTomlPath, 'utf8')
|
|
162
734
|
: '';
|
|
163
735
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
736
|
+
tomlContent = removeTomlSection(removeTomlSection(tomlContent, 'mcp.merlin'), 'mcp_servers.merlin');
|
|
737
|
+
tomlContent = removeTomlSection(tomlContent, 'features');
|
|
738
|
+
tomlContent = removeTomlSection(tomlContent, 'tui');
|
|
739
|
+
tomlContent = removeTomlSection(tomlContent, 'agents');
|
|
740
|
+
tomlContent = removeTomlSection(tomlContent, 'shell_environment_policy');
|
|
741
|
+
tomlContent = tomlContent
|
|
742
|
+
.replace(/^project_doc_fallback_filenames = .*$/gm, '')
|
|
743
|
+
.replace(/^codex_hooks = .*$/gm, '')
|
|
744
|
+
.replace(/^notifications = .*$/gm, '')
|
|
745
|
+
.replace(/^max_threads = .*$/gm, '')
|
|
746
|
+
.replace(/^max_depth = .*$/gm, '')
|
|
747
|
+
.replace(/^inherit = .*$/gm, '')
|
|
748
|
+
.replace(/^startup_timeout_sec = .*$/gm, '')
|
|
749
|
+
.replace(/^tool_timeout_sec = .*$/gm, '')
|
|
750
|
+
.replace(/^command = "merlin-brain"\n?/m, '')
|
|
751
|
+
.replace(/^args = \[.*create-merlin-brain.*\]\n?/m, '')
|
|
752
|
+
.replace(/^args = \[.*packages\/create-merlin-brain\/bin\/serve\.js.*\]\n?/m, '')
|
|
753
|
+
.trimEnd();
|
|
754
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'features', 'codex_hooks = true');
|
|
755
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'tui', 'notifications = true');
|
|
756
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_threads = 8');
|
|
757
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_depth = 2');
|
|
758
|
+
tomlContent = ensureTomlSectionLine(tomlContent, 'shell_environment_policy', 'inherit = "all"');
|
|
759
|
+
tomlContent = ensureTomlTopLevelLine(tomlContent, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
|
|
760
|
+
|
|
761
|
+
const mcpLines = [
|
|
762
|
+
`command = "${command}"`,
|
|
763
|
+
args ? `args = [${args.map((a) => `"${a}"`).join(', ')}]` : null,
|
|
764
|
+
'startup_timeout_sec = 20',
|
|
765
|
+
'tool_timeout_sec = 120',
|
|
766
|
+
].filter(Boolean).join('\n');
|
|
767
|
+
|
|
768
|
+
tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin', mcpLines);
|
|
769
|
+
if (apiKey) {
|
|
770
|
+
tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin.env', `MERLIN_API_KEY = "${apiKey}"`);
|
|
169
771
|
}
|
|
170
772
|
|
|
773
|
+
writeFile(configTomlPath, tomlContent.trimEnd() + '\n');
|
|
774
|
+
results.push('Configured Merlin MCP and Codex features in ~/.codex/config.toml');
|
|
775
|
+
|
|
171
776
|
return results;
|
|
172
777
|
}
|
|
173
778
|
|
|
@@ -216,8 +821,8 @@ function configureOpenCode(rt, useGlobalBinary, apiKey) {
|
|
|
216
821
|
results.push('AGENTS.md already has Merlin (skipped)');
|
|
217
822
|
} else {
|
|
218
823
|
const combined = existingAgentsMd
|
|
219
|
-
? existingAgentsMd.trimEnd() + '\n\n---\n\n' +
|
|
220
|
-
:
|
|
824
|
+
? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('opencode')
|
|
825
|
+
: buildInstructionMd('opencode');
|
|
221
826
|
fs.writeFileSync(agentsMdPath, combined);
|
|
222
827
|
results.push('Wrote ~/.opencode/AGENTS.md');
|
|
223
828
|
}
|
|
@@ -246,8 +851,8 @@ function configureGemini(rt, useGlobalBinary, apiKey) {
|
|
|
246
851
|
results.push('GEMINI.md already has Merlin (skipped)');
|
|
247
852
|
} else {
|
|
248
853
|
const combined = existingGeminiMd
|
|
249
|
-
? existingGeminiMd.trimEnd() + '\n\n---\n\n' +
|
|
250
|
-
:
|
|
854
|
+
? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('gemini')
|
|
855
|
+
: buildInstructionMd('gemini');
|
|
251
856
|
fs.writeFileSync(geminiMdPath, combined);
|
|
252
857
|
results.push('Wrote ~/.gemini/GEMINI.md');
|
|
253
858
|
}
|
|
@@ -297,22 +902,67 @@ function generateProjectRuntimeFiles(projectDir, runtimeIds = ['all']) {
|
|
|
297
902
|
? ['codex', 'opencode', 'gemini']
|
|
298
903
|
: runtimeIds;
|
|
299
904
|
|
|
300
|
-
const agentsMd = buildAgentsMd();
|
|
301
|
-
|
|
302
905
|
// AGENTS.md — used by Codex and OpenCode
|
|
303
906
|
if (targets.includes('codex') || targets.includes('opencode')) {
|
|
304
907
|
const agentsMdPath = path.join(projectDir, 'AGENTS.md');
|
|
305
908
|
if (!fs.existsSync(agentsMdPath)) {
|
|
306
|
-
|
|
909
|
+
const instructionMd = targets.includes('codex')
|
|
910
|
+
? buildInstructionMd('codex')
|
|
911
|
+
: buildInstructionMd('opencode');
|
|
912
|
+
fs.writeFileSync(agentsMdPath, instructionMd);
|
|
307
913
|
results.push(`Created ${agentsMdPath}`);
|
|
308
914
|
}
|
|
309
915
|
}
|
|
310
916
|
|
|
917
|
+
if (targets.includes('codex')) {
|
|
918
|
+
const projectConfigPath = path.join(projectDir, '.codex', 'config.toml');
|
|
919
|
+
let projectConfig = fs.existsSync(projectConfigPath)
|
|
920
|
+
? fs.readFileSync(projectConfigPath, 'utf8')
|
|
921
|
+
: '';
|
|
922
|
+
projectConfig = ensureTomlTopLevelLine(projectConfig, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
|
|
923
|
+
projectConfig = ensureTomlSectionLine(projectConfig, 'features', 'codex_hooks = true');
|
|
924
|
+
projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_threads = 8');
|
|
925
|
+
projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_depth = 2');
|
|
926
|
+
writeFile(projectConfigPath, projectConfig.trimEnd() + '\n');
|
|
927
|
+
results.push(`Created ${projectConfigPath}`);
|
|
928
|
+
|
|
929
|
+
const projectHooksRoot = path.join(projectDir, '.codex', 'merlin', 'hooks');
|
|
930
|
+
ensureDir(projectHooksRoot);
|
|
931
|
+
const hookSpecs = {
|
|
932
|
+
'session-start.sh': buildCodexSessionStartHook(),
|
|
933
|
+
'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
|
|
934
|
+
'bash-pre-tool.sh': buildCodexBashPreToolHook(),
|
|
935
|
+
'bash-post-tool.sh': buildCodexBashPostToolHook(),
|
|
936
|
+
'stop.sh': buildCodexStopHook(),
|
|
937
|
+
};
|
|
938
|
+
for (const [name, content] of Object.entries(hookSpecs)) {
|
|
939
|
+
const hookPath = path.join(projectHooksRoot, name);
|
|
940
|
+
writeFile(hookPath, content);
|
|
941
|
+
fs.chmodSync(hookPath, '755');
|
|
942
|
+
}
|
|
943
|
+
const projectHooksPath = path.join(projectDir, '.codex', 'hooks.json');
|
|
944
|
+
const existingProjectHooks = fs.existsSync(projectHooksPath)
|
|
945
|
+
? fs.readFileSync(projectHooksPath, 'utf8')
|
|
946
|
+
: '';
|
|
947
|
+
writeFile(projectHooksPath, mergeHooksJson(existingProjectHooks, projectHooksRoot));
|
|
948
|
+
results.push(`Created ${path.join(projectDir, '.codex', 'hooks.json')}`);
|
|
949
|
+
|
|
950
|
+
const createdAgents = installCodexAgents(projectDir);
|
|
951
|
+
if (createdAgents.length) {
|
|
952
|
+
results.push(`Installed ${createdAgents.length} Codex agents`);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const createdSkills = installCodexSkills(projectDir);
|
|
956
|
+
if (createdSkills.length) {
|
|
957
|
+
results.push(`Installed ${createdSkills.length} Codex skills`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
311
961
|
// GEMINI.md — used by Gemini CLI
|
|
312
962
|
if (targets.includes('gemini')) {
|
|
313
963
|
const geminiMdPath = path.join(projectDir, 'GEMINI.md');
|
|
314
964
|
if (!fs.existsSync(geminiMdPath)) {
|
|
315
|
-
fs.writeFileSync(geminiMdPath,
|
|
965
|
+
fs.writeFileSync(geminiMdPath, buildInstructionMd('gemini'));
|
|
316
966
|
results.push(`Created ${geminiMdPath}`);
|
|
317
967
|
}
|
|
318
968
|
}
|
|
@@ -392,5 +1042,5 @@ module.exports = {
|
|
|
392
1042
|
detectRuntimes,
|
|
393
1043
|
configureRuntimes,
|
|
394
1044
|
generateProjectRuntimeFiles,
|
|
395
|
-
|
|
1045
|
+
buildInstructionMd,
|
|
396
1046
|
};
|