create-merlin-brain 5.3.8 → 5.4.1
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-rtk.cjs +296 -0
- package/bin/install.cjs +9 -0
- package/dist/server/api/client.d.ts.map +1 -1
- package/dist/server/api/client.js +35 -6
- package/dist/server/api/client.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +148 -42
- package/dist/server/server.js.map +1 -1
- package/dist/server/session-coach.d.ts.map +1 -1
- package/dist/server/session-coach.js +12 -0
- package/dist/server/session-coach.js.map +1 -1
- package/dist/server/session-guardian.d.ts +8 -1
- package/dist/server/session-guardian.d.ts.map +1 -1
- package/dist/server/session-guardian.js +26 -14
- package/dist/server/session-guardian.js.map +1 -1
- package/dist/server/tools/challenge.d.ts.map +1 -1
- package/dist/server/tools/challenge.js +7 -1
- package/dist/server/tools/challenge.js.map +1 -1
- package/dist/server/tools/computer-use.d.ts.map +1 -1
- package/dist/server/tools/computer-use.js +13 -6
- package/dist/server/tools/computer-use.js.map +1 -1
- package/dist/server/tools/index.d.ts +0 -1
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +0 -1
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/project.d.ts.map +1 -1
- package/dist/server/tools/project.js +14 -12
- package/dist/server/tools/project.js.map +1 -1
- package/dist/server/tools/verification-runner.d.ts +45 -0
- package/dist/server/tools/verification-runner.d.ts.map +1 -0
- package/dist/server/tools/verification-runner.js +264 -0
- package/dist/server/tools/verification-runner.js.map +1 -0
- package/dist/server/tools/verification.d.ts +3 -0
- package/dist/server/tools/verification.d.ts.map +1 -1
- package/dist/server/tools/verification.js +8 -265
- package/dist/server/tools/verification.js.map +1 -1
- package/files/hooks/pre-edit-sights-check.sh +40 -3
- package/files/hooks/security-scanner.sh +3 -4
- package/files/hooks/session-end.sh +45 -32
- package/files/hooks/smart-approve.sh +11 -3
- package/files/hooks/user-prompt-router.sh +30 -3
- package/files/merlin/VERSION +1 -1
- package/package.json +2 -2
- package/dist/server/tools/context.d.ts +0 -7
- package/dist/server/tools/context.d.ts.map +0 -1
- package/dist/server/tools/context.js +0 -614
- package/dist/server/tools/context.js.map +0 -1
- package/dist/server/tools/hud.d.ts +0 -13
- package/dist/server/tools/hud.d.ts.map +0 -1
- package/dist/server/tools/hud.js +0 -295
- package/dist/server/tools/hud.js.map +0 -1
- package/dist/server/tools/provider-ask.d.ts +0 -10
- package/dist/server/tools/provider-ask.d.ts.map +0 -1
- package/dist/server/tools/provider-ask.js +0 -234
- package/dist/server/tools/provider-ask.js.map +0 -1
- package/dist/server/tools/rate-limit.d.ts +0 -8
- package/dist/server/tools/rate-limit.d.ts.map +0 -1
- package/dist/server/tools/rate-limit.js +0 -184
- package/dist/server/tools/rate-limit.js.map +0 -1
- package/dist/server/tools/team-workers.d.ts +0 -7
- package/dist/server/tools/team-workers.d.ts.map +0 -1
- package/dist/server/tools/team-workers.js +0 -271
- package/dist/server/tools/team-workers.js.map +0 -1
package/dist/server/server.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* AI-powered codebase intelligence for coding agents
|
|
4
4
|
* https://merlin.build
|
|
5
5
|
*/
|
|
6
|
+
// merlin:allow-large-file: MCP server single-entry-point — all tool registrations must live here for SDK compatibility; split deferred to a future refactor phase
|
|
6
7
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
9
|
import { z } from 'zod';
|
|
@@ -13,6 +14,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
13
14
|
import { homedir } from 'os';
|
|
14
15
|
import { join } from 'path';
|
|
15
16
|
import { VERSION } from './version.js';
|
|
17
|
+
import { startGuardian, stopGuardian } from './session-guardian.js';
|
|
16
18
|
import { registerProjectTools } from './tools/project.js';
|
|
17
19
|
import { registerBehaviorTools } from './tools/behaviors.js';
|
|
18
20
|
import { registerVerificationTools } from './tools/verification.js';
|
|
@@ -53,6 +55,23 @@ function loadSavedConfig() {
|
|
|
53
55
|
}
|
|
54
56
|
return null;
|
|
55
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the Merlin auth token for cloud sync.
|
|
60
|
+
* Prefers process.env.MERLIN_API_KEY (the real key source — client.ts uses it).
|
|
61
|
+
* Falls back to apiKey stored in ~/.merlin/config.json for callers that need
|
|
62
|
+
* an explicit token string (e.g. registerLiteTools, registerConfigSyncTools).
|
|
63
|
+
*/
|
|
64
|
+
function getAuthToken() {
|
|
65
|
+
try {
|
|
66
|
+
if (process.env.MERLIN_API_KEY)
|
|
67
|
+
return process.env.MERLIN_API_KEY;
|
|
68
|
+
const c = loadSavedConfig();
|
|
69
|
+
return c?.apiKey ?? null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return process.env.MERLIN_API_KEY ?? null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
56
75
|
/** Create and configure the MCP server */
|
|
57
76
|
export function createServer() {
|
|
58
77
|
const server = new McpServer({
|
|
@@ -64,18 +83,45 @@ export function createServer() {
|
|
|
64
83
|
// Intercept every tool response to ensure the ⟡🔮 badge is present.
|
|
65
84
|
// This guarantees Merlin's visual identity on EVERY touchpoint without
|
|
66
85
|
// patching 80+ individual tool handlers.
|
|
86
|
+
//
|
|
87
|
+
// SDK-version note: this monkey-patches the deprecated server.tool() API.
|
|
88
|
+
// If the MCP SDK is upgraded to a version that changes how tool handlers are
|
|
89
|
+
// registered, this patch may silently stop working and must be revisited.
|
|
90
|
+
//
|
|
91
|
+
// IMPORTANT: _badgeWrap must NOT call recordToolCall(). Each tool handler
|
|
92
|
+
// already records exactly once via coachWrap(), wrapResponse(), or a direct
|
|
93
|
+
// call. Adding it here would double-count every tool invocation and corrupt
|
|
94
|
+
// drift detection.
|
|
67
95
|
const _origTool = server.tool.bind(server);
|
|
68
96
|
const _badgeWrap = (handler) => {
|
|
69
97
|
return async function (...handlerArgs) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = await handler.apply(this, handlerArgs);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error('[merlin] tool handler threw:', err);
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
// Guard: skip only when there is no text content to badge.
|
|
107
|
+
// NOTE: error results ARE badged too — "badge on EVERY action" is the
|
|
108
|
+
// brand contract (CLAUDE.md); Merlin must stay visible even on failures.
|
|
109
|
+
const r = result;
|
|
110
|
+
if (!r || !Array.isArray(r.content) || r.content.length === 0) {
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
const firstTextIdx = r.content.findIndex((b) => b.type === 'text' && typeof b.text === 'string');
|
|
114
|
+
if (firstTextIdx === -1) {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
const firstBlock = r.content[firstTextIdx];
|
|
118
|
+
if (firstBlock.text && !firstBlock.text.includes('⟡🔮')) {
|
|
119
|
+
// Build a new content array — do NOT mutate in place to stay safe for
|
|
120
|
+
// structured/outputSchema responses that may share references
|
|
121
|
+
const newContent = r.content.map((b, i) => i === firstTextIdx
|
|
122
|
+
? { ...b, text: `⟡🔮 MERLIN ›\n\n${b.text}` }
|
|
123
|
+
: b);
|
|
124
|
+
return { ...r, content: newContent };
|
|
79
125
|
}
|
|
80
126
|
return result;
|
|
81
127
|
};
|
|
@@ -98,7 +144,11 @@ export function createServer() {
|
|
|
98
144
|
// Cache for resolved repo IDs to avoid repeated git/API calls
|
|
99
145
|
// Key: url or 'auto' for auto-detected, Value: { repoId, expiresAt }
|
|
100
146
|
const repoIdCache = new Map();
|
|
101
|
-
const REPO_ID_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
147
|
+
const REPO_ID_CACHE_TTL = 5 * 60 * 1000; // 5 minutes — positive hits; repo ID won't change mid-session
|
|
148
|
+
const REPO_ID_NEGATIVE_TTL = 10 * 1000; // 10 seconds — negative hits; short so a freshly-connected repo isn't stuck
|
|
149
|
+
// In-flight dedup: coalesce concurrent resolveRepoId calls for the same key
|
|
150
|
+
// Prevents multiple parallel cold-cache lookups from all hitting the API simultaneously
|
|
151
|
+
const resolveRepoIdPending = new Map();
|
|
102
152
|
// Cache the detected repo root path (full absolute path) for file operations
|
|
103
153
|
// This is crucial for Claude Code's Glob tool to work with paths containing spaces
|
|
104
154
|
let cachedRepoRootPath = null;
|
|
@@ -111,6 +161,27 @@ export function createServer() {
|
|
|
111
161
|
// Helper to resolve repo ID from URL or use session-selected repo
|
|
112
162
|
// Also caches the repo root path for use with file operations
|
|
113
163
|
async function resolveRepoId(repoUrl) {
|
|
164
|
+
const dedupKey = repoUrl ?? 'auto';
|
|
165
|
+
// Fast-path: return cached result without acquiring the pending lock
|
|
166
|
+
if (!repoUrl && selectedRepoId)
|
|
167
|
+
return selectedRepoId;
|
|
168
|
+
const earlyHit = repoIdCache.get(dedupKey);
|
|
169
|
+
if (earlyHit && Date.now() < earlyHit.expiresAt)
|
|
170
|
+
return earlyHit.repoId;
|
|
171
|
+
// Coalesce concurrent in-flight requests for the same key
|
|
172
|
+
const inflight = resolveRepoIdPending.get(dedupKey);
|
|
173
|
+
if (inflight)
|
|
174
|
+
return inflight;
|
|
175
|
+
const promise = resolveRepoIdInner(repoUrl);
|
|
176
|
+
resolveRepoIdPending.set(dedupKey, promise);
|
|
177
|
+
try {
|
|
178
|
+
return await promise;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
resolveRepoIdPending.delete(dedupKey);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function resolveRepoIdInner(repoUrl) {
|
|
114
185
|
// If explicit URL provided, use it
|
|
115
186
|
if (repoUrl) {
|
|
116
187
|
const cacheKey = repoUrl;
|
|
@@ -120,7 +191,8 @@ export function createServer() {
|
|
|
120
191
|
}
|
|
121
192
|
const repo = await client.findRepoByUrl(repoUrl);
|
|
122
193
|
const repoId = repo?.id || null;
|
|
123
|
-
|
|
194
|
+
const ttl = repoId ? REPO_ID_CACHE_TTL : REPO_ID_NEGATIVE_TTL;
|
|
195
|
+
repoIdCache.set(cacheKey, { repoId, expiresAt: Date.now() + ttl });
|
|
124
196
|
return repoId;
|
|
125
197
|
}
|
|
126
198
|
// If session has a selected repo, use it (takes precedence over auto-detect)
|
|
@@ -148,14 +220,15 @@ export function createServer() {
|
|
|
148
220
|
}
|
|
149
221
|
const detected = await detectRepository();
|
|
150
222
|
if (!detected) {
|
|
151
|
-
repoIdCache.set(cacheKey, { repoId: null, expiresAt: Date.now() +
|
|
223
|
+
repoIdCache.set(cacheKey, { repoId: null, expiresAt: Date.now() + REPO_ID_NEGATIVE_TTL });
|
|
152
224
|
return null;
|
|
153
225
|
}
|
|
154
226
|
// Cache the repo root path - CRITICAL for paths with spaces
|
|
155
227
|
cachedRepoRootPath = detected.rootDir;
|
|
156
228
|
const repo = await client.findRepoByUrl(detected.url);
|
|
157
229
|
const repoId = repo?.id || null;
|
|
158
|
-
|
|
230
|
+
const autoTtl = repoId ? REPO_ID_CACHE_TTL : REPO_ID_NEGATIVE_TTL;
|
|
231
|
+
repoIdCache.set(cacheKey, { repoId, expiresAt: Date.now() + autoTtl });
|
|
159
232
|
return repoId;
|
|
160
233
|
}
|
|
161
234
|
// Helper to get the full absolute repo root path
|
|
@@ -458,10 +531,6 @@ export function createServer() {
|
|
|
458
531
|
const liteClient = await getOrInitLiteClient(repoRoot);
|
|
459
532
|
if (liteClient) {
|
|
460
533
|
// Use Lite mode! Get local data + try cloud enhancement in parallel
|
|
461
|
-
const getAuthToken = async () => {
|
|
462
|
-
const config = loadSavedConfig();
|
|
463
|
-
return config?.apiKey || null;
|
|
464
|
-
};
|
|
465
534
|
const [howTo, files, conventions, upgradeMsg, liteExport] = await Promise.all([
|
|
466
535
|
liteClient.getHowTo(task),
|
|
467
536
|
liteClient.getFiles({ purpose: task }),
|
|
@@ -471,7 +540,7 @@ export function createServer() {
|
|
|
471
540
|
]);
|
|
472
541
|
// Try cloud enhancement in parallel (non-blocking, 8s timeout)
|
|
473
542
|
const detected = await detectRepository(repoRoot);
|
|
474
|
-
const cloudResult = await enhanceFromCloud(task, { files: liteExport.files, conventions: liteExport.conventions, overview: liteExport.manifest }, detected?.url, getAuthToken);
|
|
543
|
+
const cloudResult = await enhanceFromCloud(task, { files: liteExport.files, conventions: liteExport.conventions, overview: liteExport.manifest }, detected?.url, async () => getAuthToken());
|
|
475
544
|
let context = `# Context for: ${task}\n\n`;
|
|
476
545
|
const modeLabel = cloudResult?.enhanced
|
|
477
546
|
? '⟡🔮 MERLIN Lite + Cloud Enhanced'
|
|
@@ -2588,14 +2657,26 @@ export function createServer() {
|
|
|
2588
2657
|
description: 'List of all analyzed repositories',
|
|
2589
2658
|
mimeType: 'application/json',
|
|
2590
2659
|
}, async () => {
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2660
|
+
try {
|
|
2661
|
+
const repos = await client.getRepositories();
|
|
2662
|
+
return {
|
|
2663
|
+
contents: [{
|
|
2664
|
+
uri: 'merlin://repos',
|
|
2665
|
+
mimeType: 'application/json',
|
|
2666
|
+
text: JSON.stringify(repos, null, 2),
|
|
2667
|
+
}],
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
catch (err) {
|
|
2671
|
+
console.error('[merlin] resource repos error:', err);
|
|
2672
|
+
return {
|
|
2673
|
+
contents: [{
|
|
2674
|
+
uri: 'merlin://repos',
|
|
2675
|
+
mimeType: 'application/json',
|
|
2676
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to fetch repositories' }),
|
|
2677
|
+
}],
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2599
2680
|
});
|
|
2600
2681
|
// ============================================================
|
|
2601
2682
|
// PROJECT MANAGEMENT TOOLS
|
|
@@ -2750,11 +2831,7 @@ export function createServer() {
|
|
|
2750
2831
|
registerLiteTools({
|
|
2751
2832
|
server,
|
|
2752
2833
|
getRepoRootPath,
|
|
2753
|
-
getAuthToken: async () =>
|
|
2754
|
-
// Get token from saved config
|
|
2755
|
-
const config = loadSavedConfig();
|
|
2756
|
-
return config?.apiKey || null;
|
|
2757
|
-
},
|
|
2834
|
+
getAuthToken: async () => getAuthToken(),
|
|
2758
2835
|
});
|
|
2759
2836
|
} // end free: registerLiteTools
|
|
2760
2837
|
// ============================================================
|
|
@@ -2771,10 +2848,7 @@ export function createServer() {
|
|
|
2771
2848
|
// ============================================================
|
|
2772
2849
|
registerConfigSyncTools({
|
|
2773
2850
|
server,
|
|
2774
|
-
getAuthToken: async () =>
|
|
2775
|
-
const config = loadSavedConfig();
|
|
2776
|
-
return config?.apiKey || null;
|
|
2777
|
-
},
|
|
2851
|
+
getAuthToken: async () => getAuthToken(),
|
|
2778
2852
|
});
|
|
2779
2853
|
} // end cloud: registerConfigSyncTools
|
|
2780
2854
|
// ============================================================
|
|
@@ -2856,16 +2930,31 @@ export async function startServer() {
|
|
|
2856
2930
|
await server.connect(transport);
|
|
2857
2931
|
// Log to stderr (stdout is used for MCP protocol)
|
|
2858
2932
|
console.error('Merlin MCP server started');
|
|
2933
|
+
// ── Global error boundary — log and keep the server alive ─────────
|
|
2934
|
+
// Without these, an uncaught async error in a tool handler would silently
|
|
2935
|
+
// crash the process. These handlers log the problem and continue running
|
|
2936
|
+
// so the session is not destroyed by a single misbehaving tool.
|
|
2937
|
+
process.on('uncaughtException', (err) => {
|
|
2938
|
+
console.error('[merlin] uncaughtException', err);
|
|
2939
|
+
});
|
|
2940
|
+
process.on('unhandledRejection', (reason) => {
|
|
2941
|
+
console.error('[merlin] unhandledRejection', reason);
|
|
2942
|
+
});
|
|
2859
2943
|
// ── Start Guardian HTTP sidecar (non-blocking) ────────────────────
|
|
2860
2944
|
// Enables hooks to query MCP session state via localhost HTTP.
|
|
2861
2945
|
// Starts on a random port, writes port file for hooks to discover.
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
console.error('Merlin Guardian: module load failed (non-fatal)');
|
|
2946
|
+
// The guardian's own SIGTERM/SIGHUP/exit self-registrations have been
|
|
2947
|
+
// removed; the exit paths below call stopGuardian() (statically imported,
|
|
2948
|
+
// synchronous, idempotent) so the port file is always cleaned up before exit.
|
|
2949
|
+
startGuardian().catch((err) => {
|
|
2950
|
+
console.error(`Merlin Guardian: failed to start (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2868
2951
|
});
|
|
2952
|
+
// Final safety net: clean up the guardian on ANY process exit path
|
|
2953
|
+
// (covers paths that call process.exit directly). Idempotent.
|
|
2954
|
+
process.on('exit', () => { try {
|
|
2955
|
+
stopGuardian();
|
|
2956
|
+
}
|
|
2957
|
+
catch { /* ignore */ } });
|
|
2869
2958
|
// ── Lifecycle: exit when parent (Claude Code) disconnects ──────────
|
|
2870
2959
|
// Without these handlers, merlin-brain becomes a zombie process when
|
|
2871
2960
|
// Claude Code closes the session. Over time, dozens of orphaned
|
|
@@ -2874,11 +2963,24 @@ export async function startServer() {
|
|
|
2874
2963
|
// When Claude Code exits, stdin gets closed → the 'end' event fires.
|
|
2875
2964
|
process.stdin.on('end', () => {
|
|
2876
2965
|
console.error('Merlin MCP server: stdin closed (parent disconnected), exiting.');
|
|
2966
|
+
try {
|
|
2967
|
+
stopGuardian();
|
|
2968
|
+
}
|
|
2969
|
+
catch { /* ignore */ }
|
|
2877
2970
|
process.exit(0);
|
|
2878
2971
|
});
|
|
2879
|
-
// Handle SIGTERM/SIGHUP gracefully (sent by OS or Claude Code on shutdown)
|
|
2972
|
+
// Handle SIGTERM/SIGHUP gracefully (sent by OS or Claude Code on shutdown).
|
|
2973
|
+
// stopGuardian() is called first so the .guardian-port file is removed
|
|
2974
|
+
// before exit — a stale port file would confuse the next session startup.
|
|
2880
2975
|
const gracefulExit = (signal) => {
|
|
2881
2976
|
console.error(`Merlin MCP server: received ${signal}, exiting.`);
|
|
2977
|
+
// Synchronous guardian cleanup — stopGuardian is statically imported and
|
|
2978
|
+
// idempotent, so the .guardian-port file is always removed BEFORE exit.
|
|
2979
|
+
// (A dynamic import().then() here would never drain before process.exit.)
|
|
2980
|
+
try {
|
|
2981
|
+
stopGuardian();
|
|
2982
|
+
}
|
|
2983
|
+
catch { /* guardian may not have started — safe */ }
|
|
2882
2984
|
process.exit(0);
|
|
2883
2985
|
};
|
|
2884
2986
|
process.on('SIGTERM', () => gracefulExit('SIGTERM'));
|
|
@@ -2887,6 +2989,10 @@ export async function startServer() {
|
|
|
2887
2989
|
process.stdout.on('error', (err) => {
|
|
2888
2990
|
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
|
2889
2991
|
console.error('Merlin MCP server: stdout broken pipe, exiting.');
|
|
2992
|
+
try {
|
|
2993
|
+
stopGuardian();
|
|
2994
|
+
}
|
|
2995
|
+
catch { /* ignore */ }
|
|
2890
2996
|
process.exit(0);
|
|
2891
2997
|
}
|
|
2892
2998
|
});
|