create-merlin-brain 5.3.7 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install-rtk.cjs +282 -0
- package/bin/install.cjs +26 -4
- 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 +146 -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/CLAUDE.md +1 -0
- package/files/commands/merlin/check-size.md +152 -0
- package/files/hooks/check-file-size.sh +166 -58
- package/files/hooks/pre-edit-sights-check.sh +19 -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 +24 -3
- package/files/merlin/VERSION +1 -1
- package/files/merlin-system-prompt.txt +3 -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,43 @@ 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 badge injection for error results or responses without text content
|
|
107
|
+
const r = result;
|
|
108
|
+
if (!r || r.isError || !Array.isArray(r.content) || r.content.length === 0) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
const firstTextIdx = r.content.findIndex((b) => b.type === 'text' && typeof b.text === 'string');
|
|
112
|
+
if (firstTextIdx === -1) {
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
const firstBlock = r.content[firstTextIdx];
|
|
116
|
+
if (firstBlock.text && !firstBlock.text.includes('⟡🔮')) {
|
|
117
|
+
// Build a new content array — do NOT mutate in place to stay safe for
|
|
118
|
+
// structured/outputSchema responses that may share references
|
|
119
|
+
const newContent = r.content.map((b, i) => i === firstTextIdx
|
|
120
|
+
? { ...b, text: `⟡🔮 MERLIN ›\n\n${b.text}` }
|
|
121
|
+
: b);
|
|
122
|
+
return { ...r, content: newContent };
|
|
79
123
|
}
|
|
80
124
|
return result;
|
|
81
125
|
};
|
|
@@ -98,7 +142,11 @@ export function createServer() {
|
|
|
98
142
|
// Cache for resolved repo IDs to avoid repeated git/API calls
|
|
99
143
|
// Key: url or 'auto' for auto-detected, Value: { repoId, expiresAt }
|
|
100
144
|
const repoIdCache = new Map();
|
|
101
|
-
const REPO_ID_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
145
|
+
const REPO_ID_CACHE_TTL = 5 * 60 * 1000; // 5 minutes — positive hits; repo ID won't change mid-session
|
|
146
|
+
const REPO_ID_NEGATIVE_TTL = 10 * 1000; // 10 seconds — negative hits; short so a freshly-connected repo isn't stuck
|
|
147
|
+
// In-flight dedup: coalesce concurrent resolveRepoId calls for the same key
|
|
148
|
+
// Prevents multiple parallel cold-cache lookups from all hitting the API simultaneously
|
|
149
|
+
const resolveRepoIdPending = new Map();
|
|
102
150
|
// Cache the detected repo root path (full absolute path) for file operations
|
|
103
151
|
// This is crucial for Claude Code's Glob tool to work with paths containing spaces
|
|
104
152
|
let cachedRepoRootPath = null;
|
|
@@ -111,6 +159,27 @@ export function createServer() {
|
|
|
111
159
|
// Helper to resolve repo ID from URL or use session-selected repo
|
|
112
160
|
// Also caches the repo root path for use with file operations
|
|
113
161
|
async function resolveRepoId(repoUrl) {
|
|
162
|
+
const dedupKey = repoUrl ?? 'auto';
|
|
163
|
+
// Fast-path: return cached result without acquiring the pending lock
|
|
164
|
+
if (!repoUrl && selectedRepoId)
|
|
165
|
+
return selectedRepoId;
|
|
166
|
+
const earlyHit = repoIdCache.get(dedupKey);
|
|
167
|
+
if (earlyHit && Date.now() < earlyHit.expiresAt)
|
|
168
|
+
return earlyHit.repoId;
|
|
169
|
+
// Coalesce concurrent in-flight requests for the same key
|
|
170
|
+
const inflight = resolveRepoIdPending.get(dedupKey);
|
|
171
|
+
if (inflight)
|
|
172
|
+
return inflight;
|
|
173
|
+
const promise = resolveRepoIdInner(repoUrl);
|
|
174
|
+
resolveRepoIdPending.set(dedupKey, promise);
|
|
175
|
+
try {
|
|
176
|
+
return await promise;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
resolveRepoIdPending.delete(dedupKey);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function resolveRepoIdInner(repoUrl) {
|
|
114
183
|
// If explicit URL provided, use it
|
|
115
184
|
if (repoUrl) {
|
|
116
185
|
const cacheKey = repoUrl;
|
|
@@ -120,7 +189,8 @@ export function createServer() {
|
|
|
120
189
|
}
|
|
121
190
|
const repo = await client.findRepoByUrl(repoUrl);
|
|
122
191
|
const repoId = repo?.id || null;
|
|
123
|
-
|
|
192
|
+
const ttl = repoId ? REPO_ID_CACHE_TTL : REPO_ID_NEGATIVE_TTL;
|
|
193
|
+
repoIdCache.set(cacheKey, { repoId, expiresAt: Date.now() + ttl });
|
|
124
194
|
return repoId;
|
|
125
195
|
}
|
|
126
196
|
// If session has a selected repo, use it (takes precedence over auto-detect)
|
|
@@ -148,14 +218,15 @@ export function createServer() {
|
|
|
148
218
|
}
|
|
149
219
|
const detected = await detectRepository();
|
|
150
220
|
if (!detected) {
|
|
151
|
-
repoIdCache.set(cacheKey, { repoId: null, expiresAt: Date.now() +
|
|
221
|
+
repoIdCache.set(cacheKey, { repoId: null, expiresAt: Date.now() + REPO_ID_NEGATIVE_TTL });
|
|
152
222
|
return null;
|
|
153
223
|
}
|
|
154
224
|
// Cache the repo root path - CRITICAL for paths with spaces
|
|
155
225
|
cachedRepoRootPath = detected.rootDir;
|
|
156
226
|
const repo = await client.findRepoByUrl(detected.url);
|
|
157
227
|
const repoId = repo?.id || null;
|
|
158
|
-
|
|
228
|
+
const autoTtl = repoId ? REPO_ID_CACHE_TTL : REPO_ID_NEGATIVE_TTL;
|
|
229
|
+
repoIdCache.set(cacheKey, { repoId, expiresAt: Date.now() + autoTtl });
|
|
159
230
|
return repoId;
|
|
160
231
|
}
|
|
161
232
|
// Helper to get the full absolute repo root path
|
|
@@ -458,10 +529,6 @@ export function createServer() {
|
|
|
458
529
|
const liteClient = await getOrInitLiteClient(repoRoot);
|
|
459
530
|
if (liteClient) {
|
|
460
531
|
// 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
532
|
const [howTo, files, conventions, upgradeMsg, liteExport] = await Promise.all([
|
|
466
533
|
liteClient.getHowTo(task),
|
|
467
534
|
liteClient.getFiles({ purpose: task }),
|
|
@@ -471,7 +538,7 @@ export function createServer() {
|
|
|
471
538
|
]);
|
|
472
539
|
// Try cloud enhancement in parallel (non-blocking, 8s timeout)
|
|
473
540
|
const detected = await detectRepository(repoRoot);
|
|
474
|
-
const cloudResult = await enhanceFromCloud(task, { files: liteExport.files, conventions: liteExport.conventions, overview: liteExport.manifest }, detected?.url, getAuthToken);
|
|
541
|
+
const cloudResult = await enhanceFromCloud(task, { files: liteExport.files, conventions: liteExport.conventions, overview: liteExport.manifest }, detected?.url, async () => getAuthToken());
|
|
475
542
|
let context = `# Context for: ${task}\n\n`;
|
|
476
543
|
const modeLabel = cloudResult?.enhanced
|
|
477
544
|
? '⟡🔮 MERLIN Lite + Cloud Enhanced'
|
|
@@ -2588,14 +2655,26 @@ export function createServer() {
|
|
|
2588
2655
|
description: 'List of all analyzed repositories',
|
|
2589
2656
|
mimeType: 'application/json',
|
|
2590
2657
|
}, async () => {
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2658
|
+
try {
|
|
2659
|
+
const repos = await client.getRepositories();
|
|
2660
|
+
return {
|
|
2661
|
+
contents: [{
|
|
2662
|
+
uri: 'merlin://repos',
|
|
2663
|
+
mimeType: 'application/json',
|
|
2664
|
+
text: JSON.stringify(repos, null, 2),
|
|
2665
|
+
}],
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
catch (err) {
|
|
2669
|
+
console.error('[merlin] resource repos error:', err);
|
|
2670
|
+
return {
|
|
2671
|
+
contents: [{
|
|
2672
|
+
uri: 'merlin://repos',
|
|
2673
|
+
mimeType: 'application/json',
|
|
2674
|
+
text: JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to fetch repositories' }),
|
|
2675
|
+
}],
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2599
2678
|
});
|
|
2600
2679
|
// ============================================================
|
|
2601
2680
|
// PROJECT MANAGEMENT TOOLS
|
|
@@ -2750,11 +2829,7 @@ export function createServer() {
|
|
|
2750
2829
|
registerLiteTools({
|
|
2751
2830
|
server,
|
|
2752
2831
|
getRepoRootPath,
|
|
2753
|
-
getAuthToken: async () =>
|
|
2754
|
-
// Get token from saved config
|
|
2755
|
-
const config = loadSavedConfig();
|
|
2756
|
-
return config?.apiKey || null;
|
|
2757
|
-
},
|
|
2832
|
+
getAuthToken: async () => getAuthToken(),
|
|
2758
2833
|
});
|
|
2759
2834
|
} // end free: registerLiteTools
|
|
2760
2835
|
// ============================================================
|
|
@@ -2771,10 +2846,7 @@ export function createServer() {
|
|
|
2771
2846
|
// ============================================================
|
|
2772
2847
|
registerConfigSyncTools({
|
|
2773
2848
|
server,
|
|
2774
|
-
getAuthToken: async () =>
|
|
2775
|
-
const config = loadSavedConfig();
|
|
2776
|
-
return config?.apiKey || null;
|
|
2777
|
-
},
|
|
2849
|
+
getAuthToken: async () => getAuthToken(),
|
|
2778
2850
|
});
|
|
2779
2851
|
} // end cloud: registerConfigSyncTools
|
|
2780
2852
|
// ============================================================
|
|
@@ -2856,16 +2928,31 @@ export async function startServer() {
|
|
|
2856
2928
|
await server.connect(transport);
|
|
2857
2929
|
// Log to stderr (stdout is used for MCP protocol)
|
|
2858
2930
|
console.error('Merlin MCP server started');
|
|
2931
|
+
// ── Global error boundary — log and keep the server alive ─────────
|
|
2932
|
+
// Without these, an uncaught async error in a tool handler would silently
|
|
2933
|
+
// crash the process. These handlers log the problem and continue running
|
|
2934
|
+
// so the session is not destroyed by a single misbehaving tool.
|
|
2935
|
+
process.on('uncaughtException', (err) => {
|
|
2936
|
+
console.error('[merlin] uncaughtException', err);
|
|
2937
|
+
});
|
|
2938
|
+
process.on('unhandledRejection', (reason) => {
|
|
2939
|
+
console.error('[merlin] unhandledRejection', reason);
|
|
2940
|
+
});
|
|
2859
2941
|
// ── Start Guardian HTTP sidecar (non-blocking) ────────────────────
|
|
2860
2942
|
// Enables hooks to query MCP session state via localhost HTTP.
|
|
2861
2943
|
// 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)');
|
|
2944
|
+
// The guardian's own SIGTERM/SIGHUP/exit self-registrations have been
|
|
2945
|
+
// removed; the exit paths below call stopGuardian() (statically imported,
|
|
2946
|
+
// synchronous, idempotent) so the port file is always cleaned up before exit.
|
|
2947
|
+
startGuardian().catch((err) => {
|
|
2948
|
+
console.error(`Merlin Guardian: failed to start (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2868
2949
|
});
|
|
2950
|
+
// Final safety net: clean up the guardian on ANY process exit path
|
|
2951
|
+
// (covers paths that call process.exit directly). Idempotent.
|
|
2952
|
+
process.on('exit', () => { try {
|
|
2953
|
+
stopGuardian();
|
|
2954
|
+
}
|
|
2955
|
+
catch { /* ignore */ } });
|
|
2869
2956
|
// ── Lifecycle: exit when parent (Claude Code) disconnects ──────────
|
|
2870
2957
|
// Without these handlers, merlin-brain becomes a zombie process when
|
|
2871
2958
|
// Claude Code closes the session. Over time, dozens of orphaned
|
|
@@ -2874,11 +2961,24 @@ export async function startServer() {
|
|
|
2874
2961
|
// When Claude Code exits, stdin gets closed → the 'end' event fires.
|
|
2875
2962
|
process.stdin.on('end', () => {
|
|
2876
2963
|
console.error('Merlin MCP server: stdin closed (parent disconnected), exiting.');
|
|
2964
|
+
try {
|
|
2965
|
+
stopGuardian();
|
|
2966
|
+
}
|
|
2967
|
+
catch { /* ignore */ }
|
|
2877
2968
|
process.exit(0);
|
|
2878
2969
|
});
|
|
2879
|
-
// Handle SIGTERM/SIGHUP gracefully (sent by OS or Claude Code on shutdown)
|
|
2970
|
+
// Handle SIGTERM/SIGHUP gracefully (sent by OS or Claude Code on shutdown).
|
|
2971
|
+
// stopGuardian() is called first so the .guardian-port file is removed
|
|
2972
|
+
// before exit — a stale port file would confuse the next session startup.
|
|
2880
2973
|
const gracefulExit = (signal) => {
|
|
2881
2974
|
console.error(`Merlin MCP server: received ${signal}, exiting.`);
|
|
2975
|
+
// Synchronous guardian cleanup — stopGuardian is statically imported and
|
|
2976
|
+
// idempotent, so the .guardian-port file is always removed BEFORE exit.
|
|
2977
|
+
// (A dynamic import().then() here would never drain before process.exit.)
|
|
2978
|
+
try {
|
|
2979
|
+
stopGuardian();
|
|
2980
|
+
}
|
|
2981
|
+
catch { /* guardian may not have started — safe */ }
|
|
2882
2982
|
process.exit(0);
|
|
2883
2983
|
};
|
|
2884
2984
|
process.on('SIGTERM', () => gracefulExit('SIGTERM'));
|
|
@@ -2887,6 +2987,10 @@ export async function startServer() {
|
|
|
2887
2987
|
process.stdout.on('error', (err) => {
|
|
2888
2988
|
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
|
2889
2989
|
console.error('Merlin MCP server: stdout broken pipe, exiting.');
|
|
2990
|
+
try {
|
|
2991
|
+
stopGuardian();
|
|
2992
|
+
}
|
|
2993
|
+
catch { /* ignore */ }
|
|
2890
2994
|
process.exit(0);
|
|
2891
2995
|
}
|
|
2892
2996
|
});
|