create-merlin-brain 3.4.7 → 3.4.9
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/bin/install.cjs +119 -80
- package/files/hooks/agent-sync.sh +6 -3
- package/files/hooks/post-edit-logger.sh +3 -1
- package/files/hooks/pre-edit-sights-check.sh +3 -1
- package/files/hooks/pre-edit-sights-enforce.md +9 -22
- package/files/hooks/session-end.sh +3 -1
- package/files/hooks/session-start-context.sh +23 -0
- package/files/hooks/session-start.sh +3 -1
- package/files/hooks/stop-check.md +10 -33
- package/files/hooks/subagent-context.sh +15 -3
- package/files/hooks/task-completed-verify.md +10 -11
- package/files/hooks/task-completed-verify.sh +3 -1
- package/files/hooks/teammate-idle-verify.sh +3 -1
- package/package.json +1 -1
package/bin/install.cjs
CHANGED
|
@@ -176,14 +176,18 @@ function ensureDir(dir) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
async function promptApiKey() {
|
|
179
|
+
async function promptApiKey(hasExisting) {
|
|
180
180
|
const rl = readline.createInterface({
|
|
181
181
|
input: process.stdin,
|
|
182
182
|
output: process.stdout,
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
const prompt = hasExisting
|
|
186
|
+
? `\n${colors.cyan}?${colors.reset} Merlin Sights API key (Enter to keep existing, or paste new): `
|
|
187
|
+
: `\n${colors.cyan}?${colors.reset} Merlin Sights API key (get one at ${colors.cyan}merlin.build${colors.reset}, Enter to skip): `;
|
|
188
|
+
|
|
185
189
|
return new Promise((resolve) => {
|
|
186
|
-
rl.question(
|
|
190
|
+
rl.question(prompt, (answer) => {
|
|
187
191
|
rl.close();
|
|
188
192
|
resolve(answer.trim());
|
|
189
193
|
});
|
|
@@ -272,9 +276,9 @@ merlin-loop() {
|
|
|
272
276
|
if (rcContent.includes('# Merlin Brain - The AI Brain') ||
|
|
273
277
|
rcContent.includes('# Merlin Brain - Makes Claude Code') ||
|
|
274
278
|
rcContent.includes('# Merlin Brain -')) {
|
|
275
|
-
// Remove old blocks
|
|
276
|
-
rcContent = rcContent.replace(/\n# ═+\n# Merlin Brain[\s\S]*?alias cc="claude"\n?/g, '');
|
|
277
|
-
rcContent = rcContent.replace(/\n# Merlin Brain - Makes Claude Code[\s\S]*?alias cc='claude'\n?/g, '');
|
|
279
|
+
// Remove old blocks (capture full block including trailing merlin-loop if present)
|
|
280
|
+
rcContent = rcContent.replace(/\n# ═+\n# Merlin Brain[\s\S]*?alias cc="claude"\n?([\s\S]*?merlin-loop\(\) \{[\s\S]*?\n\}\n)?/g, '');
|
|
281
|
+
rcContent = rcContent.replace(/\n# Merlin Brain - Makes Claude Code[\s\S]*?alias cc='claude'\n?([\s\S]*?merlin-loop\(\) \{[\s\S]*?\n\}\n)?/g, '');
|
|
278
282
|
rcContent = rcContent.replace(/\n# Merlin - Claude Code with[\s\S]*?alias cc=.*\n?/g, '');
|
|
279
283
|
fs.writeFileSync(rcFile, rcContent);
|
|
280
284
|
logSuccess('Updating existing Merlin shell integration');
|
|
@@ -302,7 +306,7 @@ function cleanupLegacy() {
|
|
|
302
306
|
const content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
303
307
|
if (content.includes('ccwiki_') || content.includes('briefed')) {
|
|
304
308
|
fs.unlinkSync(claudeMdPath);
|
|
305
|
-
cleaned.push('~/.claude/CLAUDE.md (had old naming)');
|
|
309
|
+
cleaned.push('Removed ~/.claude/CLAUDE.md (had old naming)');
|
|
306
310
|
}
|
|
307
311
|
}
|
|
308
312
|
|
|
@@ -324,20 +328,27 @@ function cleanupLegacy() {
|
|
|
324
328
|
let settings = JSON.parse(content);
|
|
325
329
|
|
|
326
330
|
// Remove broken hooks (contain $HOME, gsd-, or old paths)
|
|
331
|
+
// Iterate ALL hook types, handle both old and new format
|
|
327
332
|
if (settings.hooks) {
|
|
328
|
-
const
|
|
329
|
-
for (const hookType of hookTypes) {
|
|
333
|
+
for (const hookType of Object.keys(settings.hooks)) {
|
|
330
334
|
if (Array.isArray(settings.hooks[hookType])) {
|
|
331
335
|
const originalLength = settings.hooks[hookType].length;
|
|
332
|
-
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter(entry => {
|
|
337
|
+
// Extract command paths from both old and new format
|
|
338
|
+
let hookPaths = '';
|
|
339
|
+
if (entry.command) {
|
|
340
|
+
// Old format: { type, command, ... }
|
|
341
|
+
hookPaths = entry.command;
|
|
342
|
+
} else if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
343
|
+
// New format: { matcher?, hooks: [{ type, command }] }
|
|
344
|
+
hookPaths = entry.hooks.map(h => h.command || h.path || '').join(' ');
|
|
345
|
+
} else if (typeof entry === 'string') {
|
|
346
|
+
hookPaths = entry;
|
|
347
|
+
}
|
|
348
|
+
if (hookPaths.includes('$HOME') ||
|
|
349
|
+
hookPaths.includes('gsd-') ||
|
|
350
|
+
hookPaths.includes('ccwiki') ||
|
|
351
|
+
hookPaths.includes('briefed')) {
|
|
341
352
|
return false;
|
|
342
353
|
}
|
|
343
354
|
return true;
|
|
@@ -359,13 +370,13 @@ function cleanupLegacy() {
|
|
|
359
370
|
|
|
360
371
|
if (modified) {
|
|
361
372
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
362
|
-
cleaned.push('~/.claude/settings.local.json (
|
|
373
|
+
cleaned.push('Cleaned ~/.claude/settings.local.json (old tools and broken hooks)');
|
|
363
374
|
}
|
|
364
375
|
} catch (jsonErr) {
|
|
365
376
|
// If JSON parsing fails, just do string replacement
|
|
366
377
|
if (modified) {
|
|
367
378
|
fs.writeFileSync(settingsPath, content);
|
|
368
|
-
cleaned.push('~/.claude/settings.local.json (
|
|
379
|
+
cleaned.push('Cleaned ~/.claude/settings.local.json (old tools)');
|
|
369
380
|
}
|
|
370
381
|
}
|
|
371
382
|
} catch (e) { /* ignore */ }
|
|
@@ -379,7 +390,7 @@ function cleanupLegacy() {
|
|
|
379
390
|
// Remove old gsd-* hook files
|
|
380
391
|
if (file.startsWith('gsd-') || file.includes('ccwiki') || file.includes('briefed')) {
|
|
381
392
|
removeFile(path.join(hooksDir, file));
|
|
382
|
-
cleaned.push(
|
|
393
|
+
cleaned.push(`Removed ~/.claude/hooks/${file}`);
|
|
383
394
|
}
|
|
384
395
|
}
|
|
385
396
|
}
|
|
@@ -424,7 +435,7 @@ function cleanupLegacy() {
|
|
|
424
435
|
|
|
425
436
|
if (modified) {
|
|
426
437
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
427
|
-
cleaned.push('~/.claude/config.json (
|
|
438
|
+
cleaned.push('Migrated ~/.claude/config.json (to global binary)');
|
|
428
439
|
}
|
|
429
440
|
} catch (e) { /* ignore */ }
|
|
430
441
|
}
|
|
@@ -470,7 +481,7 @@ function cleanupLegacy() {
|
|
|
470
481
|
|
|
471
482
|
if (modified) {
|
|
472
483
|
fs.writeFileSync(desktopConfigPath, JSON.stringify(config, null, 2));
|
|
473
|
-
cleaned.push('~/.claude/claude_desktop_config.json (
|
|
484
|
+
cleaned.push('Migrated ~/.claude/claude_desktop_config.json (to global binary)');
|
|
474
485
|
}
|
|
475
486
|
} catch (e) { /* ignore */ }
|
|
476
487
|
}
|
|
@@ -513,7 +524,7 @@ function cleanupLegacy() {
|
|
|
513
524
|
|
|
514
525
|
for (const dir of dirsToRemove) {
|
|
515
526
|
if (removeDirRecursive(dir)) {
|
|
516
|
-
cleaned.push(dir.replace(os.homedir(), '~'));
|
|
527
|
+
cleaned.push('Removed ' + dir.replace(os.homedir(), '~'));
|
|
517
528
|
}
|
|
518
529
|
}
|
|
519
530
|
|
|
@@ -543,7 +554,7 @@ function cleanupLegacy() {
|
|
|
543
554
|
|
|
544
555
|
for (const file of filesToRemove) {
|
|
545
556
|
if (removeFile(file)) {
|
|
546
|
-
cleaned.push(file.replace(os.homedir(), '~'));
|
|
557
|
+
cleaned.push('Removed ' + file.replace(os.homedir(), '~'));
|
|
547
558
|
}
|
|
548
559
|
}
|
|
549
560
|
|
|
@@ -565,7 +576,7 @@ function cleanupLegacy() {
|
|
|
565
576
|
try {
|
|
566
577
|
// Reset to empty object so Merlin auto-detects from git remote
|
|
567
578
|
fs.writeFileSync(merlinConfigPath, '{}');
|
|
568
|
-
cleaned.push('~/.merlin/config.json (
|
|
579
|
+
cleaned.push('Reset ~/.merlin/config.json (for auto-detect)');
|
|
569
580
|
} catch (e) { /* ignore */ }
|
|
570
581
|
}
|
|
571
582
|
|
|
@@ -586,7 +597,7 @@ function cleanupLegacy() {
|
|
|
586
597
|
claudeJson.mcpServers.merlin.command = 'merlin-brain';
|
|
587
598
|
delete claudeJson.mcpServers.merlin.args;
|
|
588
599
|
modified = true;
|
|
589
|
-
cleaned.push('~/.claude.json (
|
|
600
|
+
cleaned.push('Migrated ~/.claude.json (to global binary)');
|
|
590
601
|
}
|
|
591
602
|
}
|
|
592
603
|
|
|
@@ -594,7 +605,7 @@ function cleanupLegacy() {
|
|
|
594
605
|
if (claudeJson.mcpServers?.briefed) {
|
|
595
606
|
delete claudeJson.mcpServers.briefed;
|
|
596
607
|
modified = true;
|
|
597
|
-
cleaned.push('
|
|
608
|
+
cleaned.push('Removed deprecated briefed server from ~/.claude.json');
|
|
598
609
|
}
|
|
599
610
|
|
|
600
611
|
if (modified) {
|
|
@@ -622,7 +633,7 @@ function cleanupLegacy() {
|
|
|
622
633
|
const pkgJsonPath2 = path.join(entryPath, 'node_modules', 'create-merlin-pro', 'package.json');
|
|
623
634
|
if (fs.existsSync(pkgJsonPath) || fs.existsSync(pkgJsonPath2)) {
|
|
624
635
|
removeDirRecursive(entryPath);
|
|
625
|
-
cleaned.push('~/.npm/_npx/' + entry.name + ' (old npx cache)');
|
|
636
|
+
cleaned.push('Removed ~/.npm/_npx/' + entry.name + ' (old npx cache)');
|
|
626
637
|
}
|
|
627
638
|
}
|
|
628
639
|
}
|
|
@@ -639,12 +650,12 @@ function cleanupLegacy() {
|
|
|
639
650
|
// Remove old gsd-*.md and ccwiki-*.md agents
|
|
640
651
|
if ((file.startsWith('gsd-') || file.startsWith('ccwiki-')) && file.endsWith('.md')) {
|
|
641
652
|
removeFile(path.join(agentsDir, file));
|
|
642
|
-
cleaned.push(
|
|
653
|
+
cleaned.push(`Removed ~/.claude/agents/${file}`);
|
|
643
654
|
}
|
|
644
655
|
// Remove backup files in agents
|
|
645
656
|
if (file.endsWith('.backup') || file.endsWith('.bak') || file.endsWith('.old') || file.endsWith('~')) {
|
|
646
657
|
removeFile(path.join(agentsDir, file));
|
|
647
|
-
cleaned.push(
|
|
658
|
+
cleaned.push(`Removed ~/.claude/agents/${file}`);
|
|
648
659
|
}
|
|
649
660
|
}
|
|
650
661
|
}
|
|
@@ -709,7 +720,7 @@ function cleanupLegacy() {
|
|
|
709
720
|
|
|
710
721
|
if (modified) {
|
|
711
722
|
fs.writeFileSync(rcFile, content);
|
|
712
|
-
cleaned.push(rcFile.replace(os.homedir(), '~') + ' (removed old integrations)');
|
|
723
|
+
cleaned.push('Updated ' + rcFile.replace(os.homedir(), '~') + ' (removed old integrations)');
|
|
713
724
|
}
|
|
714
725
|
}
|
|
715
726
|
}
|
|
@@ -734,7 +745,7 @@ async function install() {
|
|
|
734
745
|
const cleaned = cleanupLegacy();
|
|
735
746
|
if (cleaned.length > 0) {
|
|
736
747
|
for (const item of cleaned) {
|
|
737
|
-
logSuccess(
|
|
748
|
+
logSuccess(item);
|
|
738
749
|
}
|
|
739
750
|
} else {
|
|
740
751
|
logSuccess('No legacy artifacts found');
|
|
@@ -890,60 +901,83 @@ async function install() {
|
|
|
890
901
|
}
|
|
891
902
|
settings.hooks = settings.hooks || {};
|
|
892
903
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════
|
|
905
|
+
// MIGRATE old-format hooks to new format (matcher + hooks array)
|
|
906
|
+
// Old: { type, command, matcher?, prompt? }
|
|
907
|
+
// New: { matcher?, hooks: [{ type, command, prompt? }] }
|
|
908
|
+
// ═══════════════════════════════════════════════════════════════
|
|
909
|
+
for (const [eventName, entries] of Object.entries(settings.hooks)) {
|
|
910
|
+
if (!Array.isArray(entries)) continue;
|
|
911
|
+
settings.hooks[eventName] = entries.map(entry => {
|
|
912
|
+
// Already in new format (has hooks array)
|
|
913
|
+
if (entry.hooks && Array.isArray(entry.hooks)) return entry;
|
|
914
|
+
// Old format → convert to new format
|
|
915
|
+
const handler = {};
|
|
916
|
+
if (entry.type) handler.type = entry.type;
|
|
917
|
+
if (entry.command) handler.command = entry.command;
|
|
918
|
+
if (entry.prompt) handler.prompt = entry.prompt;
|
|
919
|
+
if (entry.timeout !== undefined) handler.timeout = entry.timeout;
|
|
920
|
+
if (entry.async !== undefined) handler.async = entry.async;
|
|
921
|
+
if (entry.statusMessage) handler.statusMessage = entry.statusMessage;
|
|
922
|
+
const newEntry = {};
|
|
923
|
+
if (entry.matcher) newEntry.matcher = entry.matcher;
|
|
924
|
+
newEntry.hooks = [handler];
|
|
925
|
+
return newEntry;
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Helper: check if a command hook already exists in an array (new format)
|
|
930
|
+
const addHookIfMissing = (hookArray, matcherGroup) => {
|
|
931
|
+
const cmd = matcherGroup.hooks?.[0]?.command;
|
|
932
|
+
if (!cmd) { hookArray.push(matcherGroup); return hookArray; }
|
|
933
|
+
const exists = hookArray.some(entry =>
|
|
934
|
+
entry.hooks?.some(h => h.command === cmd)
|
|
935
|
+
);
|
|
936
|
+
if (!exists) hookArray.push(matcherGroup);
|
|
896
937
|
return hookArray;
|
|
897
938
|
};
|
|
898
939
|
|
|
899
|
-
// SessionStart
|
|
940
|
+
// SessionStart hooks
|
|
900
941
|
settings.hooks.SessionStart = settings.hooks.SessionStart || [];
|
|
901
942
|
addHookIfMissing(settings.hooks.SessionStart, {
|
|
902
|
-
type: 'command',
|
|
903
|
-
command: 'bash ~/.claude/hooks/session-start.sh'
|
|
943
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-start.sh' }]
|
|
904
944
|
});
|
|
905
945
|
|
|
906
946
|
// SessionStart hook: Agent sync (background, runs at most once/hour)
|
|
907
947
|
addHookIfMissing(settings.hooks.SessionStart, {
|
|
908
|
-
type: 'command',
|
|
909
|
-
command: 'bash ~/.claude/hooks/agent-sync.sh'
|
|
948
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/agent-sync.sh' }]
|
|
910
949
|
});
|
|
911
950
|
|
|
912
951
|
// PreToolUse hook (Edit/Write only)
|
|
913
952
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
914
953
|
addHookIfMissing(settings.hooks.PreToolUse, {
|
|
915
|
-
|
|
916
|
-
command: 'bash ~/.claude/hooks/pre-edit-sights-check.sh'
|
|
917
|
-
matcher: 'Edit|Write'
|
|
954
|
+
matcher: 'Edit|Write',
|
|
955
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/pre-edit-sights-check.sh' }]
|
|
918
956
|
});
|
|
919
957
|
|
|
920
958
|
// PostToolUse hook (Edit/Write only)
|
|
921
959
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
|
|
922
960
|
addHookIfMissing(settings.hooks.PostToolUse, {
|
|
923
|
-
|
|
924
|
-
command: 'bash ~/.claude/hooks/post-edit-logger.sh'
|
|
925
|
-
matcher: 'Edit|Write'
|
|
961
|
+
matcher: 'Edit|Write',
|
|
962
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/post-edit-logger.sh' }]
|
|
926
963
|
});
|
|
927
964
|
|
|
928
965
|
// Stop hook (session end)
|
|
929
966
|
settings.hooks.Stop = settings.hooks.Stop || [];
|
|
930
967
|
addHookIfMissing(settings.hooks.Stop, {
|
|
931
|
-
type: 'command',
|
|
932
|
-
command: 'bash ~/.claude/hooks/session-end.sh'
|
|
968
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-end.sh' }]
|
|
933
969
|
});
|
|
934
970
|
|
|
935
971
|
// TeammateIdle hook (Agent Teams quality gate — only fires when Teams active)
|
|
936
972
|
settings.hooks.TeammateIdle = settings.hooks.TeammateIdle || [];
|
|
937
973
|
addHookIfMissing(settings.hooks.TeammateIdle, {
|
|
938
|
-
type: 'command',
|
|
939
|
-
command: 'bash ~/.claude/hooks/teammate-idle-verify.sh'
|
|
974
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/teammate-idle-verify.sh' }]
|
|
940
975
|
});
|
|
941
976
|
|
|
942
977
|
// SubagentStart hook (Agent Teams context injection — only fires when Teams active)
|
|
943
978
|
settings.hooks.SubagentStart = settings.hooks.SubagentStart || [];
|
|
944
979
|
addHookIfMissing(settings.hooks.SubagentStart, {
|
|
945
|
-
type: 'command',
|
|
946
|
-
command: 'bash ~/.claude/hooks/subagent-context.sh'
|
|
980
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/subagent-context.sh' }]
|
|
947
981
|
});
|
|
948
982
|
|
|
949
983
|
// --- Prompt-based hooks (LLM evaluates .md content) ---
|
|
@@ -951,11 +985,15 @@ async function install() {
|
|
|
951
985
|
// first (identified by containing 'merlin' or 'Merlin' in their prompt text),
|
|
952
986
|
// then re-add fresh ones. This prevents duplicate hooks across version upgrades.
|
|
953
987
|
const removeMerlinPromptHooks = (hookArray) => {
|
|
954
|
-
return hookArray.filter(
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
988
|
+
return hookArray.filter(entry => {
|
|
989
|
+
if (!entry.hooks || !Array.isArray(entry.hooks)) return true;
|
|
990
|
+
// Remove entry if ALL its hooks are Merlin prompt hooks
|
|
991
|
+
const allMerlinPrompts = entry.hooks.every(h => {
|
|
992
|
+
if (h.type !== 'prompt') return false;
|
|
993
|
+
const prompt = (h.prompt || '').toLowerCase();
|
|
994
|
+
return prompt.includes('merlin') || prompt.includes('sights');
|
|
995
|
+
});
|
|
996
|
+
return !allMerlinPrompts;
|
|
959
997
|
});
|
|
960
998
|
};
|
|
961
999
|
|
|
@@ -964,34 +1002,31 @@ async function install() {
|
|
|
964
1002
|
settings.hooks.Stop = removeMerlinPromptHooks(settings.hooks.Stop);
|
|
965
1003
|
settings.hooks.Notification = settings.hooks.Notification || [];
|
|
966
1004
|
settings.hooks.Notification = removeMerlinPromptHooks(settings.hooks.Notification);
|
|
1005
|
+
settings.hooks.TaskCompleted = settings.hooks.TaskCompleted || [];
|
|
1006
|
+
settings.hooks.TaskCompleted = removeMerlinPromptHooks(settings.hooks.TaskCompleted);
|
|
967
1007
|
|
|
968
|
-
// Now add fresh prompt hooks
|
|
969
|
-
//
|
|
970
|
-
//
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1008
|
+
// Now add fresh prompt/command hooks in new format
|
|
1009
|
+
// NOTE: SessionStart does NOT support prompt hooks (only command hooks).
|
|
1010
|
+
// The Merlin boot sequence is handled via CLAUDE.md instructions instead.
|
|
1011
|
+
// We use a command hook to inject additionalContext at session start.
|
|
1012
|
+
addHookIfMissing(settings.hooks.SessionStart, {
|
|
1013
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-start-context.sh' }]
|
|
974
1014
|
});
|
|
975
1015
|
|
|
976
|
-
// Prompt-based PreToolUse hook:
|
|
977
|
-
// This is the Sights guarantee — no edit without context
|
|
1016
|
+
// Prompt-based PreToolUse hook: evaluate Sights check before edits
|
|
978
1017
|
settings.hooks.PreToolUse.push({
|
|
979
|
-
|
|
980
|
-
prompt: fs.readFileSync(path.join(HOOKS_DIR, 'pre-edit-sights-enforce.md'), 'utf8')
|
|
981
|
-
matcher: 'Edit|Write'
|
|
1018
|
+
matcher: 'Edit|Write',
|
|
1019
|
+
hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'pre-edit-sights-enforce.md'), 'utf8') }]
|
|
982
1020
|
});
|
|
983
1021
|
|
|
984
|
-
// Prompt-based Stop hook:
|
|
1022
|
+
// Prompt-based Stop hook: evaluate whether stopping is appropriate
|
|
985
1023
|
settings.hooks.Stop.push({
|
|
986
|
-
type: 'prompt',
|
|
987
|
-
prompt: fs.readFileSync(path.join(HOOKS_DIR, 'stop-check.md'), 'utf8')
|
|
1024
|
+
hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'stop-check.md'), 'utf8') }]
|
|
988
1025
|
});
|
|
989
1026
|
|
|
990
|
-
//
|
|
991
|
-
settings.hooks.
|
|
992
|
-
type: 'prompt',
|
|
993
|
-
prompt: fs.readFileSync(path.join(HOOKS_DIR, 'task-completed-verify.md'), 'utf8'),
|
|
994
|
-
matcher: 'task_completed'
|
|
1027
|
+
// Task completion verification hook (using dedicated TaskCompleted event)
|
|
1028
|
+
settings.hooks.TaskCompleted.push({
|
|
1029
|
+
hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'task-completed-verify.md'), 'utf8') }]
|
|
995
1030
|
});
|
|
996
1031
|
|
|
997
1032
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
@@ -1031,11 +1066,15 @@ async function install() {
|
|
|
1031
1066
|
|
|
1032
1067
|
let apiKey = existingApiKey;
|
|
1033
1068
|
if (existingApiKey) {
|
|
1034
|
-
|
|
1069
|
+
const masked = existingApiKey.slice(0, 8) + '...' + existingApiKey.slice(-4);
|
|
1070
|
+
console.log(` Existing API key found: ${colors.cyan}${masked}${colors.reset}`);
|
|
1035
1071
|
} else {
|
|
1036
|
-
console.log(`Merlin Sights provides instant codebase context.`);
|
|
1037
|
-
console.log(`Get your API key at: ${colors.cyan}https://merlin.build${colors.reset}`);
|
|
1038
|
-
|
|
1072
|
+
console.log(` Merlin Sights provides instant codebase context.`);
|
|
1073
|
+
console.log(` Get your API key at: ${colors.cyan}https://merlin.build${colors.reset}`);
|
|
1074
|
+
}
|
|
1075
|
+
const inputKey = await promptApiKey(!!existingApiKey);
|
|
1076
|
+
if (inputKey) {
|
|
1077
|
+
apiKey = inputKey;
|
|
1039
1078
|
}
|
|
1040
1079
|
|
|
1041
1080
|
if (apiKey) {
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
# Merlin Hook: Agent Sync (SessionStart, background)
|
|
3
3
|
# Checks installed agents freshness and updates from cloud. Max once/hour.
|
|
4
4
|
set -euo pipefail
|
|
5
|
-
trap 'exit 0' ERR
|
|
5
|
+
trap 'echo "{}"; exit 0' ERR
|
|
6
6
|
|
|
7
7
|
AGENTS_DIR="${HOME}/.claude/agents"
|
|
8
8
|
MERLIN_DIR="${HOME}/.claude/merlin"
|
|
9
9
|
LAST_SYNC="${MERLIN_DIR}/.last-agent-sync"
|
|
10
10
|
API_URL="${MERLIN_API_URL:-https://api.merlin.build}"
|
|
11
11
|
|
|
12
|
-
[ -d "${AGENTS_DIR}" ] || exit 0
|
|
12
|
+
[ -d "${AGENTS_DIR}" ] || { echo '{}'; exit 0; }
|
|
13
13
|
|
|
14
14
|
# Skip if synced within the last hour
|
|
15
15
|
if [ -f "${LAST_SYNC}" ]; then
|
|
16
16
|
last=$(cat "${LAST_SYNC}" 2>/dev/null || echo "0")
|
|
17
|
-
[ $(($(date +%s) - last)) -lt 3600 ] && exit 0
|
|
17
|
+
[ $(($(date +%s) - last)) -lt 3600 ] && { echo '{}'; exit 0; }
|
|
18
18
|
fi
|
|
19
19
|
|
|
20
20
|
sync_agents() {
|
|
@@ -41,4 +41,7 @@ sync_agents() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
sync_agents &
|
|
44
|
+
|
|
45
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
46
|
+
echo '{}'
|
|
44
47
|
exit 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Always exits 0 — never blocks Claude Code.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -30,4 +30,6 @@ fi
|
|
|
30
30
|
# Log file change event
|
|
31
31
|
log_event "file_changed" "$(printf '{"file":"%s","tool":"%s"}' "${file_path:-unknown}" "${tool_name:-unknown}")"
|
|
32
32
|
|
|
33
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
34
|
+
echo '{}'
|
|
33
35
|
exit 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Advisory only — always exits 0, never blocks edits.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -35,4 +35,6 @@ fi
|
|
|
35
35
|
# Log the pre-edit event
|
|
36
36
|
log_event "pre_edit" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
|
|
37
37
|
|
|
38
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
39
|
+
echo '{}'
|
|
38
40
|
exit 0
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
You are evaluating whether a file edit should proceed. Context: $ARGUMENTS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Check: Has the assistant recently called merlin_get_context, merlin_find_files, or merlin_search for this file's area in the current conversation? Look at recent tool calls in the transcript.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
If
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
3. Check if similar code already exists (don't duplicate)
|
|
10
|
-
4. THEN proceed with the edit
|
|
5
|
+
Rules:
|
|
6
|
+
- If Sights was consulted recently (within the last few messages) for this area: allow the edit
|
|
7
|
+
- If this is a minor/trivial edit (formatting, typo fix, small change): allow the edit
|
|
8
|
+
- If this is a significant edit and Sights was NOT consulted: suggest checking first
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
- Naming conventions — match existing patterns
|
|
16
|
-
- Testing requirements — note if tests are needed
|
|
17
|
-
|
|
18
|
-
## Required: Show Your Work
|
|
19
|
-
After the Sights check, show a brief line so the user sees Merlin is involved:
|
|
20
|
-
```
|
|
21
|
-
Merlin | Checked [area] | [what you found or "clear to edit"]
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Never Skip This
|
|
25
|
-
Every edit should be informed by codebase knowledge. This is what makes Merlin sessions better than raw Claude sessions. The user chose Merlin for a reason — deliver on it.
|
|
10
|
+
Respond with ONLY valid JSON, no other text:
|
|
11
|
+
- Allow the edit: {"ok": true}
|
|
12
|
+
- Suggest Sights check first: {"ok": false, "reason": "Call merlin_get_context before editing this file to check for existing patterns and conventions"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Always exits 0 — never blocks Claude Code shutdown.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -24,4 +24,6 @@ if command -v merlin >/dev/null 2>&1; then
|
|
|
24
24
|
merlin save-checkpoint "Session ended" >/dev/null 2>&1 &
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
28
|
+
echo '{}'
|
|
27
29
|
exit 0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: SessionStart (context injection)
|
|
4
|
+
# Injects Merlin identity context into the session via additionalContext.
|
|
5
|
+
# Uses the SessionStart command hook JSON output format.
|
|
6
|
+
# Always exits 0 — never blocks Claude Code startup.
|
|
7
|
+
#
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
trap 'echo "{}"; exit 0' ERR
|
|
10
|
+
|
|
11
|
+
# Output additionalContext JSON for Claude to see at session start.
|
|
12
|
+
# This reminds Claude it's a Merlin-powered session.
|
|
13
|
+
# Full boot instructions are in CLAUDE.md — this is a lightweight nudge.
|
|
14
|
+
cat <<'CONTEXT_JSON'
|
|
15
|
+
{
|
|
16
|
+
"hookSpecificOutput": {
|
|
17
|
+
"hookEventName": "SessionStart",
|
|
18
|
+
"additionalContext": "You are a Merlin-powered session. Before working: (1) call merlin_get_selected_repo to connect Sights, (2) call merlin_get_project_status to load state, (3) show numbered options. Check Sights before every edit. Route complex tasks to specialists via /merlin:route. Save checkpoints before stopping."
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
CONTEXT_JSON
|
|
22
|
+
|
|
23
|
+
exit 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Always exits 0 — never blocks Claude Code startup.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -28,4 +28,6 @@ if command -v merlin >/dev/null 2>&1; then
|
|
|
28
28
|
record_sights_call
|
|
29
29
|
fi
|
|
30
30
|
|
|
31
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
32
|
+
echo '{}'
|
|
31
33
|
exit 0
|
|
@@ -1,36 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
You are evaluating whether Claude should stop working. Analyze the conversation context: $ARGUMENTS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Check these criteria:
|
|
4
|
+
1. Did the user's most recent request get fully addressed?
|
|
5
|
+
2. Are there any obvious errors or broken code that should be fixed before stopping?
|
|
6
|
+
3. Did Claude mention follow-up tasks it intended to complete but hasn't done yet?
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Stage and commit with a clear message
|
|
8
|
-
- Run `merlin_run_verification` before committing
|
|
8
|
+
IMPORTANT: This is NOT about saving checkpoints or showing summaries — those are handled elsewhere.
|
|
9
|
+
Only evaluate whether the core work is done.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
- What's left to do
|
|
14
|
-
- Any blockers or decisions made
|
|
15
|
-
This lets the next session (or another agent) pick up exactly where you left off.
|
|
16
|
-
|
|
17
|
-
## 3. Session Summary
|
|
18
|
-
Show the user a brief summary:
|
|
19
|
-
```
|
|
20
|
-
Merlin | Session Complete
|
|
21
|
-
- [X] files changed
|
|
22
|
-
- [Y] Sights queries made
|
|
23
|
-
- [Z] tasks completed
|
|
24
|
-
- Checkpoint saved: [yes/no]
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## 4. Next Steps
|
|
28
|
-
Always end with what should happen next:
|
|
29
|
-
```
|
|
30
|
-
Next session:
|
|
31
|
-
[1] Continue with [next task]
|
|
32
|
-
[2] Review what was built
|
|
33
|
-
[3] [other relevant option]
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Do NOT stop without saving checkpoint. The user trusts Merlin to maintain continuity across sessions.
|
|
11
|
+
Respond with ONLY valid JSON, no other text:
|
|
12
|
+
- If stopping is appropriate: {"ok": true}
|
|
13
|
+
- If Claude should continue: {"ok": false, "reason": "brief explanation of what's unfinished"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Advisory only — always exits 0, never blocks subagent startup.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -28,9 +28,21 @@ fi
|
|
|
28
28
|
# Log subagent start event
|
|
29
29
|
log_event "subagent_start" "$(printf '{"task":"%s"}' "${task_desc:-unknown}")"
|
|
30
30
|
|
|
31
|
-
# If Merlin CLI available, fetch
|
|
31
|
+
# If Merlin CLI available, fetch context for the task and output as JSON
|
|
32
32
|
if [ -n "$task_desc" ] && command -v merlin >/dev/null 2>&1; then
|
|
33
|
-
merlin context "$task_desc" 2>/dev/null || true
|
|
33
|
+
context_text=$(merlin context "$task_desc" 2>/dev/null || true)
|
|
34
|
+
if [ -n "$context_text" ]; then
|
|
35
|
+
# Output as proper JSON additionalContext for the subagent
|
|
36
|
+
# Use jq if available, otherwise output empty JSON (context is best-effort)
|
|
37
|
+
if command -v jq >/dev/null 2>&1; then
|
|
38
|
+
jq -n --arg ctx "$context_text" '{hookSpecificOutput:{hookEventName:"SubagentStart",additionalContext:$ctx}}'
|
|
39
|
+
else
|
|
40
|
+
echo '{}'
|
|
41
|
+
fi
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
34
44
|
fi
|
|
35
45
|
|
|
46
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
47
|
+
echo '{}'
|
|
36
48
|
exit 0
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
You are evaluating whether a task should be marked as completed. Context: $ARGUMENTS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Check these criteria:
|
|
4
|
+
1. Was the task's stated objective accomplished based on the conversation?
|
|
5
|
+
2. Were there any errors or test failures mentioned that haven't been resolved?
|
|
6
|
+
3. Is the implementation reasonably complete (not a half-finished skeleton)?
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Be lenient — don't block completion for minor issues like missing docs or style nits.
|
|
9
|
+
Only block if the core objective clearly wasn't met or there are unresolved errors.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
4. **Documentation**: If the task added new APIs or features, are they documented?
|
|
12
|
-
|
|
13
|
-
If all verifications pass, allow task completion.
|
|
14
|
-
If issues are found, list them and suggest fixes before completing.
|
|
11
|
+
Respond with ONLY valid JSON, no other text:
|
|
12
|
+
- Task is complete: {"ok": true}
|
|
13
|
+
- Task is not complete: {"ok": false, "reason": "brief explanation of what's unfinished"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Informational only — always exits 0 for v1.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -43,4 +43,6 @@ if [ -f "tsconfig.json" ] && command -v npx >/dev/null 2>&1; then
|
|
|
43
43
|
fi
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
47
|
+
echo '{}'
|
|
46
48
|
exit 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Advisory only — always exits 0, never blocks teammates.
|
|
6
6
|
#
|
|
7
7
|
set -euo pipefail
|
|
8
|
-
trap 'exit 0' ERR
|
|
8
|
+
trap 'echo "{}"; exit 0' ERR
|
|
9
9
|
|
|
10
10
|
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
@@ -38,4 +38,6 @@ if command -v npm >/dev/null 2>&1; then
|
|
|
38
38
|
npm run build --if-present >/dev/null 2>&1 || true
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
42
|
+
echo '{}'
|
|
41
43
|
exit 0
|
package/package.json
CHANGED