create-merlin-brain 3.4.6 → 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.
@@ -6,7 +6,7 @@
6
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
8
  import { z } from 'zod';
9
- import { getClient } from './api/client.js';
9
+ import { getClient, AuthError } from './api/client.js';
10
10
  import { detectRepository } from './utils/git.js';
11
11
  import { readMerlinManifest, extractSightId, writeMerlinManifest, buildManifest, injectSightIntoClaudeMd, MERLIN_SIGHTS_BASE } from './utils/merlin-manifest.js';
12
12
  import { existsSync, readFileSync } from 'fs';
@@ -152,6 +152,33 @@ export function createServer() {
152
152
  }
153
153
  return false;
154
154
  }
155
+ // Helper to check if an error is an auth failure and build a graceful response
156
+ function isAuthError(error) {
157
+ return error instanceof AuthError;
158
+ }
159
+ function buildAuthErrorResponse() {
160
+ const apiKey = process.env.MERLIN_API_KEY || '';
161
+ const maskedKey = apiKey.length > 10
162
+ ? `${apiKey.slice(0, 8)}...${apiKey.slice(-4)} (${apiKey.length} chars)`
163
+ : apiKey ? '(key too short)' : '(no key set)';
164
+ let msg = `⚠️ **Authentication Failed**\n\n`;
165
+ msg += `Your Merlin API key is invalid, expired, or not configured.\n\n`;
166
+ msg += `**Current key:** ${maskedKey}\n\n`;
167
+ msg += `**To fix this:**\n\n`;
168
+ msg += `1. Get a new API key at: **https://merlin.build/settings**\n`;
169
+ msg += `2. Update your installation:\n`;
170
+ msg += ` \`\`\`\n npx create-merlin-brain@latest\n \`\`\`\n`;
171
+ msg += ` Or manually edit \`~/.claude/settings.json\` → \`mcpServers.merlin.env.MERLIN_API_KEY\`\n\n`;
172
+ msg += `3. Restart Claude Code after updating the key.\n\n`;
173
+ msg += `**Note:** Merlin still works without Sights — agents, workflows, and loop are fully local.\n`;
174
+ msg += `The API key is only needed for cloud features (codebase context, search, etc.).\n`;
175
+ return {
176
+ content: [{
177
+ type: 'text',
178
+ text: msg,
179
+ }],
180
+ };
181
+ }
155
182
  // Helper to build an AGGRESSIVE "repo selector" response
156
183
  // This is shown when there's any repo-related error - it MUST be impossible to miss
