@getmarrow/install 0.1.69 → 0.1.70
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/README.md +16 -8
- package/package.json +1 -1
- package/src/governed-runner.js +3 -3
- package/src/installer.js +121 -63
- package/src/marrow-own-tools.js +121 -0
- package/src/mcp-runtime.js +361 -309
- package/src/pins.js +3 -3
- package/src/runtime-core.js +594 -0
package/README.md
CHANGED
|
@@ -103,15 +103,23 @@ Persistent controller lifecycle is currently Linux-only. On macOS or Windows, ac
|
|
|
103
103
|
|
|
104
104
|
The controller is not a boot service. After a host restart, or any exit that skips its shutdown handler, doctor reports it as `stale` until the next install, update, or `controller ensure`. Local control can be inspected and changed with `npx @getmarrow/install control status|disable --yes|enable`. While local control is disabled, the controller is not started, and doctor reports a stopped or stale controller as not required instead of recommending `controller ensure`; missing state means enabled, and an explicit owner disable is preserved. Unsafe controller state or an unverified or unresponsive controller process keeps its exact fix.
|
|
105
105
|
|
|
106
|
-
##
|
|
106
|
+
## Key handling at install
|
|
107
107
|
|
|
108
|
-
v0.1.69
|
|
108
|
+
These behaviors shipped in v0.1.69 and still apply.
|
|
109
109
|
|
|
110
|
-
- **No key, no half install.** With no API key, `activate` and every `--yes` run
|
|
111
|
-
- **A blank key
|
|
110
|
+
- **No key, no half install.** With no API key, `activate` and every `--yes` run stop, write nothing and print how to get a key.
|
|
111
|
+
- **A blank key does not hide a saved one.** An empty `MARROW_API_KEY` value does not hide a valid key saved in `~/.marrow/env`.
|
|
112
112
|
- **`MARROW_KEY` works on its own.** It is used when `MARROW_API_KEY` is absent.
|
|
113
|
-
- **Key-file steps.** They
|
|
114
|
-
|
|
113
|
+
- **Key-file steps.** They include `chmod 700 ~/.marrow` as well as `chmod 600 ~/.marrow/env`. See [Get started](#get-started).
|
|
114
|
+
|
|
115
|
+
## What's New in v0.1.70
|
|
116
|
+
|
|
117
|
+
v0.1.70 installs MCP `3.9.102`. In short (the full entry is in the [CHANGELOG](https://github.com/getmarrow/marrow-install/blob/master/CHANGELOG.md)):
|
|
118
|
+
|
|
119
|
+
- **Own-tool names are exact.** Hook matchers and the Cursor guard skip the gate only for Marrow's registered tools on server `marrow`, by exact name. Look-alike servers or names go through the gate. Re-run `update` to refresh hooks written with the old matcher. See [Hooks the installer writes](#hooks-the-installer-writes).
|
|
120
|
+
- **Hooks run only the verified local MCP runtime.** They no longer start `npx`; when the runtime is missing, they heal it from the pinned, integrity-checked package, and hold if they cannot. When npm or the network is down, pre-action hooks warn and let the action run; an integrity mismatch or any other failure holds. See [Local MCP runtime](#local-mcp-runtime).
|
|
121
|
+
- **Fallback npm install protects your npm login.** It sends npm auth only when the network settings come from your own npm config, and installs exactly the pinned MCP and SDK.
|
|
122
|
+
- **MCP 3.9.102.** A cloned project's `.env` can no longer point the MCP server at another account or server.
|
|
115
123
|
|
|
116
124
|
Earlier release notes are in the [CHANGELOG](https://github.com/getmarrow/marrow-install/blob/master/CHANGELOG.md).
|
|
117
125
|
|
|
@@ -267,12 +275,12 @@ Never connect an agent to the email inbox that receives Marrow approval mail. An
|
|
|
267
275
|
|
|
268
276
|
### Hooks the installer writes
|
|
269
277
|
|
|
270
|
-
These hooks are written with MCP `3.9.99` and later (`MCP_HOST_APPROVAL_HOOKS_SINCE` in `src/pins.js`). v0.1.
|
|
278
|
+
These hooks are written with MCP `3.9.99` and later (`MCP_HOST_APPROVAL_HOOKS_SINCE` in `src/pins.js`). v0.1.70 installs MCP `3.9.102`, so `install` and `update` write them; with an older MCP the earlier layout stays. `update` also uses a registry-verified MCP at or above that version.
|
|
271
279
|
|
|
272
280
|
| Host | Hooks for approvals | How the operator answers |
|
|
273
281
|
| --- | --- | --- |
|
|
274
282
|
| Claude Code | `PermissionRequest` → `claude-permission-request-hook` and `PostToolBatch` → `claude-hook`, both `async: true`, next to the existing hooks; the same entries `marrow-mcp setup` writes | In Claude Code's own permission dialog. The pass-through marker never answers or delays the dialog; a permission rejection is recorded as a decline, an interruption never is |
|
|
275
|
-
| Cursor | `beforeShellExecution` → `cursor-pre-action-hook` and `beforeMCPExecution` → the same entrypoint through a small guard, both `failClosed: true` with a 15-second timeout; `afterShellExecution` and `afterMCPExecution` → `cursor-hook`; `sessionStart` → `cursor-session-hook`; `beforeSubmitPrompt` → `cursor-context-hook`. Shell calls leave the `preToolUse` matcher. MCP calls stay in it, because cloud agents never run `beforeMCPExecution`: there `preToolUse` holds them, and in a local interactive session it lets `beforeMCPExecution` ask. The guard answers Marrow's own `
|
|
283
|
+
| Cursor | `beforeShellExecution` → `cursor-pre-action-hook` and `beforeMCPExecution` → the same entrypoint through a small guard, both `failClosed: true` with a 15-second timeout; `afterShellExecution` and `afterMCPExecution` → `cursor-hook`; `sessionStart` → `cursor-session-hook`; `beforeSubmitPrompt` → `cursor-context-hook`. Shell calls leave the `preToolUse` matcher. MCP calls stay in it, because cloud agents never run `beforeMCPExecution`: there `preToolUse` holds them, and in a local interactive session it lets `beforeMCPExecution` ask. The guard answers at once only for Marrow's own tools: server exactly `marrow` and one of the 58 registered names (`src/marrow-own-tools.js`). Only the Claude Code matcher skips them, by the exact name `mcp__marrow__<name>`; Cursor (`MCP:<tool>`) and Gemini (`mcp_<server>_<tool>`) names do not prove the server, so every MCP call there reaches the hook, as does an unregistered `marrow_*` name, a case variant or a look-alike server | Cursor's own prompt for shell and MCP calls in a local interactive session; the typed reply the hook shows the user (`marrow approve CODE`) for its other tools there. Background and cloud agents hold quietly |
|
|
276
284
|
| Gemini CLI | `BeforeAgent` → `gemini-context-hook`. The `BeforeTool` guard passes a denial that carries a code for the user only (`systemMessage`); any other output, a crash, or no answer within 4.5 seconds still blocks the call | The typed reply, in a local interactive session (`gemini`); `gemini -p` holds quietly |
|
|
277
285
|
| Codex | `UserPromptSubmit` → `codex-context-hook` (unchanged). Codex is never configured to "ask", because Codex lets an asked call run | The typed reply, in a local interactive session; `codex exec` holds quietly |
|
|
278
286
|
|
package/package.json
CHANGED
package/src/governed-runner.js
CHANGED
|
@@ -3089,9 +3089,9 @@ async function sidecarOnly(parsed) {
|
|
|
3089
3089
|
const detection = detectEnvironment(root, process.env);
|
|
3090
3090
|
// Maintenance re-applies the controller's own configured agent id and base URL. A different
|
|
3091
3091
|
// value found in managed MCP config is replaced unless the owner allowlisted it, and the
|
|
3092
|
-
// divergence is reported. It never edits the owner's Hermes config. Hooks
|
|
3093
|
-
//
|
|
3094
|
-
const mcpRuntime = maintenanceMcpRuntime(detection
|
|
3092
|
+
// divergence is reported. It never edits the owner's Hermes config. Hooks are written in
|
|
3093
|
+
// the local MCP runtime form (never npx); the copy is checked, never installed, here.
|
|
3094
|
+
const mcpRuntime = maintenanceMcpRuntime(detection);
|
|
3095
3095
|
const plan = buildPlan(detection, {
|
|
3096
3096
|
mode: managedMode,
|
|
3097
3097
|
agentId: options.agentId,
|
package/src/installer.js
CHANGED
|
@@ -20,11 +20,18 @@ const {
|
|
|
20
20
|
planHermesMcpConfig,
|
|
21
21
|
} = require('./hermes-config');
|
|
22
22
|
const { ensureOwnerApiKey, readOwnerApiKey } = require('./owner-env');
|
|
23
|
+
const { buildOwnToolMatchers, cursorOwnToolGuardCondition } = require('./marrow-own-tools');
|
|
23
24
|
const {
|
|
24
25
|
delocalizeHookCommand,
|
|
26
|
+
delocalizeHookScriptText,
|
|
25
27
|
ensureMcpRuntime,
|
|
28
|
+
hookTextUsesNpx,
|
|
29
|
+
localizeHookScriptText,
|
|
26
30
|
localizeHookSettingsText,
|
|
27
31
|
mapHookCommands,
|
|
32
|
+
mcpServerLaunch,
|
|
33
|
+
mcpServerLaunchVersion,
|
|
34
|
+
mcpServerUsesNpx,
|
|
28
35
|
removeMcpRuntime,
|
|
29
36
|
verifyMcpRuntime,
|
|
30
37
|
} = require('./mcp-runtime');
|
|
@@ -231,7 +238,8 @@ function adapterProvenanceForMcpTarget(target) {
|
|
|
231
238
|
|
|
232
239
|
function retargetMcpPackageSpec(value, version) {
|
|
233
240
|
const target = compatibleMcpTargetVersion(version) ? version : MCP_ADAPTER_VERSION;
|
|
234
|
-
return String(value).split(MCP_PACKAGE_SPEC).join(`@getmarrow/mcp@${target}`)
|
|
241
|
+
return String(value).split(MCP_PACKAGE_SPEC).join(`@getmarrow/mcp@${target}`)
|
|
242
|
+
.split(`/.marrow/runtime/mcp/${MCP_ADAPTER_VERSION}/`).join(`/.marrow/runtime/mcp/${target}/`);
|
|
235
243
|
}
|
|
236
244
|
|
|
237
245
|
function retargetMcpDowngradeRecommendation(value, targetVersion) {
|
|
@@ -280,8 +288,8 @@ const CURSOR_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC}
|
|
|
280
288
|
const CURSOR_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-session-hook`;
|
|
281
289
|
const CURSOR_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-context-hook`;
|
|
282
290
|
// Cursor's beforeMCPExecution takes no matcher, so this guard answers allow at once for
|
|
283
|
-
// Marrow's own tools (server "marrow",
|
|
284
|
-
//
|
|
291
|
+
// Marrow's own tools (server exactly "marrow", tool one of the registered names in
|
|
292
|
+
// marrow-own-tools.js; this event names the server) without starting npx. Every other call goes to the MCP entrypoint with its input
|
|
285
293
|
// unchanged; a crash, a non-zero exit or output that is not a JSON object blocks (exit 2).
|
|
286
294
|
const CURSOR_MCP_GUARD_FAILURE = 'Marrow governance adapter was unavailable; this MCP call is blocked.';
|
|
287
295
|
const CURSOR_MCP_PRE_ACTION_GUARD_SOURCE = [
|
|
@@ -291,7 +299,7 @@ const CURSOR_MCP_PRE_ACTION_GUARD_SOURCE = [
|
|
|
291
299
|
'process.stdin.on("error",fail);',
|
|
292
300
|
'process.stdin.on("data",c=>{size+=c.length;if(size>16777216){over=true;return;}chunks.push(c);});',
|
|
293
301
|
'process.stdin.on("end",()=>{if(over){fail();return;}const raw=Buffer.concat(chunks);let ev=null;try{ev=JSON.parse(raw.toString("utf8"));}catch{ev=null;}',
|
|
294
|
-
|
|
302
|
+
`if(ev&&typeof ev==="object"&&${cursorOwnToolGuardCondition()}){done=true;process.stdout.write("{\\"permission\\":\\"allow\\"}\\n");return;}`,
|
|
295
303
|
'let child;try{',
|
|
296
304
|
`child=spawn(process.platform==="win32"?"npx.cmd":"npx",${JSON.stringify(['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp', 'cursor-pre-action-hook'])},{stdio:["pipe","pipe","ignore"]});`,
|
|
297
305
|
'}catch{fail();return;}',
|
|
@@ -387,21 +395,22 @@ const GROK_PRE_ACTION_GUARD_SOURCE = [
|
|
|
387
395
|
'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
|
|
388
396
|
].join('');
|
|
389
397
|
const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
398
|
+
const OWN_TOOL_MATCHERS = buildOwnToolMatchers();
|
|
399
|
+
const NATIVE_HOOK_MATCHER = OWN_TOOL_MATCHERS.claude;
|
|
400
|
+
const CODEX_NATIVE_HOOK_MATCHER = OWN_TOOL_MATCHERS.codex;
|
|
401
|
+
const CURSOR_NATIVE_HOOK_MATCHER = OWN_TOOL_MATCHERS.cursor;
|
|
393
402
|
// Host approvals: Cursor enforces an "ask" only on beforeShellExecution and beforeMCPExecution,
|
|
394
403
|
// never on preToolUse. Shell calls move to beforeShellExecution (it runs in cloud agents too).
|
|
395
404
|
// MCP calls stay in preToolUse as well, because cloud agents never run beforeMCPExecution: the
|
|
396
405
|
// MCP hook defers there to beforeMCPExecution only in a local interactive session (sessionStart
|
|
397
406
|
// evidence) and holds in cloud and background agents. The result hooks keep the full matcher.
|
|
398
|
-
const CURSOR_PRE_TOOL_USE_HOST_APPROVAL_MATCHER =
|
|
407
|
+
const CURSOR_PRE_TOOL_USE_HOST_APPROVAL_MATCHER = OWN_TOOL_MATCHERS.cursorPreToolUseHostApproval;
|
|
399
408
|
// Cursor's gating hooks are failClosed: a cold npx start on a slow network must not block a
|
|
400
409
|
// call, so they get room beyond the MCP hook's own 4-second budget.
|
|
401
410
|
const CURSOR_GATE_HOOK_TIMEOUT_SECONDS = 15;
|
|
402
411
|
const CURSOR_EXECUTION_PRE_EVENTS = Object.freeze(['beforeShellExecution', 'beforeMCPExecution']);
|
|
403
412
|
const CURSOR_EXECUTION_POST_EVENTS = Object.freeze(['afterShellExecution', 'afterMCPExecution']);
|
|
404
|
-
const GEMINI_NATIVE_HOOK_MATCHER =
|
|
413
|
+
const GEMINI_NATIVE_HOOK_MATCHER = OWN_TOOL_MATCHERS.gemini;
|
|
405
414
|
const GROK_NATIVE_HOOK_MATCHER = 'run_terminal_command|search_replace|write|spawn_subagent|use_tool|workflow|image_gen|image_edit|image_to_video|reference_to_video';
|
|
406
415
|
const CODEX_HOOK_TIMEOUT_SECONDS = 5;
|
|
407
416
|
const CODEX_SESSION_TIMEOUT_SECONDS = 3;
|
|
@@ -644,6 +653,9 @@ function isMcpProcessCommand(command) {
|
|
|
644
653
|
const executable = path.basename(args[0]);
|
|
645
654
|
if (new Set(['bash', 'bwrap', 'dash', 'fish', 'sh', 'zsh']).has(executable)) return false;
|
|
646
655
|
if (executable === 'marrow-mcp') return true;
|
|
656
|
+
// The verified local runtime: node on the inline loader with the pinned package spec.
|
|
657
|
+
if (executable === 'node' && args[1] === '-e' && args.includes('marrow-mcp')
|
|
658
|
+
&& args.some((arg) => /^--package=@getmarrow\/mcp@\d+\.\d+\.\d+$/.test(arg))) return true;
|
|
647
659
|
|
|
648
660
|
if (executable === 'node'
|
|
649
661
|
&& args[1]
|
|
@@ -862,9 +874,11 @@ function parseArgs(argv, env = process.env) {
|
|
|
862
874
|
json: false,
|
|
863
875
|
activate: false,
|
|
864
876
|
controller: true,
|
|
865
|
-
// Hooks start
|
|
866
|
-
// npx
|
|
867
|
-
|
|
877
|
+
// Hooks start only the verified local copy of the pinned MCP (installed on write runs); they
|
|
878
|
+
// never start npx, which resolves the package from the project directory first. The old
|
|
879
|
+
// opt-out (MARROW_LOCAL_RUNTIME=0, --no-local-runtime) is accepted and ignored.
|
|
880
|
+
mcpLocalRuntime: true,
|
|
881
|
+
...(env.MARROW_LOCAL_RUNTIME === '0' ? { localRuntimeSwitchIgnored: true } : {}),
|
|
868
882
|
};
|
|
869
883
|
let explicitOperation = false;
|
|
870
884
|
|
|
@@ -905,6 +919,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
905
919
|
explicitOperation = true;
|
|
906
920
|
options.uninstall = true;
|
|
907
921
|
}
|
|
922
|
+
else if (arg === '--all') {
|
|
923
|
+
options.all = true;
|
|
924
|
+
}
|
|
908
925
|
else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') {
|
|
909
926
|
explicitOperation = true;
|
|
910
927
|
options.doctor = true;
|
|
@@ -917,7 +934,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
917
934
|
options.selfTestExplicitlyDisabled = true;
|
|
918
935
|
}
|
|
919
936
|
else if (arg === '--no-controller') options.controller = false;
|
|
920
|
-
else if (arg === '--no-local-runtime') options.
|
|
937
|
+
else if (arg === '--no-local-runtime') options.localRuntimeSwitchIgnored = true;
|
|
921
938
|
else if (arg === '--self-test') options.selfTest = true;
|
|
922
939
|
else if (arg === '--cwd') options.cwd = path.resolve(optionValue() || options.cwd);
|
|
923
940
|
else if (arg === '--mode') {
|
|
@@ -954,6 +971,9 @@ function parseArgs(argv, env = process.env) {
|
|
|
954
971
|
}
|
|
955
972
|
}
|
|
956
973
|
|
|
974
|
+
if (options.all && !options.uninstall) {
|
|
975
|
+
throw new Error('--all is only valid with uninstall');
|
|
976
|
+
}
|
|
957
977
|
if (options.uninstall && (options.activate || options.repair || options.doctor)) {
|
|
958
978
|
throw new Error('uninstall cannot be combined with activate, update, repair, or doctor');
|
|
959
979
|
}
|
|
@@ -989,7 +1009,7 @@ function usage() {
|
|
|
989
1009
|
npx @getmarrow/install doctor --self-test
|
|
990
1010
|
npx @getmarrow/install --mcp --yes
|
|
991
1011
|
npx @getmarrow/install --sdk --yes
|
|
992
|
-
npx @getmarrow/install uninstall [--yes]
|
|
1012
|
+
npx @getmarrow/install uninstall [--yes] [--all]
|
|
993
1013
|
|
|
994
1014
|
Options:
|
|
995
1015
|
(no command) Detect, install, self-test, and start the supported persistent controller
|
|
@@ -1004,12 +1024,12 @@ Options:
|
|
|
1004
1024
|
--base-url <url> Marrow API base URL
|
|
1005
1025
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
1006
1026
|
--no-controller Do not start the local background controller during install/repair
|
|
1007
|
-
--no-local-runtime
|
|
1027
|
+
--no-local-runtime Ignored: hooks always start the verified local MCP copy in ~/.marrow/runtime, never npx
|
|
1008
1028
|
--no-self-test Skip API smoke/self-test
|
|
1009
1029
|
--verbose Print the full report instead of the one-line summary and log file
|
|
1010
|
-
uninstall Preview removing only Marrow's own hooks, MCP server entries
|
|
1011
|
-
|
|
1012
|
-
|
|
1030
|
+
uninstall Preview removing only Marrow's own hooks, MCP server entries and instructions
|
|
1031
|
+
from this project; add --yes to remove them. Your own hooks and settings are
|
|
1032
|
+
kept, and so is the local MCP runtime other projects share; --all removes it.
|
|
1013
1033
|
|
|
1014
1034
|
Get a key at https://getmarrow.ai/signup. Put it in the owner-only ~/.marrow/env as
|
|
1015
1035
|
MARROW_API_KEY=your_key, then run chmod 700 ~/.marrow && chmod 600 ~/.marrow/env, or export MARROW_API_KEY. Keep it off the command line: command-line
|
|
@@ -2190,19 +2210,25 @@ function clineHookContract(detection) {
|
|
|
2190
2210
|
stage: 'pre_action',
|
|
2191
2211
|
path: detection.paths.clinePreToolUseHook,
|
|
2192
2212
|
label: 'Cline PreToolUse native hook',
|
|
2193
|
-
content: clinePreToolUseHookSource(),
|
|
2213
|
+
content: localizeHookScriptText(clinePreToolUseHookSource(), MCP_ADAPTER_VERSION),
|
|
2214
|
+
source: clinePreToolUseHookSource(),
|
|
2215
|
+
legacyContent: clinePreToolUseHookSource(),
|
|
2194
2216
|
},
|
|
2195
2217
|
{
|
|
2196
2218
|
stage: 'action_result',
|
|
2197
2219
|
path: detection.paths.clinePostToolUseHook,
|
|
2198
2220
|
label: 'Cline PostToolUse native hook',
|
|
2199
|
-
content: clineTelemetryHookSource('cline-hook'),
|
|
2221
|
+
content: localizeHookScriptText(clineTelemetryHookSource('cline-hook'), MCP_ADAPTER_VERSION),
|
|
2222
|
+
source: clineTelemetryHookSource('cline-hook'),
|
|
2223
|
+
legacyContent: clineTelemetryHookSource('cline-hook'),
|
|
2200
2224
|
},
|
|
2201
2225
|
{
|
|
2202
2226
|
stage: 'cancel_closeout',
|
|
2203
2227
|
path: detection.paths.clineTaskCancelHook,
|
|
2204
2228
|
label: 'Cline TaskCancel native hook',
|
|
2205
|
-
content: clineTelemetryHookSource('cline-session-hook'),
|
|
2229
|
+
content: localizeHookScriptText(clineTelemetryHookSource('cline-session-hook'), MCP_ADAPTER_VERSION),
|
|
2230
|
+
source: clineTelemetryHookSource('cline-session-hook'),
|
|
2231
|
+
legacyContent: clineTelemetryHookSource('cline-session-hook'),
|
|
2206
2232
|
},
|
|
2207
2233
|
];
|
|
2208
2234
|
}
|
|
@@ -2348,9 +2374,7 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
2348
2374
|
const mcpConfigs = [detection.paths.mcpJson, detection.paths.cursorMcp]
|
|
2349
2375
|
.map((filePath) => safeJsonObject(filePath));
|
|
2350
2376
|
if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
|
|
2351
|
-
config?.mcpServers?.marrow
|
|
2352
|
-
&& Array.isArray(config.mcpServers.marrow.args)
|
|
2353
|
-
&& config.mcpServers.marrow.args.join(' ') === `-y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp`
|
|
2377
|
+
mcpServerLaunchVersion(config?.mcpServers?.marrow) === mcpTargetVersion
|
|
2354
2378
|
))) observedHooks.push('mcp_tool_calls');
|
|
2355
2379
|
const fingerprintMaterial = changes
|
|
2356
2380
|
.filter((change) => change.applied || change.already_present)
|
|
@@ -2522,9 +2546,15 @@ function upsertMcpServerConfig(filePath, options = {}) {
|
|
|
2522
2546
|
const env = { MARROW_BASE_URL: baseUrl };
|
|
2523
2547
|
if (keptAgentId) env.MARROW_FLEET_AGENT_ID = keptAgentId;
|
|
2524
2548
|
if (configuredProfile !== 'unset') env.MARROW_TOOL_PROFILE = configuredProfile;
|
|
2549
|
+
// The server starts the verified local runtime (it installs the pinned copy itself when it is
|
|
2550
|
+
// missing), never npx from the project directory: a copy planted there would otherwise become
|
|
2551
|
+
// the Marrow MCP server, whose own tools the hook matchers leave ungated.
|
|
2552
|
+
const serverVersion = compatibleMcpTargetVersion(options.mcpVersion) ? options.mcpVersion : MCP_ADAPTER_VERSION;
|
|
2525
2553
|
servers.marrow = {
|
|
2526
|
-
|
|
2527
|
-
|
|
2554
|
+
...mcpServerLaunch(serverVersion, {
|
|
2555
|
+
platform: options.hookPlatform || process.platform,
|
|
2556
|
+
...(options.mcpIntegrity && serverVersion === options.mcpVersion ? { integrity: options.mcpIntegrity } : {}),
|
|
2557
|
+
}),
|
|
2528
2558
|
env,
|
|
2529
2559
|
};
|
|
2530
2560
|
config.mcpServers = servers;
|
|
@@ -2827,15 +2857,25 @@ function buildPlan(detection, options) {
|
|
|
2827
2857
|
write.identityDivergence = [];
|
|
2828
2858
|
return retarget(upsertMcpServerConfig(target, {
|
|
2829
2859
|
...mcpConfigOptions,
|
|
2860
|
+
mcpVersion: mcpTargetVersion,
|
|
2861
|
+
mcpIntegrity: mcpTargetIntegrity,
|
|
2862
|
+
hookPlatform,
|
|
2830
2863
|
onDivergence: (divergence) => write.identityDivergence.push(divergence),
|
|
2831
2864
|
}));
|
|
2832
2865
|
};
|
|
2833
2866
|
return write;
|
|
2834
2867
|
};
|
|
2835
2868
|
const mcpTargetVersion = executableMcpTarget(options).version;
|
|
2836
|
-
// Hooks start the verified local MCP runtime for this exact version
|
|
2837
|
-
|
|
2838
|
-
const
|
|
2869
|
+
// Hooks always start the verified local MCP runtime for this exact version, never npx: when
|
|
2870
|
+
// that copy is missing or changed, a pre-action hook holds the call until a repair installs it.
|
|
2871
|
+
const hookPlatform = options.hookPlatform || process.platform;
|
|
2872
|
+
const mcpTargetIntegrity = executableMcpTarget(options).integrity;
|
|
2873
|
+
// A Marrow command already in the file with the same configuration is kept byte for byte.
|
|
2874
|
+
const localize = (text, filePath) => localizeHookSettingsText(text, mcpTargetVersion, {
|
|
2875
|
+
platform: hookPlatform,
|
|
2876
|
+
integrity: mcpTargetIntegrity,
|
|
2877
|
+
previousText: filePath ? safeRead(filePath) : null,
|
|
2878
|
+
});
|
|
2839
2879
|
const retarget = (value) => retargetMcpPackageSpec(value, mcpTargetVersion);
|
|
2840
2880
|
// The host-approval hook layout only for an MCP that answers those hooks.
|
|
2841
2881
|
const hostApprovals = hostApprovalHooksSupported(mcpTargetVersion);
|
|
@@ -2866,7 +2906,7 @@ function buildPlan(detection, options) {
|
|
|
2866
2906
|
type: 'json-transform',
|
|
2867
2907
|
path: detection.paths.claudeSettings,
|
|
2868
2908
|
label: 'Claude Code MCP passive hooks',
|
|
2869
|
-
transform: (filePath) => localize(retarget(upsertClaudeHooks(filePath, { hostApprovals }))),
|
|
2909
|
+
transform: (filePath) => localize(retarget(upsertClaudeHooks(filePath, { hostApprovals })), filePath),
|
|
2870
2910
|
});
|
|
2871
2911
|
}
|
|
2872
2912
|
if (detection.codex) {
|
|
@@ -2874,7 +2914,7 @@ function buildPlan(detection, options) {
|
|
|
2874
2914
|
type: 'json-transform',
|
|
2875
2915
|
path: detection.paths.codexHooks,
|
|
2876
2916
|
label: 'Codex native hooks',
|
|
2877
|
-
transform: (filePath) => localize(retarget(upsertCodexHooks(filePath))),
|
|
2917
|
+
transform: (filePath) => localize(retarget(upsertCodexHooks(filePath)), filePath),
|
|
2878
2918
|
});
|
|
2879
2919
|
}
|
|
2880
2920
|
if (detection.cline) {
|
|
@@ -2883,7 +2923,9 @@ function buildPlan(detection, options) {
|
|
|
2883
2923
|
type: 'owned-executable',
|
|
2884
2924
|
path: hook.path,
|
|
2885
2925
|
label: hook.label,
|
|
2886
|
-
content: retarget(hook.
|
|
2926
|
+
content: localizeHookScriptText(retarget(hook.source), mcpTargetVersion, { integrity: mcpTargetIntegrity }),
|
|
2927
|
+
// The npx script 0.1.68 and earlier wrote is Marrow's too, and is rewritten.
|
|
2928
|
+
legacyContent: retarget(hook.legacyContent),
|
|
2887
2929
|
mode: 0o755,
|
|
2888
2930
|
conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
|
|
2889
2931
|
});
|
|
@@ -2894,7 +2936,7 @@ function buildPlan(detection, options) {
|
|
|
2894
2936
|
type: 'json-transform',
|
|
2895
2937
|
path: detection.paths.windsurfHooks,
|
|
2896
2938
|
label: 'Windsurf native hooks',
|
|
2897
|
-
transform: (filePath) => localize(retarget(upsertWindsurfHooks(filePath))),
|
|
2939
|
+
transform: (filePath) => localize(retarget(upsertWindsurfHooks(filePath)), filePath),
|
|
2898
2940
|
});
|
|
2899
2941
|
}
|
|
2900
2942
|
if (detection.gemini) {
|
|
@@ -2902,7 +2944,7 @@ function buildPlan(detection, options) {
|
|
|
2902
2944
|
type: 'json-transform',
|
|
2903
2945
|
path: detection.paths.geminiSettings,
|
|
2904
2946
|
label: 'Gemini CLI native hooks',
|
|
2905
|
-
transform: (filePath) => localize(retarget(upsertGeminiHooks(filePath, { hostApprovals }))),
|
|
2947
|
+
transform: (filePath) => localize(retarget(upsertGeminiHooks(filePath, { hostApprovals })), filePath),
|
|
2906
2948
|
});
|
|
2907
2949
|
}
|
|
2908
2950
|
if (detection.grok) {
|
|
@@ -2913,7 +2955,7 @@ function buildPlan(detection, options) {
|
|
|
2913
2955
|
label: 'Grok native hooks',
|
|
2914
2956
|
isManaged: managedGrokHooksFile,
|
|
2915
2957
|
conflict_fix: 'Preserve or move the unmanaged owner Grok hook file after owner review, then rerun npx @getmarrow/install update.',
|
|
2916
|
-
transform: (filePath) => localize(retarget(upsertGrokHooks(filePath))),
|
|
2958
|
+
transform: (filePath) => localize(retarget(upsertGrokHooks(filePath)), filePath),
|
|
2917
2959
|
});
|
|
2918
2960
|
}
|
|
2919
2961
|
writes.push(mcpConfigWrite(detection.paths.mcpJson, 'Project MCP server config'));
|
|
@@ -2937,7 +2979,7 @@ function buildPlan(detection, options) {
|
|
|
2937
2979
|
type: 'json-transform',
|
|
2938
2980
|
path: detection.paths.cursorHooks,
|
|
2939
2981
|
label: 'Cursor native hooks',
|
|
2940
|
-
transform: (filePath) => localize(retarget(upsertCursorHooks(filePath, { hostApprovals }))),
|
|
2982
|
+
transform: (filePath) => localize(retarget(upsertCursorHooks(filePath, { hostApprovals })), filePath),
|
|
2941
2983
|
});
|
|
2942
2984
|
writes.push(mcpConfigWrite(detection.paths.cursorMcp, 'Cursor MCP server config'));
|
|
2943
2985
|
}
|
|
@@ -3082,7 +3124,7 @@ function applyPlan(plan, options) {
|
|
|
3082
3124
|
} else if (write.type === 'owned-executable') {
|
|
3083
3125
|
// A file Marrow wrote for another pinned MCP version is still Marrow's and moves to this
|
|
3084
3126
|
// pin; any other difference is the owner's edit and is never overwritten.
|
|
3085
|
-
if (fileExists && before !== write.content && !sameMarrowFile(before, [write.content])) {
|
|
3127
|
+
if (fileExists && before !== write.content && !sameMarrowFile(before, [write.content, write.legacyContent].filter(Boolean))) {
|
|
3086
3128
|
after = before;
|
|
3087
3129
|
hookConflict = true;
|
|
3088
3130
|
} else {
|
|
@@ -3238,8 +3280,8 @@ function withoutMarrowMcpServer(config) {
|
|
|
3238
3280
|
const entry = servers && typeof servers === 'object' && !Array.isArray(servers) ? servers.marrow : null;
|
|
3239
3281
|
if (!entry) return { config, removed: 0, kept: 0, custom: false };
|
|
3240
3282
|
const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
3241
|
-
const ours = entry.command === 'npx' && args.includes('marrow-mcp')
|
|
3242
|
-
&& args.some((arg) => /^--package=@getmarrow\/mcp(?:@[^\s]+)?$/.test(arg));
|
|
3283
|
+
const ours = Boolean(mcpServerLaunchVersion(entry)) || (entry.command === 'npx' && args.includes('marrow-mcp')
|
|
3284
|
+
&& args.some((arg) => /^--package=@getmarrow\/mcp(?:@[^\s]+)?$/.test(arg)));
|
|
3243
3285
|
if (!ours) return { config, removed: 0, kept: 1, custom: true };
|
|
3244
3286
|
const nextServers = { ...servers };
|
|
3245
3287
|
delete nextServers.marrow;
|
|
@@ -3260,7 +3302,8 @@ function withoutMarrowInstructionBlock(content) {
|
|
|
3260
3302
|
|
|
3261
3303
|
// Marrow-written files compare equal once the pinned MCP version is normalized.
|
|
3262
3304
|
function sameMarrowFile(actual, candidates) {
|
|
3263
|
-
const normalize = (value) => String(value).replace(/@getmarrow\/mcp@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g, '@getmarrow/mcp@<pinned>')
|
|
3305
|
+
const normalize = (value) => delocalizeHookScriptText(String(value)).replace(/@getmarrow\/mcp@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g, '@getmarrow/mcp@<pinned>')
|
|
3306
|
+
.replace(/\/\.marrow\/runtime\/mcp\/\d+\.\d+\.\d+\//g, '/.marrow/runtime/mcp/<pinned>/');
|
|
3264
3307
|
const normalized = normalize(actual);
|
|
3265
3308
|
return candidates.some((candidate) => normalize(candidate) === normalized);
|
|
3266
3309
|
}
|
|
@@ -3289,7 +3332,7 @@ function buildUninstallPlan(detection) {
|
|
|
3289
3332
|
const instructionVariants = [false, true].map((hostApprovals) => passiveInstructions({ hostApprovals }).replace(/<!--[^>]+-->/g, '').trim() + '\n');
|
|
3290
3333
|
steps.push({ kind: 'owned-file', path: detection.paths.cursorRules, label: 'Cursor Marrow rule', candidates: instructionVariants });
|
|
3291
3334
|
for (const hook of clineHookContract(detection)) {
|
|
3292
|
-
steps.push({ kind: 'owned-file', path: hook.path, label: hook.label, candidates: [hook.content] });
|
|
3335
|
+
steps.push({ kind: 'owned-file', path: hook.path, label: hook.label, candidates: [hook.content, hook.legacyContent] });
|
|
3293
3336
|
}
|
|
3294
3337
|
return steps;
|
|
3295
3338
|
}
|
|
@@ -3406,8 +3449,12 @@ async function uninstall(options = {}) {
|
|
|
3406
3449
|
hermesEntry = false;
|
|
3407
3450
|
}
|
|
3408
3451
|
if (hermesEntry) kept.push({ path: detection.paths.hermesConfig, reason: 'remove mcp_servers.marrow by hand; uninstall never rewrites the Hermes config, which holds other servers\' settings' });
|
|
3409
|
-
// The local MCP runtime (~/.marrow/runtime)
|
|
3410
|
-
|
|
3452
|
+
// The local MCP runtime (~/.marrow/runtime) is shared by every project on this machine, so a
|
|
3453
|
+
// project uninstall keeps it. `uninstall --all` removes it too; hooks left in other projects
|
|
3454
|
+
// then install it again on their next start (or hold when they cannot).
|
|
3455
|
+
const runtime = options.all === true
|
|
3456
|
+
? removeMcpRuntime(options.home || detection.home, { dryRun: !apply })
|
|
3457
|
+
: { removed: false, would_remove: false, kept: true, path: path.join(options.home || detection.home, '.marrow', 'runtime') };
|
|
3411
3458
|
const removedEntries = changes.reduce((sum, change) => sum + change.removed_entries, 0);
|
|
3412
3459
|
return {
|
|
3413
3460
|
uninstall: {
|
|
@@ -3418,6 +3465,7 @@ async function uninstall(options = {}) {
|
|
|
3418
3465
|
changes,
|
|
3419
3466
|
kept,
|
|
3420
3467
|
runtime: runtime.removed || runtime.would_remove ? runtime : null,
|
|
3468
|
+
runtime_kept: runtime.kept === true && fs.existsSync(runtime.path) ? runtime.path : null,
|
|
3421
3469
|
removed_entries: removedEntries,
|
|
3422
3470
|
next_step: apply
|
|
3423
3471
|
? 'Restart the agent hosts so they stop loading the removed hooks and MCP server.'
|
|
@@ -3440,7 +3488,8 @@ function printUninstallReport(report, sink = (text) => process.stdout.write(text
|
|
|
3440
3488
|
else if (change.action === 'kept' || change.action === 'skipped') sink(`- ${where(change.path)}: left unchanged (${change.reason}).\n`);
|
|
3441
3489
|
}
|
|
3442
3490
|
for (const entry of result.kept) sink(`- ${where(entry.path)}: left in place; ${entry.reason}.\n`);
|
|
3443
|
-
if (result.runtime) sink(`- ${result.runtime.path}: ${result.applied ? 'removed' : 'would remove'} Marrow's local MCP runtime (other projects
|
|
3491
|
+
if (result.runtime) sink(`- ${result.runtime.path}: ${result.applied ? 'removed' : 'would remove'} Marrow's local MCP runtime (Marrow hooks left in other projects install it again on their next start).\n`);
|
|
3492
|
+
else if (result.runtime_kept) sink(`- ${result.runtime_kept}: kept; other projects on this machine may use Marrow's local MCP runtime. Add --all to remove it too.\n`);
|
|
3444
3493
|
if (result.controller?.stopped) sink(`Stopped ${result.controller.stopped} Marrow controller${result.controller.stopped === 1 ? '' : 's'} for this project.\n`);
|
|
3445
3494
|
sink(`${result.next_step}\n`);
|
|
3446
3495
|
}
|
|
@@ -4144,7 +4193,10 @@ function printReport(report, sink) {
|
|
|
4144
4193
|
if (report.mcp_runtime) {
|
|
4145
4194
|
const runtime = report.mcp_runtime;
|
|
4146
4195
|
out('\nMCP local runtime:\n');
|
|
4147
|
-
out(`- hooks start: ${runtime.hooks_start === 'local_runtime' ? `the verified local copy of @getmarrow/mcp@${runtime.version} (${runtime.path})` : '
|
|
4196
|
+
out(`- hooks start: ${runtime.hooks_start === 'local_runtime' ? `the verified local copy of @getmarrow/mcp@${runtime.version} (${runtime.path})` : 'no verified local copy yet: Marrow hooks install the pinned copy on their next start; if npm or the registry cannot be reached they warn and let the action run, and any other failure holds it'}\n`);
|
|
4197
|
+
for (const filePath of runtime.legacy_npx_hooks || []) out(`- needs repair: ${filePath} still starts @getmarrow/mcp through npx\n`);
|
|
4198
|
+
if (runtime.needs_repair) out('- repair: run npx -y @getmarrow/install@latest --repair (from a terminal outside the project, with --cwd <project folder>)\n');
|
|
4199
|
+
if (runtime.local_runtime_switch === 'ignored') out('- note: MARROW_LOCAL_RUNTIME=0 / --no-local-runtime are ignored; hooks never start npx\n');
|
|
4148
4200
|
out(`- state: ${runtime.state}${runtime.reason ? ` (${runtime.reason})` : ''}\n`);
|
|
4149
4201
|
if (runtime.removed_versions?.length) out(`- removed older versions: ${runtime.removed_versions.join(', ')}\n`);
|
|
4150
4202
|
}
|
|
@@ -4410,11 +4462,9 @@ function hermesWiringReport(detection, changes, options, planMode) {
|
|
|
4410
4462
|
}
|
|
4411
4463
|
|
|
4412
4464
|
// The local MCP runtime for the target version: installed (or kept) on a write run of the
|
|
4413
|
-
// command, otherwise only an existing verified copy is used.
|
|
4414
|
-
// MARROW_LOCAL_RUNTIME=0, --no-local-runtime) leaves every hook of this project on npx.
|
|
4465
|
+
// command, otherwise only an existing verified copy is used. Hooks start nothing else.
|
|
4415
4466
|
function resolveMcpRuntime(detection, options, mcpTarget) {
|
|
4416
4467
|
const home = options.home || detection.home;
|
|
4417
|
-
if (options.mcpLocalRuntime === false) return { state: 'disabled' };
|
|
4418
4468
|
const version = mcpTarget.version;
|
|
4419
4469
|
const integrity = mcpTarget.integrity || (version === MCP_ADAPTER_VERSION ? MCP_ADAPTER_INTEGRITY : null);
|
|
4420
4470
|
if (!integrity) return { state: 'skipped', reason: 'no_verified_integrity', version };
|
|
@@ -4425,6 +4475,7 @@ function resolveMcpRuntime(detection, options, mcpTarget) {
|
|
|
4425
4475
|
version,
|
|
4426
4476
|
integrity,
|
|
4427
4477
|
sdk: { version: SDK_ADAPTER_VERSION, integrity: SDK_ADAPTER_INTEGRITY },
|
|
4478
|
+
...(options.hookPlatform ? { platform: options.hookPlatform } : {}),
|
|
4428
4479
|
...(typeof options.mcpRuntimeInstall === 'function' ? { npmInstall: options.mcpRuntimeInstall } : {}),
|
|
4429
4480
|
...(options.mcpRuntimeNode ? { nodePath: options.mcpRuntimeNode } : {}),
|
|
4430
4481
|
// One line on stderr before npm runs, so the wait is never silent (stdout stays clean
|
|
@@ -4432,36 +4483,43 @@ function resolveMcpRuntime(detection, options, mcpTarget) {
|
|
|
4432
4483
|
onProgress: typeof options.mcpRuntimeProgress === 'function' ? options.mcpRuntimeProgress : (text) => process.stderr.write(text),
|
|
4433
4484
|
});
|
|
4434
4485
|
}
|
|
4435
|
-
return verifyMcpRuntime(home, version, integrity) || { state: 'absent', version };
|
|
4486
|
+
return verifyMcpRuntime(home, version, integrity, options.hookPlatform ? { platform: options.hookPlatform } : {}) || { state: 'absent', version };
|
|
4436
4487
|
}
|
|
4437
4488
|
|
|
4438
|
-
//
|
|
4439
|
-
|
|
4489
|
+
// Managed hook files that still start @getmarrow/mcp through npx (written by 0.1.68 or earlier,
|
|
4490
|
+
// or by hand): each needs a repair, which rewrites it in the local runtime form.
|
|
4491
|
+
function legacyNpxHookFiles(detection) {
|
|
4440
4492
|
const paths = detection.paths || {};
|
|
4441
|
-
return [paths.claudeSettings, paths.codexHooks, paths.cursorHooks, paths.windsurfHooks, paths.geminiSettings
|
|
4442
|
-
.
|
|
4493
|
+
return [paths.claudeSettings, paths.codexHooks, paths.cursorHooks, paths.windsurfHooks, paths.geminiSettings,
|
|
4494
|
+
paths.grokHooks, paths.clinePreToolUseHook, paths.clinePostToolUseHook, paths.clineTaskCancelHook]
|
|
4495
|
+
.filter((filePath, index, all) => filePath && all.indexOf(filePath) === index && hookTextUsesNpx(safeRead(filePath)))
|
|
4496
|
+
// The Marrow MCP server entry too (F-I1): npx there would start a copy planted in the project.
|
|
4497
|
+
.concat([paths.mcpJson, paths.cursorMcp].filter((filePath, index, all) => filePath && all.indexOf(filePath) === index
|
|
4498
|
+
&& mcpServerUsesNpx(safeJsonObject(filePath))));
|
|
4443
4499
|
}
|
|
4444
4500
|
|
|
4445
|
-
// The controller's maintenance rewrites managed hooks for the pinned MCP
|
|
4446
|
-
//
|
|
4447
|
-
// project's hooks start it (verify only: maintenance never installs), on npx otherwise. A
|
|
4448
|
-
// project installed with --no-local-runtime stays on npx even when another project's install
|
|
4449
|
-
// put a runtime on this machine.
|
|
4501
|
+
// The controller's maintenance rewrites managed hooks for the pinned MCP, always in the local
|
|
4502
|
+
// runtime form; it only verifies the copy (maintenance never installs one).
|
|
4450
4503
|
function maintenanceMcpRuntime(detection, options = {}) {
|
|
4451
|
-
if (options.mcpLocalRuntime === false || !projectHooksUseLocalRuntime(detection)) return null;
|
|
4452
4504
|
const runtime = resolveMcpRuntime(detection, { ...options, maintenance: true, yes: false }, executableMcpTarget(options));
|
|
4453
4505
|
return runtime.version && ['present', 'installed', 'verified'].includes(runtime.state) ? runtime : null;
|
|
4454
4506
|
}
|
|
4455
4507
|
|
|
4456
|
-
function mcpRuntimeReport(runtime) {
|
|
4508
|
+
function mcpRuntimeReport(runtime, { legacyNpxHooks = [], switchIgnored = false } = {}) {
|
|
4457
4509
|
if (!runtime) return null;
|
|
4510
|
+
const verified = Boolean(runtime.directory && ['present', 'installed', 'verified'].includes(runtime.state));
|
|
4458
4511
|
return {
|
|
4459
4512
|
state: runtime.state,
|
|
4460
4513
|
version: runtime.version || null,
|
|
4461
4514
|
path: runtime.directory || null,
|
|
4462
4515
|
...(runtime.reason ? { reason: runtime.reason } : {}),
|
|
4463
4516
|
...(runtime.removed_versions?.length ? { removed_versions: runtime.removed_versions } : {}),
|
|
4464
|
-
|
|
4517
|
+
// Hooks never start npx: without a verified copy they install the pinned one themselves on
|
|
4518
|
+
// their next start, and pre-action hooks hold the call only when that fails.
|
|
4519
|
+
hooks_start: verified ? 'local_runtime' : 'self_heal_on_next_start',
|
|
4520
|
+
...(legacyNpxHooks.length ? { legacy_npx_hooks: legacyNpxHooks } : {}),
|
|
4521
|
+
needs_repair: !verified || legacyNpxHooks.length > 0,
|
|
4522
|
+
...(switchIgnored ? { local_runtime_switch: 'ignored' } : {}),
|
|
4465
4523
|
};
|
|
4466
4524
|
}
|
|
4467
4525
|
|
|
@@ -4513,7 +4571,7 @@ async function install(options) {
|
|
|
4513
4571
|
options.mcpTargetVersion = mcpTarget.version;
|
|
4514
4572
|
// A maintenance pass keeps the project's hooks in the form its install chose.
|
|
4515
4573
|
const mcpRuntime = options.maintenance === true
|
|
4516
|
-
? maintenanceMcpRuntime(detection, options) || { state:
|
|
4574
|
+
? maintenanceMcpRuntime(detection, options) || { state: 'absent', version: mcpTarget.version }
|
|
4517
4575
|
: resolveMcpRuntime(detection, options, mcpTarget);
|
|
4518
4576
|
options.mcpRuntime = mcpRuntime.version && ['present', 'installed', 'verified'].includes(mcpRuntime.state) ? mcpRuntime : null;
|
|
4519
4577
|
const plan = buildPlan(detection, options);
|
|
@@ -4775,7 +4833,7 @@ async function install(options) {
|
|
|
4775
4833
|
harnessReload,
|
|
4776
4834
|
firstCapture: firstCapturePath(detection),
|
|
4777
4835
|
hermes,
|
|
4778
|
-
mcp_runtime: mcpRuntimeReport(mcpRuntime),
|
|
4836
|
+
mcp_runtime: mcpRuntimeReport(mcpRuntime, { legacyNpxHooks: legacyNpxHookFiles(detection), switchIgnored: options.localRuntimeSwitchIgnored === true }),
|
|
4779
4837
|
changes,
|
|
4780
4838
|
doctor: {
|
|
4781
4839
|
active: Boolean(!selfTest.skipped && selfTest.active),
|