create-merlin-brain 3.4.7 → 3.4.8

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.
Files changed (2) hide show
  1. package/bin/install.cjs +113 -76
  2. 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(`\n${colors.cyan}?${colors.reset} Merlin Sights API key (press Enter to skip): `, (answer) => {
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 hookTypes = ['PreToolUse', 'PostToolUse', 'Stop', 'Start'];
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(hook => {
333
- // Remove hooks with $HOME (doesn't work on Windows)
334
- // Remove hooks with gsd- (old Get Shit Done)
335
- // Remove hooks referencing non-existent files
336
- const hookPath = typeof hook === 'string' ? hook : hook?.command || hook?.path || '';
337
- if (hookPath.includes('$HOME') ||
338
- hookPath.includes('gsd-') ||
339
- hookPath.includes('ccwiki') ||
340
- hookPath.includes('briefed')) {
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 (removed old tools and broken hooks)');
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 (removed old tools)');
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(`~/.claude/hooks/${file}`);
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 (migrated to global binary)');
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 (migrated to global binary)');
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 (reset for auto-detect)');
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 (migrated to global binary)');
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('~/.claude.json (removed deprecated briefed server)');
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(`~/.claude/agents/${file}`);
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(`~/.claude/agents/${file}`);
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(`Removed ${item}`);
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
- const addHookIfMissing = (hookArray, hookConfig) => {
894
- const exists = hookArray.some(h => h.command === hookConfig.command);
895
- if (!exists) hookArray.push(hookConfig);
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 hook
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
- type: 'command',
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
- type: 'command',
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(h => {
955
- if (h.type !== 'prompt') return true; // keep non-prompt hooks
956
- const prompt = (h.prompt || '').toLowerCase();
957
- // Merlin prompt hooks contain these markers
958
- return !(prompt.includes('merlin') || prompt.includes('sights'));
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,29 @@ 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
1008
+ // Now add fresh prompt hooks in new format
969
1009
  // Prompt-based SessionStart hook: FULL Merlin boot sequence
970
- // This is the key hook that makes every session feel like Loop
971
1010
  settings.hooks.SessionStart.push({
972
- type: 'prompt',
973
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'session-start-boot.md'), 'utf8')
1011
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'session-start-boot.md'), 'utf8') }]
974
1012
  });
975
1013
 
976
1014
  // Prompt-based PreToolUse hook: enforce Sights check before edits
977
- // This is the Sights guarantee — no edit without context
978
1015
  settings.hooks.PreToolUse.push({
979
- type: 'prompt',
980
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'pre-edit-sights-enforce.md'), 'utf8'),
981
- matcher: 'Edit|Write'
1016
+ matcher: 'Edit|Write',
1017
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'pre-edit-sights-enforce.md'), 'utf8') }]
982
1018
  });
983
1019
 
984
1020
  // Prompt-based Stop hook: quality gate before session ends
985
1021
  settings.hooks.Stop.push({
986
- type: 'prompt',
987
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'stop-check.md'), 'utf8')
1022
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'stop-check.md'), 'utf8') }]
988
1023
  });
989
1024
 
990
- // Prompt-based Notification hook: verify task completion quality
991
- settings.hooks.Notification.push({
992
- type: 'prompt',
993
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'task-completed-verify.md'), 'utf8'),
994
- matcher: 'task_completed'
1025
+ // Task completion verification hook (using dedicated TaskCompleted event)
1026
+ settings.hooks.TaskCompleted.push({
1027
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'task-completed-verify.md'), 'utf8') }]
995
1028
  });
996
1029
 
997
1030
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -1031,11 +1064,15 @@ async function install() {
1031
1064
 
1032
1065
  let apiKey = existingApiKey;
1033
1066
  if (existingApiKey) {
1034
- logSuccess(`Sights API key already configured (skipping prompt)`);
1067
+ const masked = existingApiKey.slice(0, 8) + '...' + existingApiKey.slice(-4);
1068
+ console.log(` Existing API key found: ${colors.cyan}${masked}${colors.reset}`);
1035
1069
  } 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
- apiKey = await promptApiKey();
1070
+ console.log(` Merlin Sights provides instant codebase context.`);
1071
+ console.log(` Get your API key at: ${colors.cyan}https://merlin.build${colors.reset}`);
1072
+ }
1073
+ const inputKey = await promptApiKey(!!existingApiKey);
1074
+ if (inputKey) {
1075
+ apiKey = inputKey;
1039
1076
  }
1040
1077
 
1041
1078
  if (apiKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-merlin-brain",
3
- "version": "3.4.7",
3
+ "version": "3.4.8",
4
4
  "description": "Merlin - The Ultimate AI Brain for Claude Code. One install: workflows, agents, loop, and Sights MCP server.",
5
5
  "type": "module",
6
6
  "main": "./dist/server/index.js",