157
184
  async function buildRepoSelectorResponse(options = {}) {
@@ -342,6 +369,8 @@ export function createServer() {
342
369
  return { content: [{ type: 'text', text: brief }] };
343
370
  }
344
371
  catch (error) {
372
+ if (isAuthError(error))
373
+ return buildAuthErrorResponse();
345
374
  // Check if this is a "repo not found" type error - show the selector!
346
375
  if (isRepoNotFoundError(error)) {
347
376
  selectedRepoId = null;
@@ -531,6 +560,8 @@ export function createServer() {
531
560
  };
532
561
  }
533
562
  catch (error) {
563
+ if (isAuthError(error))
564
+ return buildAuthErrorResponse();
534
565
  // Check if this is a "repo not found" type error - show the selector!
535
566
  if (isRepoNotFoundError(error)) {
536
567
  // Clear the stale selected repo ID so user can select fresh
@@ -629,6 +660,8 @@ export function createServer() {
629
660
  return { content: [{ type: 'text', text: findResult }] };
630
661
  }
631
662
  catch (error) {
663
+ if (isAuthError(error))
664
+ return buildAuthErrorResponse();
632
665
  // Check if this is a "repo not found" type error - show the selector!
633
666
  if (isRepoNotFoundError(error)) {
634
667
  selectedRepoId = null;
@@ -722,6 +755,8 @@ export function createServer() {
722
755
  return { content: [{ type: 'text', text: result }] };
723
756
  }
724
757
  catch (error) {
758
+ if (isAuthError(error))
759
+ return buildAuthErrorResponse();
725
760
  // Check if this is a "repo not found" type error - show the selector!
726
761
  if (isRepoNotFoundError(error)) {
727
762
  selectedRepoId = null;
@@ -777,6 +812,8 @@ export function createServer() {
777
812
  return { content: [{ type: 'text', text: quickstart }] };
778
813
  }
779
814
  catch (error) {
815
+ if (isAuthError(error))
816
+ return buildAuthErrorResponse();
780
817
  // Check if this is a "repo not found" type error - show the selector!
781
818
  if (isRepoNotFoundError(error)) {
782
819
  selectedRepoId = null;
@@ -826,6 +863,8 @@ export function createServer() {
826
863
  return { content: [{ type: 'text', text: results }] };
827
864
  }
828
865
  catch (error) {
866
+ if (isAuthError(error))
867
+ return buildAuthErrorResponse();
829
868
  // Check if this is a "repo not found" type error - show the selector!
830
869
  if (isRepoNotFoundError(error)) {
831
870
  selectedRepoId = null;
@@ -899,6 +938,8 @@ export function createServer() {
899
938
  return { content: [{ type: 'text', text: result }] };
900
939
  }
901
940
  catch (error) {
941
+ if (isAuthError(error))
942
+ return buildAuthErrorResponse();
902
943
  return {
903
944
  content: [{
904
945
  type: 'text',
@@ -987,6 +1028,8 @@ export function createServer() {
987
1028
  };
988
1029
  }
989
1030
  catch (error) {
1031
+ if (isAuthError(error))
1032
+ return buildAuthErrorResponse();
990
1033
  return {
991
1034
  content: [{
992
1035
  type: 'text',
@@ -1005,56 +1048,31 @@ export function createServer() {
1005
1048
  const maskedKey = apiKey.length > 10
1006
1049
  ? `${apiKey.slice(0, 8)}...${apiKey.slice(-4)} (${apiKey.length} chars)`
1007
1050
  : apiKey ? '(key too short)' : '(no key)';
1008
- // Get repo root path - CRITICAL for file operations with paths containing spaces
1009
- const repoRoot = await getRepoRootPath();
1010
- if (selectedRepoId && selectedRepoName) {
1011
- // Build Merlin Sights dashboard URL
1012
- const sightsUrl = `https://merlin.build/sights/${selectedRepoId}`;
1013
- const stats = getStats();
1014
- let response = `**🔮 Merlin MCP v${VERSION}**\n\n`;
1015
- response += `**API Key:** ${maskedKey}\n`;
1016
- response += `**Connected to:** ${selectedRepoName}\n`;
1017
- response += `**GitHub:** ${selectedRepoUrl}\n`;
1018
- response += `**Merlin Sights:** ${sightsUrl}\n`;
1019
- if (repoRoot) {
1020
- response += `**Local path:** \`${repoRoot}\`\n`;
1021
- response += `\n**IMPORTANT for file operations:** Use this full absolute path with Glob tool.\n`;
1022
- response += `Example: \`Glob(pattern: "**/*.ts", path: "${repoRoot}")\`\n`;
1023
- }
1024
- response += `\nAll Merlin queries are using this repository.`;
1025
- // Add session stats if there have been queries
1026
- if (stats.queries > 0) {
1027
- const compactStats = getCompactStats();
1028
- if (compactStats) {
1029
- response += `\n\n${compactStats}`;
1030
- }
1031
- }
1032
- return {
1033
- content: [{
1034
- type: 'text',
1035
- text: response,
1036
- }],
1037
- };
1038
- }
1039
- // Check if auto-detect would work
1040
- const detected = await detectRepository();
1041
- if (detected) {
1042
- // Cache the root path
1043
- cachedRepoRootPath = detected.rootDir;
1044
- const repo = await client.findRepoByUrl(detected.url);
1045
- if (repo) {
1051
+ try {
1052
+ // Get repo root path - CRITICAL for file operations with paths containing spaces
1053
+ const repoRoot = await getRepoRootPath();
1054
+ if (selectedRepoId && selectedRepoName) {
1046
1055
  // Build Merlin Sights dashboard URL
1047
- const sightsUrl = `https://merlin.build/sights/${repo.id}`;
1056
+ const sightsUrl = `https://merlin.build/sights/${selectedRepoId}`;
1057
+ const stats = getStats();
1048
1058
  let response = `**🔮 Merlin MCP v${VERSION}**\n\n`;
1049
1059
  response += `**API Key:** ${maskedKey}\n`;
1050
- response += `Auto-detecting from git remote.\n\n`;
1051
- response += `**Detected:** ${repo.fullName || repo.name}\n`;
1052
- response += `**GitHub:** ${repo.url}\n`;
1060
+ response += `**Connected to:** ${selectedRepoName}\n`;
1061
+ response += `**GitHub:** ${selectedRepoUrl}\n`;
1053
1062
  response += `**Merlin Sights:** ${sightsUrl}\n`;
1054
- response += `**Local path:** \`${detected.rootDir}\`\n`;
1055
- response += `\n**IMPORTANT for file operations:** Use this full absolute path with Glob tool.\n`;
1056
- response += `Example: \`Glob(pattern: "**/*.ts", path: "${detected.rootDir}")\`\n`;
1057
- response += `\n**Tip:** Use \`merlin_select_repo\` to explicitly select a Sight.`;
1063
+ if (repoRoot) {
1064
+ response += `**Local path:** \`${repoRoot}\`\n`;
1065
+ response += `\n**IMPORTANT for file operations:** Use this full absolute path with Glob tool.\n`;
1066
+ response += `Example: \`Glob(pattern: "**/*.ts", path: "${repoRoot}")\`\n`;
1067
+ }
1068
+ response += `\nAll Merlin queries are using this repository.`;
1069
+ // Add session stats if there have been queries
1070
+ if (stats.queries > 0) {
1071
+ const compactStats = getCompactStats();
1072
+ if (compactStats) {
1073
+ response += `\n\n${compactStats}`;
1074
+ }
1075
+ }
1058
1076
  return {
1059
1077
  content: [{
1060
1078
  type: 'text',
@@ -1062,35 +1080,80 @@ export function createServer() {
1062
1080
  }],
1063
1081
  };
1064
1082
  }
1065
- // Fallback: Check for .merlin.json before giving up
1066
- const manifest = readMerlinManifest(detected.rootDir);
1067
- if (manifest) {
1068
- const manifestSightId = extractSightId(manifest.sight);
1069
- if (manifestSightId) {
1070
- selectedRepoId = manifestSightId;
1071
- selectedRepoUrl = `https://${manifest.repo}`;
1072
- selectedRepoName = manifest.repo;
1073
- repoIdCache.delete('auto');
1074
- console.error(`[merlin] Auto-selected from .merlin.json: ${manifest.repo}`);
1075
- const sightsUrl = `https://merlin.build/sights/${manifestSightId}`;
1076
- let mResponse = `**🔮 Merlin MCP v${VERSION}**\n\n`;
1077
- mResponse += `**API Key:** ${maskedKey}\n`;
1078
- mResponse += `🔮 **Auto-connected from .merlin.json**\n\n`;
1079
- mResponse += `**Repository:** ${manifest.repo}\n`;
1080
- mResponse += `**Merlin Sights:** ${sightsUrl}\n`;
1081
- mResponse += `**Local path:** \`${detected.rootDir}\`\n`;
1082
- mResponse += `\n**IMPORTANT for file operations:** Use this full absolute path with Glob tool.\n`;
1083
- mResponse += `Example: \`Glob(pattern: "**/*.ts", path: "${detected.rootDir}")\`\n`;
1083
+ // Check if auto-detect would work
1084
+ const detected = await detectRepository();
1085
+ if (detected) {
1086
+ // Cache the root path
1087
+ cachedRepoRootPath = detected.rootDir;
1088
+ const repo = await client.findRepoByUrl(detected.url);
1089
+ if (repo) {
1090
+ // Build Merlin Sights dashboard URL
1091
+ const sightsUrl = `https://merlin.build/sights/${repo.id}`;
1092
+ let response = `**🔮 Merlin MCP v${VERSION}**\n\n`;
1093
+ response += `**API Key:** ${maskedKey}\n`;
1094
+ response += `Auto-detecting from git remote.\n\n`;
1095
+ response += `**Detected:** ${repo.fullName || repo.name}\n`;
1096
+ response += `**GitHub:** ${repo.url}\n`;
1097
+ response += `**Merlin Sights:** ${sightsUrl}\n`;
1098
+ response += `**Local path:** \`${detected.rootDir}\`\n`;
1099
+ response += `\n**IMPORTANT for file operations:** Use this full absolute path with Glob tool.\n`;
1100
+ response += `Example: \`Glob(pattern: "**/*.ts", path: "${detected.rootDir}")\`\n`;
1101
+ response += `\n**Tip:** Use \`merlin_select_repo\` to explicitly select a Sight.`;
1084
1102
  return {
1085
1103
  content: [{
1086
1104
  type: 'text',
1087
- text: mResponse,
1105
+ text: response,
1088
1106
  }],
1089
1107
  };
1090
1108
  }
1109
+ // Fallback: Check for .merlin.json before giving up
1110
+ const manifest = readMerlinManifest(detected.rootDir);
1111
+ if (manifest) {
1112
+ const manifestSightId = extractSightId(manifest.sight);
1113
+ if (manifestSightId) {
1114
+ selectedRepoId = manifestSightId;
1115
+ selectedRepoUrl = `https://${manifest.repo}`;
1116
+ selectedRepoName = manifest.repo;
1117
+ repoIdCache.delete('auto');
1118
+ console.error(`[merlin] Auto-selected from .merlin.json: ${manifest.repo}`);
1119
+ const sightsUrl = `https://merlin.build/sights/${manifestSightId}`;
1120
+ let mResponse = `**🔮 Merlin MCP v${VERSION}**\n\n`;
1121
+ mResponse += `**API Key:** ${maskedKey}\n`;
1122
+ mResponse += `🔮 **Auto-connected from .merlin.json**\n\n`;
1123
+ mResponse += `**Repository:** ${manifest.repo}\n`;
1124
+ mResponse += `**Merlin Sights:** ${sightsUrl}\n`;
1125
+ mResponse += `**Local path:** \`${detected.rootDir}\`\n`;
1126
+ mResponse += `\n**IMPORTANT for file operations:** Use this full absolute path with Glob tool.\n`;
1127
+ mResponse += `Example: \`Glob(pattern: "**/*.ts", path: "${detected.rootDir}")\`\n`;
1128
+ return {
1129
+ content: [{
1130
+ type: 'text',
1131
+ text: mResponse,
1132
+ }],
1133
+ };
1134
+ }
1135
+ }
1136
+ // GitHub detected but not in Sights - show available Sights and offer to connect
1137
+ const notFoundResponse = await buildRepoNotFoundResponse(detected.url);
1138
+ return {
1139
+ content: [{
1140
+ type: 'text',
1141
+ text: notFoundResponse,
1142
+ }],
1143
+ };
1091
1144
  }
1092
- // GitHub detected but not in Sights - show available Sights and offer to connect
1093
- const notFoundResponse = await buildRepoNotFoundResponse(detected.url);
1145
+ // Check if we're watching a directory for GitHub connection
1146
+ const config = loadSavedConfig();
1147
+ if (config?.watchingDir) {
1148
+ return {
1149
+ content: [{
1150
+ type: 'text',
1151
+ text: `**Watching for GitHub connection.**\n\n**Directory:** ${config.watchingDir}\n\nThis project isn't on GitHub yet. When you add a GitHub remote, Merlin will detect it automatically.\n\n**To connect to GitHub:**\n\`\`\`\ngit remote add origin https://github.com/you/repo\ngit push -u origin main\n\`\`\`\n\nThen restart Claude Code or run \`merlin_get_selected_repo\` again.`,
1152
+ }],
1153
+ };
1154
+ }
1155
+ // No repo detected at all - show available Sights
1156
+ const notFoundResponse = await buildRepoNotFoundResponse();
1094
1157
  return {
1095
1158
  content: [{
1096
1159
  type: 'text',
@@ -1098,24 +1161,13 @@ export function createServer() {
1098
1161
  }],
1099
1162
  };
1100
1163
  }
1101
- // Check if we're watching a directory for GitHub connection
1102
- const config = loadSavedConfig();
1103
- if (config?.watchingDir) {
1104
- return {
1105
- content: [{
1106
- type: 'text',
1107
- text: `**Watching for GitHub connection.**\n\n**Directory:** ${config.watchingDir}\n\nThis project isn't on GitHub yet. When you add a GitHub remote, Merlin will detect it automatically.\n\n**To connect to GitHub:**\n\`\`\`\ngit remote add origin https://github.com/you/repo\ngit push -u origin main\n\`\`\`\n\nThen restart Claude Code or run \`merlin_get_selected_repo\` again.`,
1108
- }],
1109
- };
1164
+ catch (error) {
1165
+ if (isAuthError(error)) {
1166
+ return buildAuthErrorResponse();
1167
+ }
1168
+ // Re-throw non-auth errors (MCP SDK handles them)
1169
+ throw error;
1110
1170
  }
1111
- // No repo detected at all - show available Sights
1112
- const notFoundResponse = await buildRepoNotFoundResponse();
1113
- return {
1114
- content: [{
1115
- type: 'text',
1116
- text: notFoundResponse,
1117
- }],
1118
- };
1119
1171
  });
1120
1172
  // Tool: merlin_check_repo_status
1121
1173
  server.tool('merlin_check_repo_status', 'Check if a repository analysis is complete. Use after merlin_connect_repo to poll for completion. Returns status: pending, analyzing, completed, or failed.', {
@@ -1207,6 +1259,8 @@ export function createServer() {
1207
1259
  return { content: [{ type: 'text', text: response }] };
1208
1260
  }
1209
1261
  catch (error) {
1262
+ if (isAuthError(error))
1263
+ return buildAuthErrorResponse();
1210
1264
  return {
1211
1265
  content: [{
1212
1266
  type: 'text',
@@ -1379,6 +1433,8 @@ export function createServer() {
1379
1433
  };
1380
1434
  }
1381
1435
  catch (error) {
1436
+ if (isAuthError(error))
1437
+ return buildAuthErrorResponse();
1382
1438
  return {
1383
1439
  content: [{
1384
1440
  type: 'text',
@@ -1407,6 +1463,8 @@ export function createServer() {
1407
1463
  return { content: [{ type: 'text', text: impact }] };
1408
1464
  }
1409
1465
  catch (error) {
1466
+ if (isAuthError(error))
1467
+ return buildAuthErrorResponse();
1410
1468
  return {
1411
1469
  content: [{
1412
1470
  type: 'text',
@@ -1435,6 +1493,8 @@ export function createServer() {
1435
1493
  return { content: [{ type: 'text', text: similar }] };
1436
1494
  }
1437
1495
  catch (error) {
1496
+ if (isAuthError(error))
1497
+ return buildAuthErrorResponse();
1438
1498
  return {
1439
1499
  content: [{
1440
1500
  type: 'text',
@@ -1463,6 +1523,8 @@ export function createServer() {
1463
1523
  return { content: [{ type: 'text', text: examples }] };
1464
1524
  }
1465
1525
  catch (error) {
1526
+ if (isAuthError(error))
1527
+ return buildAuthErrorResponse();
1466
1528
  return {
1467
1529
  content: [{
1468
1530
  type: 'text',
@@ -1526,6 +1588,8 @@ export function createServer() {
1526
1588
  return { content: [{ type: 'text', text: result }] };
1527
1589
  }
1528
1590
  catch (error) {
1591
+ if (isAuthError(error))
1592
+ return buildAuthErrorResponse();
1529
1593
  return {
1530
1594
  content: [{
1531
1595
  type: 'text',
@@ -1585,6 +1649,8 @@ export function createServer() {
1585
1649
  };
1586
1650
  }
1587
1651
  catch (error) {
1652
+ if (isAuthError(error))
1653
+ return buildAuthErrorResponse();
1588
1654
  return {
1589
1655
  content: [{
1590
1656
  type: 'text',
@@ -1626,6 +1692,8 @@ export function createServer() {
1626
1692
  };
1627
1693
  }
1628
1694
  catch (error) {
1695
+ if (isAuthError(error))
1696
+ return buildAuthErrorResponse();
1629
1697
  return {
1630
1698
  content: [{
1631
1699
  type: 'text',
@@ -1669,6 +1737,8 @@ export function createServer() {
1669
1737
  return { content: [{ type: 'text', text: response }] };
1670
1738
  }
1671
1739
  catch (error) {
1740
+ if (isAuthError(error))
1741
+ return buildAuthErrorResponse();
1672
1742
  return {
1673
1743
  content: [{
1674
1744
  type: 'text',
@@ -1715,6 +1785,8 @@ export function createServer() {
1715
1785
  };
1716
1786
  }
1717
1787
  catch (error) {
1788
+ if (isAuthError(error))
1789
+ return buildAuthErrorResponse();
1718
1790
  return {
1719
1791
  content: [{
1720
1792
  type: 'text',
@@ -1779,6 +1851,8 @@ export function createServer() {
1779
1851
  };
1780
1852
  }
1781
1853
  catch (error) {
1854
+ if (isAuthError(error))
1855
+ return buildAuthErrorResponse();
1782
1856
  return {
1783
1857
  content: [{
1784
1858
  type: 'text',
@@ -1839,6 +1913,8 @@ export function createServer() {
1839
1913
  return { content: [{ type: 'text', text: response }] };
1840
1914
  }
1841
1915
  catch (error) {
1916
+ if (isAuthError(error))
1917
+ return buildAuthErrorResponse();
1842
1918
  return {
1843
1919
  content: [{
1844
1920
  type: 'text',
@@ -1889,6 +1965,8 @@ export function createServer() {
1889
1965
  };
1890
1966
  }
1891
1967
  catch (error) {
1968
+ if (isAuthError(error))
1969
+ return buildAuthErrorResponse();
1892
1970
  return {
1893
1971
  content: [{
1894
1972
  type: 'text',
@@ -1938,6 +2016,8 @@ export function createServer() {
1938
2016
  return { content: [{ type: 'text', text: response }] };
1939
2017
  }
1940
2018
  catch (error) {
2019
+ if (isAuthError(error))
2020
+ return buildAuthErrorResponse();
1941
2021
  return {
1942
2022
  content: [{
1943
2023
  type: 'text',
@@ -1994,6 +2074,8 @@ export function createServer() {
1994
2074
  };
1995
2075
  }
1996
2076
  catch (error) {
2077
+ if (isAuthError(error))
2078
+ return buildAuthErrorResponse();
1997
2079
  return {
1998
2080
  content: [{
1999
2081
  type: 'text',
@@ -2086,6 +2168,8 @@ export function createServer() {
2086
2168
  return { content: [{ type: 'text', text: response }] };
2087
2169
  }
2088
2170
  catch (error) {
2171
+ if (isAuthError(error))
2172
+ return buildAuthErrorResponse();
2089
2173
  return {
2090
2174
  content: [{
2091
2175
  type: 'text',
@@ -2144,6 +2228,8 @@ export function createServer() {
2144
2228
  };
2145
2229
  }
2146
2230
  catch (error) {
2231
+ if (isAuthError(error))
2232
+ return buildAuthErrorResponse();
2147
2233
  return {
2148
2234
  content: [{
2149
2235
  type: 'text',
@@ -2205,6 +2291,8 @@ export function createServer() {
2205
2291
  };
2206
2292
  }
2207
2293
  catch (error) {
2294
+ if (isAuthError(error))
2295
+ return buildAuthErrorResponse();
2208
2296
  return {
2209
2297
  content: [{
2210
2298
  type: 'text',
@@ -2251,6 +2339,8 @@ export function createServer() {
2251
2339
  return { content: [{ type: 'text', text: response }] };
2252
2340
  }
2253
2341
  catch (error) {
2342
+ if (isAuthError(error))
2343
+ return buildAuthErrorResponse();
2254
2344
  return {
2255
2345
  content: [{
2256
2346
  type: 'text',
@@ -2312,6 +2402,8 @@ export function createServer() {
2312
2402
  };
2313
2403
  }
2314
2404
  catch (error) {
2405
+ if (isAuthError(error))
2406
+ return buildAuthErrorResponse();
2315
2407
  return {
2316
2408
  content: [{
2317
2409
  type: 'text',