@haven_ai/connect 0.1.2-alpha → 0.1.4-alpha
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 +1 -1
- package/dist/cli.cjs +663 -137
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +666 -140
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +663 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +666 -140
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -207,14 +207,156 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
207
207
|
);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
var MCP_RUNTIME_MANIFEST = {
|
|
211
|
+
mcpPackage: "@haven_ai/mcp",
|
|
212
|
+
mcpVersion: mcp.MCP_VERSION,
|
|
213
|
+
sdkPackage: "@haven_ai/sdk",
|
|
214
|
+
sdkVersion: "0.1.7",
|
|
215
|
+
signerPackage: "@haven_ai/signer",
|
|
216
|
+
signerVersion: "0.1.0-alpha",
|
|
217
|
+
minimumNodeVersion: "20.0.0",
|
|
218
|
+
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
219
|
+
requiredTools: mcp.registeredToolNames()
|
|
220
|
+
};
|
|
221
|
+
function mcpPackageSpec() {
|
|
222
|
+
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
223
|
+
}
|
|
224
|
+
function sdkPackageSpec() {
|
|
225
|
+
return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
226
|
+
}
|
|
227
|
+
function signerPackageSpec() {
|
|
228
|
+
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/runtime-registry.ts
|
|
232
|
+
var RUNTIME_PROFILES = {
|
|
233
|
+
"claude-code": {
|
|
234
|
+
id: "claude-code",
|
|
235
|
+
label: "Claude Code",
|
|
236
|
+
restartMode: "restart-session",
|
|
237
|
+
canWriteRuntimeConfig: true
|
|
238
|
+
},
|
|
239
|
+
"codex-cli": {
|
|
240
|
+
id: "codex-cli",
|
|
241
|
+
label: "Codex CLI",
|
|
242
|
+
restartMode: "restart-session",
|
|
243
|
+
canWriteRuntimeConfig: true
|
|
244
|
+
},
|
|
245
|
+
"codex-desktop": {
|
|
246
|
+
id: "codex-desktop",
|
|
247
|
+
label: "Codex Desktop",
|
|
248
|
+
restartMode: "restart-session",
|
|
249
|
+
canWriteRuntimeConfig: true
|
|
250
|
+
},
|
|
251
|
+
cursor: {
|
|
252
|
+
id: "cursor",
|
|
253
|
+
label: "Cursor",
|
|
254
|
+
restartMode: "hot-reload",
|
|
255
|
+
canWriteRuntimeConfig: true
|
|
256
|
+
},
|
|
257
|
+
vscode: {
|
|
258
|
+
id: "vscode",
|
|
259
|
+
label: "VS Code",
|
|
260
|
+
restartMode: "hot-reload",
|
|
261
|
+
canWriteRuntimeConfig: true
|
|
262
|
+
},
|
|
263
|
+
"vscode-insiders": {
|
|
264
|
+
id: "vscode-insiders",
|
|
265
|
+
label: "VS Code Insiders",
|
|
266
|
+
restartMode: "hot-reload",
|
|
267
|
+
canWriteRuntimeConfig: true
|
|
268
|
+
},
|
|
269
|
+
"claude-desktop": {
|
|
270
|
+
id: "claude-desktop",
|
|
271
|
+
label: "Claude Desktop",
|
|
272
|
+
restartMode: "restart-app",
|
|
273
|
+
canWriteRuntimeConfig: true
|
|
274
|
+
},
|
|
275
|
+
other: {
|
|
276
|
+
id: "other",
|
|
277
|
+
label: "Other agent runtime",
|
|
278
|
+
restartMode: "manual",
|
|
279
|
+
canWriteRuntimeConfig: false
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
var RUNTIME_ALIASES = {
|
|
283
|
+
claude: "claude-code",
|
|
284
|
+
"claude-code": "claude-code",
|
|
285
|
+
claudecode: "claude-code",
|
|
286
|
+
"claude_code": "claude-code",
|
|
287
|
+
codex: "codex-cli",
|
|
288
|
+
"codex-cli": "codex-cli",
|
|
289
|
+
codexcli: "codex-cli",
|
|
290
|
+
"codex_cli": "codex-cli",
|
|
291
|
+
"codex-desktop": "codex-desktop",
|
|
292
|
+
"codex_desktop": "codex-desktop",
|
|
293
|
+
codexdesktop: "codex-desktop",
|
|
294
|
+
"codex-app": "codex-desktop",
|
|
295
|
+
"codex_app": "codex-desktop",
|
|
296
|
+
codexapp: "codex-desktop",
|
|
297
|
+
cursor: "cursor",
|
|
298
|
+
vscode: "vscode",
|
|
299
|
+
"vs-code": "vscode",
|
|
300
|
+
"vs_code": "vscode",
|
|
301
|
+
code: "vscode",
|
|
302
|
+
"vscode-insiders": "vscode-insiders",
|
|
303
|
+
"vscode_insiders": "vscode-insiders",
|
|
304
|
+
vscodeinsiders: "vscode-insiders",
|
|
305
|
+
"vs-code-insiders": "vscode-insiders",
|
|
306
|
+
"code-insiders": "vscode-insiders",
|
|
307
|
+
insiders: "vscode-insiders",
|
|
308
|
+
"claude-desktop": "claude-desktop",
|
|
309
|
+
"claude_desktop": "claude-desktop",
|
|
310
|
+
claudesktop: "claude-desktop",
|
|
311
|
+
desktop: "claude-desktop",
|
|
312
|
+
other: "other",
|
|
313
|
+
manual: "other"
|
|
314
|
+
};
|
|
315
|
+
function runtimeProfile(runtime, env = process.env) {
|
|
316
|
+
return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
|
|
317
|
+
}
|
|
318
|
+
function normalizeRuntime(runtime, env = process.env) {
|
|
319
|
+
const explicit = normalizeRuntimeName(runtime);
|
|
320
|
+
if (explicit) return explicit;
|
|
321
|
+
return detectRuntime(env) ?? "other";
|
|
322
|
+
}
|
|
323
|
+
function restartRequiredForRuntime(runtime, env = process.env) {
|
|
324
|
+
const mode = runtimeProfile(runtime, env).restartMode;
|
|
325
|
+
return mode === "restart-session" || mode === "restart-app";
|
|
326
|
+
}
|
|
327
|
+
function runtimeRequiresHardRestart(runtime) {
|
|
328
|
+
return runtime === "claude-desktop" || runtime === "codex-desktop";
|
|
329
|
+
}
|
|
330
|
+
function normalizeRuntimeName(runtime) {
|
|
331
|
+
const key = runtime?.trim().toLowerCase();
|
|
332
|
+
if (!key) return null;
|
|
333
|
+
return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
|
|
334
|
+
}
|
|
335
|
+
function detectRuntime(env) {
|
|
336
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
|
|
337
|
+
if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
|
|
338
|
+
if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/config-writers.ts
|
|
343
|
+
var InvalidCodexTomlError = class extends Error {
|
|
344
|
+
constructor(message) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.name = "InvalidCodexTomlError";
|
|
347
|
+
}
|
|
348
|
+
};
|
|
210
349
|
async function writeRuntimeConfig(input) {
|
|
211
350
|
switch (input.runtime) {
|
|
212
351
|
case "codex-cli":
|
|
352
|
+
case "codex-desktop":
|
|
213
353
|
return writeCodexConfig(input);
|
|
214
354
|
case "cursor":
|
|
215
355
|
return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
|
|
216
356
|
case "vscode":
|
|
217
357
|
return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
|
|
358
|
+
case "vscode-insiders":
|
|
359
|
+
return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
|
|
218
360
|
case "claude-desktop":
|
|
219
361
|
return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
|
|
220
362
|
default:
|
|
@@ -232,7 +374,7 @@ async function writeRuntimeConfig(input) {
|
|
|
232
374
|
}
|
|
233
375
|
}
|
|
234
376
|
function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
|
|
235
|
-
if (runtime === "vscode") {
|
|
377
|
+
if (runtime === "vscode" || runtime === "vscode-insiders") {
|
|
236
378
|
return {
|
|
237
379
|
type: "http",
|
|
238
380
|
url: hostedMcpUrl,
|
|
@@ -249,7 +391,7 @@ function buildSignerServer(signerPath, runtime) {
|
|
|
249
391
|
command: "npx",
|
|
250
392
|
args: ["-y", signerPackageName(), "--credentials", signerPath]
|
|
251
393
|
};
|
|
252
|
-
if (runtime === "vscode") return { type: "stdio", ...server };
|
|
394
|
+
if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
|
|
253
395
|
return server;
|
|
254
396
|
}
|
|
255
397
|
function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
|
|
@@ -264,18 +406,21 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
|
|
|
264
406
|
return `${JSON.stringify(config, null, 2)}
|
|
265
407
|
`;
|
|
266
408
|
}
|
|
267
|
-
function mergeCodexToml(existingToml,
|
|
268
|
-
let next =
|
|
409
|
+
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
410
|
+
let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
|
|
269
411
|
next = next.trimEnd();
|
|
270
412
|
const block = [
|
|
271
413
|
"[mcp_servers.haven]",
|
|
272
|
-
|
|
273
|
-
|
|
414
|
+
`command = ${tomlString(localMcpCommand)}`,
|
|
415
|
+
"args = []",
|
|
416
|
+
"startup_timeout_sec = 120"
|
|
274
417
|
].join("\n");
|
|
275
|
-
|
|
418
|
+
validateCodexToml(block, "Generated Codex Haven config");
|
|
419
|
+
const merged = `${next ? `${next}
|
|
276
420
|
|
|
277
421
|
` : ""}${block}
|
|
278
422
|
`;
|
|
423
|
+
return merged;
|
|
279
424
|
}
|
|
280
425
|
async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
281
426
|
try {
|
|
@@ -315,32 +460,39 @@ async function writeCodexConfig(input) {
|
|
|
315
460
|
const target = codexConfigPath(input.homeDir);
|
|
316
461
|
try {
|
|
317
462
|
const existing = await readOptional(target);
|
|
318
|
-
|
|
463
|
+
if (!input.localMcpCommand) {
|
|
464
|
+
throw new Error("local MCP wrapper command is required");
|
|
465
|
+
}
|
|
466
|
+
const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
319
467
|
await writeOwnerOnlyText(target, merged);
|
|
320
468
|
return {
|
|
321
469
|
hostedConfigured: false,
|
|
322
470
|
signerConfigured: true,
|
|
323
471
|
localMcpConfigured: true,
|
|
324
472
|
runtimeMcpMode: "local_stdio",
|
|
325
|
-
target:
|
|
473
|
+
target: configTargetLabel(input.runtime),
|
|
326
474
|
changed: existing !== merged,
|
|
327
475
|
restartRequired: true,
|
|
328
476
|
messages: [
|
|
329
|
-
|
|
330
|
-
|
|
477
|
+
`Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
|
|
478
|
+
// Codex Desktop loads MCP servers at app launch; Codex CLI typically
|
|
479
|
+
// picks them up in the next session. Branch the copy so desktop users
|
|
480
|
+
// get the unambiguous instruction.
|
|
481
|
+
runtimeRequiresHardRestart(input.runtime) ? "After Haven approval, restart Codex Desktop so it can load Haven tools." : "After Haven approval, Haven tools should appear in your next Codex message. If they don't, restart Codex to load them."
|
|
331
482
|
]
|
|
332
483
|
};
|
|
333
484
|
} catch (err) {
|
|
485
|
+
const invalidToml = err instanceof InvalidCodexTomlError;
|
|
334
486
|
return {
|
|
335
487
|
hostedConfigured: false,
|
|
336
488
|
signerConfigured: false,
|
|
337
489
|
localMcpConfigured: false,
|
|
338
490
|
runtimeMcpMode: "local_stdio",
|
|
339
|
-
target:
|
|
491
|
+
target: configTargetLabel(input.runtime),
|
|
340
492
|
changed: false,
|
|
341
493
|
restartRequired: true,
|
|
342
|
-
messages: [`Could not update
|
|
343
|
-
errorCode: "runtime_config_write_failed"
|
|
494
|
+
messages: [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
495
|
+
errorCode: invalidToml ? "codex_config_invalid" : "runtime_config_write_failed"
|
|
344
496
|
};
|
|
345
497
|
}
|
|
346
498
|
}
|
|
@@ -364,24 +516,191 @@ function parseJsonObject(value) {
|
|
|
364
516
|
}
|
|
365
517
|
return parsed;
|
|
366
518
|
}
|
|
367
|
-
function
|
|
519
|
+
function removeTomlTableTree(toml, table) {
|
|
368
520
|
const lines = toml.split(/\r?\n/);
|
|
369
|
-
const start = `[${table}]`;
|
|
370
521
|
const kept = [];
|
|
371
522
|
let skipping = false;
|
|
372
523
|
for (const line of lines) {
|
|
373
524
|
const trimmed = line.trim();
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (skipping && trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
379
|
-
skipping = false;
|
|
525
|
+
const tableName = tomlTableName(trimmed);
|
|
526
|
+
if (tableName) {
|
|
527
|
+
skipping = tableName === table || tableName.startsWith(`${table}.`);
|
|
528
|
+
if (skipping) continue;
|
|
380
529
|
}
|
|
381
530
|
if (!skipping) kept.push(line);
|
|
382
531
|
}
|
|
383
532
|
return kept.join("\n");
|
|
384
533
|
}
|
|
534
|
+
function tomlTableName(line) {
|
|
535
|
+
if (line.startsWith("[[") && line.endsWith("]]")) return line.slice(2, -2).trim();
|
|
536
|
+
if (line.startsWith("[") && line.endsWith("]")) return line.slice(1, -1).trim();
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function validateCodexToml(toml, label = "Codex config") {
|
|
540
|
+
const lines = toml.split(/\r?\n/);
|
|
541
|
+
let pendingValue = null;
|
|
542
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
543
|
+
const raw = lines[index];
|
|
544
|
+
const line = stripTomlComment(raw).trim();
|
|
545
|
+
if (!line) continue;
|
|
546
|
+
if (pendingValue) {
|
|
547
|
+
pendingValue.value = `${pendingValue.value}
|
|
548
|
+
${line}`;
|
|
549
|
+
if (hasBalancedTomlContainers(pendingValue.value)) {
|
|
550
|
+
if (!isTomlValue(pendingValue.value)) {
|
|
551
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
|
|
552
|
+
}
|
|
553
|
+
pendingValue = null;
|
|
554
|
+
}
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (isTomlTable(line)) continue;
|
|
558
|
+
const equalsIndex = line.indexOf("=");
|
|
559
|
+
if (equalsIndex <= 0) {
|
|
560
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
561
|
+
}
|
|
562
|
+
const key = line.slice(0, equalsIndex).trim();
|
|
563
|
+
const value = line.slice(equalsIndex + 1).trim();
|
|
564
|
+
if (!isTomlKey(key)) {
|
|
565
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
566
|
+
}
|
|
567
|
+
if (startsTomlContainer(value) && !hasBalancedTomlContainers(value)) {
|
|
568
|
+
pendingValue = { value, line: index + 1 };
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (!isTomlValue(value)) {
|
|
572
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (pendingValue) {
|
|
576
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function isTomlTable(line) {
|
|
580
|
+
const table = tomlTableName(line);
|
|
581
|
+
return Boolean(table && splitTomlDottedKey(table).every(isTomlKeyPart));
|
|
582
|
+
}
|
|
583
|
+
function isTomlKey(value) {
|
|
584
|
+
return splitTomlDottedKey(value).every(isTomlKeyPart);
|
|
585
|
+
}
|
|
586
|
+
function splitTomlDottedKey(value) {
|
|
587
|
+
const parts = [];
|
|
588
|
+
let current = "";
|
|
589
|
+
let quote = null;
|
|
590
|
+
let escaped = false;
|
|
591
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
592
|
+
const char = value[i];
|
|
593
|
+
if (quote) {
|
|
594
|
+
current += char;
|
|
595
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
596
|
+
escaped = true;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (char === quote && !escaped) quote = null;
|
|
600
|
+
escaped = false;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (char === '"' || char === "'") {
|
|
604
|
+
quote = char;
|
|
605
|
+
current += char;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (char === ".") {
|
|
609
|
+
parts.push(current.trim());
|
|
610
|
+
current = "";
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
current += char;
|
|
614
|
+
}
|
|
615
|
+
parts.push(current.trim());
|
|
616
|
+
return quote ? [] : parts;
|
|
617
|
+
}
|
|
618
|
+
function isTomlKeyPart(value) {
|
|
619
|
+
return isTomlBareKey(value) || isTomlQuotedString(value);
|
|
620
|
+
}
|
|
621
|
+
function isTomlBareKey(value) {
|
|
622
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
623
|
+
}
|
|
624
|
+
function isTomlValue(value) {
|
|
625
|
+
if (!value) return false;
|
|
626
|
+
if (isTomlQuotedString(value)) return true;
|
|
627
|
+
if (/^(true|false)$/i.test(value)) return true;
|
|
628
|
+
if (/^[+-]?(?:inf|nan)$/i.test(value)) return true;
|
|
629
|
+
if (/^[+-]?(?:0|[1-9][0-9_]*)(?:\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?$/.test(value)) return true;
|
|
630
|
+
if (/^\d{4}-\d{2}-\d{2}(?:[Tt ][0-9:.+-Zz]+)?$/.test(value)) return true;
|
|
631
|
+
if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
|
|
632
|
+
return hasBalancedTomlContainers(value);
|
|
633
|
+
}
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
function startsTomlContainer(value) {
|
|
637
|
+
return value.startsWith("[") || value.startsWith("{");
|
|
638
|
+
}
|
|
639
|
+
function isTomlQuotedString(value) {
|
|
640
|
+
if (value.startsWith('"""') || value.startsWith("'''")) {
|
|
641
|
+
const marker = value.slice(0, 3);
|
|
642
|
+
return value.length >= 6 && value.endsWith(marker);
|
|
643
|
+
}
|
|
644
|
+
if ((!value.startsWith('"') || !value.endsWith('"')) && (!value.startsWith("'") || !value.endsWith("'"))) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
return hasBalancedTomlContainers(value);
|
|
648
|
+
}
|
|
649
|
+
function stripTomlComment(value) {
|
|
650
|
+
let quote = null;
|
|
651
|
+
let escaped = false;
|
|
652
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
653
|
+
const char = value[i];
|
|
654
|
+
if (quote) {
|
|
655
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
656
|
+
escaped = true;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (char === quote && !escaped) quote = null;
|
|
660
|
+
escaped = false;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (char === '"' || char === "'") {
|
|
664
|
+
quote = char;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (char === "#") return value.slice(0, i);
|
|
668
|
+
}
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
function hasBalancedTomlContainers(value) {
|
|
672
|
+
const stack = [];
|
|
673
|
+
let quote = null;
|
|
674
|
+
let escaped = false;
|
|
675
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
676
|
+
const char = value[i];
|
|
677
|
+
if (quote) {
|
|
678
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
679
|
+
escaped = true;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (char === quote && !escaped) quote = null;
|
|
683
|
+
escaped = false;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (char === '"' || char === "'") {
|
|
687
|
+
quote = char;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (char === "[" || char === "{") {
|
|
691
|
+
stack.push(char);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (char === "]") {
|
|
695
|
+
if (stack.pop() !== "[") return false;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (char === "}") {
|
|
699
|
+
if (stack.pop() !== "{") return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return stack.length === 0 && quote === null;
|
|
703
|
+
}
|
|
385
704
|
function tomlString(value) {
|
|
386
705
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
387
706
|
}
|
|
@@ -398,6 +717,13 @@ function vscodeConfigPath(homeDir = os.homedir()) {
|
|
|
398
717
|
}
|
|
399
718
|
return path.resolve(homeDir, ".config", "Code", "User", "mcp.json");
|
|
400
719
|
}
|
|
720
|
+
function vscodeInsidersConfigPath(homeDir = os.homedir()) {
|
|
721
|
+
if (os.platform() === "darwin") return path.resolve(homeDir, "Library", "Application Support", "Code - Insiders", "User", "mcp.json");
|
|
722
|
+
if (os.platform() === "win32") {
|
|
723
|
+
return path.resolve(process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming"), "Code - Insiders", "User", "mcp.json");
|
|
724
|
+
}
|
|
725
|
+
return path.resolve(homeDir, ".config", "Code - Insiders", "User", "mcp.json");
|
|
726
|
+
}
|
|
401
727
|
function claudeDesktopConfigPath(homeDir = os.homedir()) {
|
|
402
728
|
if (os.platform() === "darwin") {
|
|
403
729
|
return path.resolve(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
@@ -409,10 +735,16 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
|
|
|
409
735
|
}
|
|
410
736
|
function configTargetLabel(runtime) {
|
|
411
737
|
switch (runtime) {
|
|
738
|
+
case "codex-cli":
|
|
739
|
+
return "Codex CLI config";
|
|
740
|
+
case "codex-desktop":
|
|
741
|
+
return "Codex Desktop config";
|
|
412
742
|
case "cursor":
|
|
413
743
|
return "Cursor MCP config";
|
|
414
744
|
case "vscode":
|
|
415
745
|
return "VS Code MCP config";
|
|
746
|
+
case "vscode-insiders":
|
|
747
|
+
return "VS Code Insiders MCP config";
|
|
416
748
|
case "claude-desktop":
|
|
417
749
|
return "Claude Desktop config";
|
|
418
750
|
default:
|
|
@@ -420,10 +752,7 @@ function configTargetLabel(runtime) {
|
|
|
420
752
|
}
|
|
421
753
|
}
|
|
422
754
|
function signerPackageName() {
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
function localMcpPackageName() {
|
|
426
|
-
return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
|
|
755
|
+
return signerPackageSpec();
|
|
427
756
|
}
|
|
428
757
|
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
429
758
|
try {
|
|
@@ -537,6 +866,72 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
537
866
|
return false;
|
|
538
867
|
}
|
|
539
868
|
}
|
|
869
|
+
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
870
|
+
return new Promise((resolve6) => {
|
|
871
|
+
const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
872
|
+
let stdout = "";
|
|
873
|
+
let settled = false;
|
|
874
|
+
let sawInitialize = false;
|
|
875
|
+
const finish = (result) => {
|
|
876
|
+
if (settled) return;
|
|
877
|
+
settled = true;
|
|
878
|
+
clearTimeout(timeout);
|
|
879
|
+
child.kill();
|
|
880
|
+
resolve6(result);
|
|
881
|
+
};
|
|
882
|
+
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
883
|
+
child.on("error", () => finish({ status: "process_error" }));
|
|
884
|
+
child.on("exit", (code) => {
|
|
885
|
+
if (!settled && code !== 0) finish({ status: "process_error" });
|
|
886
|
+
});
|
|
887
|
+
child.stdout.on("data", (chunk) => {
|
|
888
|
+
stdout += chunk.toString("utf8");
|
|
889
|
+
const lines = stdout.split(/\r?\n/);
|
|
890
|
+
stdout = lines.pop() ?? "";
|
|
891
|
+
for (const line of lines) {
|
|
892
|
+
const trimmed = line.trim();
|
|
893
|
+
if (!trimmed) continue;
|
|
894
|
+
let payload;
|
|
895
|
+
try {
|
|
896
|
+
payload = JSON.parse(trimmed);
|
|
897
|
+
} catch {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (payload.error) {
|
|
901
|
+
finish({ status: "bad_response" });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (payload.id === 1 && !sawInitialize) {
|
|
905
|
+
sawInitialize = true;
|
|
906
|
+
writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
907
|
+
writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (payload.id === 2) {
|
|
911
|
+
const tools = payload.result?.tools;
|
|
912
|
+
const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
|
|
913
|
+
const missing = requiredTools.filter((name) => !toolNames.includes(name));
|
|
914
|
+
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
writeJsonRpc(child, {
|
|
920
|
+
jsonrpc: "2.0",
|
|
921
|
+
id: 1,
|
|
922
|
+
method: "initialize",
|
|
923
|
+
params: {
|
|
924
|
+
protocolVersion: "2025-06-18",
|
|
925
|
+
capabilities: {},
|
|
926
|
+
clientInfo: { name: "haven-connect-probe", version: "0.0.0" }
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
function writeJsonRpc(child, payload) {
|
|
932
|
+
child.stdin?.write(`${JSON.stringify(payload)}
|
|
933
|
+
`);
|
|
934
|
+
}
|
|
540
935
|
function parseJsonRpcPayload(raw) {
|
|
541
936
|
const trimmed = raw.trim();
|
|
542
937
|
if (!trimmed) return null;
|
|
@@ -565,89 +960,160 @@ async function fetchWithTimeout(fetchImpl, url, init) {
|
|
|
565
960
|
clearTimeout(timeout);
|
|
566
961
|
}
|
|
567
962
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
restartMode: "restart-session",
|
|
575
|
-
canWriteRuntimeConfig: true
|
|
576
|
-
},
|
|
577
|
-
"codex-cli": {
|
|
578
|
-
id: "codex-cli",
|
|
579
|
-
label: "Codex CLI",
|
|
580
|
-
restartMode: "restart-session",
|
|
581
|
-
canWriteRuntimeConfig: true
|
|
582
|
-
},
|
|
583
|
-
cursor: {
|
|
584
|
-
id: "cursor",
|
|
585
|
-
label: "Cursor",
|
|
586
|
-
restartMode: "hot-reload",
|
|
587
|
-
canWriteRuntimeConfig: true
|
|
588
|
-
},
|
|
589
|
-
vscode: {
|
|
590
|
-
id: "vscode",
|
|
591
|
-
label: "VS Code",
|
|
592
|
-
restartMode: "hot-reload",
|
|
593
|
-
canWriteRuntimeConfig: true
|
|
594
|
-
},
|
|
595
|
-
"claude-desktop": {
|
|
596
|
-
id: "claude-desktop",
|
|
597
|
-
label: "Claude Desktop",
|
|
598
|
-
restartMode: "restart-app",
|
|
599
|
-
canWriteRuntimeConfig: true
|
|
600
|
-
},
|
|
601
|
-
other: {
|
|
602
|
-
id: "other",
|
|
603
|
-
label: "Other agent runtime",
|
|
604
|
-
restartMode: "manual",
|
|
605
|
-
canWriteRuntimeConfig: false
|
|
963
|
+
var execFileAsync = util.promisify(child_process.execFile);
|
|
964
|
+
var UnsupportedNodeVersionError = class extends Error {
|
|
965
|
+
code = "local_mcp_unsupported_node_version";
|
|
966
|
+
constructor(nodeVersion, minimumNodeVersion) {
|
|
967
|
+
super(`Node.js ${nodeVersion} is not supported. Haven local MCP requires Node.js >=${minimumNodeVersion}.`);
|
|
968
|
+
this.name = "UnsupportedNodeVersionError";
|
|
606
969
|
}
|
|
607
970
|
};
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
"
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
971
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
972
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
973
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
974
|
+
const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
975
|
+
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
976
|
+
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
977
|
+
const messages = [];
|
|
978
|
+
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
979
|
+
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
980
|
+
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
981
|
+
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
982
|
+
if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
983
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
984
|
+
} else {
|
|
985
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps.runCommand);
|
|
986
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
987
|
+
}
|
|
988
|
+
await assertFileExists(cliPath, "local Haven MCP CLI");
|
|
989
|
+
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
|
|
990
|
+
await writeWrapper({
|
|
991
|
+
wrapperPath,
|
|
992
|
+
cliPath,
|
|
993
|
+
identityPath: input.identityPath,
|
|
994
|
+
signerPath: input.signerPath
|
|
995
|
+
});
|
|
996
|
+
await writeRuntimeSidecar({
|
|
997
|
+
path: path.join(input.credentialDirectory, "mcp-runtime.json"),
|
|
998
|
+
wrapperPath,
|
|
999
|
+
runtimeDirectory,
|
|
1000
|
+
npmCacheDirectory,
|
|
1001
|
+
cliPath
|
|
1002
|
+
});
|
|
1003
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1004
|
+
return {
|
|
1005
|
+
command: wrapperPath,
|
|
1006
|
+
args: [],
|
|
1007
|
+
wrapperPath,
|
|
1008
|
+
runtimeDirectory,
|
|
1009
|
+
npmCacheDirectory,
|
|
1010
|
+
cliPath,
|
|
1011
|
+
messages
|
|
1012
|
+
};
|
|
631
1013
|
}
|
|
632
|
-
function
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1014
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion) {
|
|
1015
|
+
if (compareNodeVersions(nodeVersion, minimumNodeVersion) < 0) {
|
|
1016
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion);
|
|
1017
|
+
}
|
|
636
1018
|
}
|
|
637
|
-
function
|
|
638
|
-
const
|
|
639
|
-
|
|
1019
|
+
function compareNodeVersions(left, right) {
|
|
1020
|
+
const leftParts = parseNodeVersion(left);
|
|
1021
|
+
const rightParts = parseNodeVersion(right);
|
|
1022
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1023
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
1024
|
+
}
|
|
1025
|
+
return 0;
|
|
640
1026
|
}
|
|
641
|
-
function
|
|
642
|
-
const
|
|
643
|
-
if (!
|
|
644
|
-
return
|
|
1027
|
+
function parseNodeVersion(value) {
|
|
1028
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
1029
|
+
if (!match) return [0, 0, 0];
|
|
1030
|
+
return [
|
|
1031
|
+
Number(match[1] ?? 0),
|
|
1032
|
+
Number(match[2] ?? 0),
|
|
1033
|
+
Number(match[3] ?? 0)
|
|
1034
|
+
];
|
|
1035
|
+
}
|
|
1036
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
|
|
1037
|
+
const args = [
|
|
1038
|
+
"install",
|
|
1039
|
+
"--prefix",
|
|
1040
|
+
runtimeDirectory,
|
|
1041
|
+
"--cache",
|
|
1042
|
+
npmCacheDirectory,
|
|
1043
|
+
"--no-audit",
|
|
1044
|
+
"--no-fund",
|
|
1045
|
+
"--omit=dev",
|
|
1046
|
+
mcpPackageSpec(),
|
|
1047
|
+
sdkPackageSpec()
|
|
1048
|
+
];
|
|
1049
|
+
try {
|
|
1050
|
+
if (runCommand) await runCommand("npm", args);
|
|
1051
|
+
else await execFileAsync("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1054
|
+
}
|
|
645
1055
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1056
|
+
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
1057
|
+
try {
|
|
1058
|
+
await assertFileExists(cliPath, "local Haven MCP CLI");
|
|
1059
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
1060
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
1061
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1062
|
+
]);
|
|
1063
|
+
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function readPackageJson(path) {
|
|
1069
|
+
return JSON.parse(await promises.readFile(path, "utf8"));
|
|
1070
|
+
}
|
|
1071
|
+
async function writeWrapper(input) {
|
|
1072
|
+
await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
1073
|
+
await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
1074
|
+
const source = [
|
|
1075
|
+
"#!/usr/bin/env node",
|
|
1076
|
+
"import { spawn } from 'node:child_process'",
|
|
1077
|
+
"",
|
|
1078
|
+
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
1079
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
1080
|
+
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
1081
|
+
"",
|
|
1082
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
1083
|
+
" stdio: 'inherit',",
|
|
1084
|
+
"})",
|
|
1085
|
+
"",
|
|
1086
|
+
"child.on('exit', (code, signal) => {",
|
|
1087
|
+
" if (signal) process.kill(process.pid, signal)",
|
|
1088
|
+
" else process.exit(code ?? 1)",
|
|
1089
|
+
"})",
|
|
1090
|
+
""
|
|
1091
|
+
].join("\n");
|
|
1092
|
+
await promises.writeFile(input.wrapperPath, source, { mode: 448 });
|
|
1093
|
+
await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
1094
|
+
}
|
|
1095
|
+
async function writeRuntimeSidecar(input) {
|
|
1096
|
+
const value = {
|
|
1097
|
+
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1098
|
+
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1099
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1100
|
+
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1101
|
+
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
1102
|
+
wrapper_path: input.wrapperPath,
|
|
1103
|
+
runtime_directory: input.runtimeDirectory,
|
|
1104
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
1105
|
+
cli_path: input.cliPath
|
|
1106
|
+
};
|
|
1107
|
+
await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1108
|
+
`, { mode: 384 });
|
|
1109
|
+
await promises.chmod(input.path, 384).catch(() => void 0);
|
|
1110
|
+
}
|
|
1111
|
+
async function assertFileExists(path, label) {
|
|
1112
|
+
try {
|
|
1113
|
+
await promises.access(path);
|
|
1114
|
+
} catch {
|
|
1115
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1116
|
+
}
|
|
651
1117
|
}
|
|
652
1118
|
async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
653
1119
|
try {
|
|
@@ -720,7 +1186,7 @@ function writeLogChunk2(log, chunk) {
|
|
|
720
1186
|
}
|
|
721
1187
|
|
|
722
1188
|
// src/runtime-install.ts
|
|
723
|
-
var
|
|
1189
|
+
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
724
1190
|
async function installRuntime(input, deps = {}) {
|
|
725
1191
|
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
726
1192
|
const profile = runtimeProfile(runtime, deps.env);
|
|
@@ -750,31 +1216,66 @@ async function installRuntime(input, deps = {}) {
|
|
|
750
1216
|
]
|
|
751
1217
|
};
|
|
752
1218
|
}
|
|
753
|
-
|
|
1219
|
+
let localRuntimeInstall;
|
|
1220
|
+
let localRuntimeError;
|
|
1221
|
+
if (localRuntime) {
|
|
1222
|
+
try {
|
|
1223
|
+
localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
localRuntimeError = err;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (localRuntimeError) {
|
|
1229
|
+
const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
|
|
1230
|
+
return {
|
|
1231
|
+
runtime,
|
|
1232
|
+
runtimeMcpMode: "local_stdio",
|
|
1233
|
+
hostedMcpConfigured: false,
|
|
1234
|
+
localSignerConfigured: false,
|
|
1235
|
+
localMcpConfigured: false,
|
|
1236
|
+
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
1237
|
+
restartRequired: true,
|
|
1238
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
1239
|
+
errorCode: errorCode2,
|
|
1240
|
+
configTarget: profile.label,
|
|
1241
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
1242
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
1243
|
+
activationCommand: void 0,
|
|
1244
|
+
messages: [
|
|
1245
|
+
...consentMessages,
|
|
1246
|
+
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
1247
|
+
]
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
|
|
754
1251
|
runtime,
|
|
755
1252
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
756
1253
|
apiKey: input.apiKey,
|
|
757
1254
|
identityPath: input.identityPath,
|
|
758
1255
|
signerPath: input.signerPath,
|
|
759
1256
|
credentialDirectory: input.credentialDirectory,
|
|
1257
|
+
localMcpCommand: localRuntimeInstall?.command,
|
|
760
1258
|
homeDir: deps.homeDir
|
|
761
1259
|
});
|
|
762
|
-
const
|
|
1260
|
+
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
1261
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
|
|
763
1262
|
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
764
|
-
probeLocalSignerCredential(input.signerPath)
|
|
1263
|
+
probeLocalSignerCredential(input.signerPath),
|
|
1264
|
+
localProbePromise
|
|
765
1265
|
]);
|
|
766
1266
|
const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
|
|
767
|
-
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged);
|
|
1267
|
+
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
768
1268
|
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
|
|
769
1269
|
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
770
|
-
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ?
|
|
1270
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
1271
|
+
const localProbeMessages = localMcpProbe && localMcpProbe.status !== "ok" ? [`Local Haven MCP handshake failed: ${localMcpProbe.status}.`] : localMcpProbe?.status === "ok" ? ["Verified local Haven MCP tools with a stdio handshake."] : [];
|
|
771
1272
|
return {
|
|
772
1273
|
runtime,
|
|
773
1274
|
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
774
1275
|
hostedMcpConfigured: hostedOk,
|
|
775
1276
|
localSignerConfigured: signerOk,
|
|
776
1277
|
localMcpConfigured: localMcpOk,
|
|
777
|
-
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk),
|
|
1278
|
+
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
|
|
778
1279
|
restartRequired,
|
|
779
1280
|
nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
|
|
780
1281
|
errorCode,
|
|
@@ -782,7 +1283,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
782
1283
|
signerAcknowledged: signerConsent?.acknowledged,
|
|
783
1284
|
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
784
1285
|
activationCommand: configResult.activationCommand,
|
|
785
|
-
messages: [...consentMessages, ...configResult.messages]
|
|
1286
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
|
|
786
1287
|
};
|
|
787
1288
|
}
|
|
788
1289
|
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
@@ -792,23 +1293,21 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
792
1293
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
793
1294
|
};
|
|
794
1295
|
}
|
|
795
|
-
async function configureClaudeCode(
|
|
1296
|
+
async function configureClaudeCode(deps, localMcpCommand) {
|
|
796
1297
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
1298
|
+
const serverJson = JSON.stringify({
|
|
1299
|
+
type: "stdio",
|
|
1300
|
+
command: localMcpCommand,
|
|
1301
|
+
args: [],
|
|
1302
|
+
env: {}
|
|
1303
|
+
});
|
|
797
1304
|
try {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
"add",
|
|
801
|
-
|
|
802
|
-
"--",
|
|
803
|
-
"npx",
|
|
804
|
-
"-y",
|
|
805
|
-
localMcpPackageName2(),
|
|
806
|
-
"--identity",
|
|
807
|
-
input.identityPath,
|
|
808
|
-
"--signer",
|
|
809
|
-
input.signerPath
|
|
810
|
-
]);
|
|
1305
|
+
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
1306
|
+
await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
|
|
1307
|
+
await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
|
|
1308
|
+
});
|
|
811
1309
|
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
1310
|
+
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
812
1311
|
return {
|
|
813
1312
|
hostedConfigured: false,
|
|
814
1313
|
signerConfigured: true,
|
|
@@ -819,7 +1318,8 @@ async function configureClaudeCode(input, deps) {
|
|
|
819
1318
|
restartRequired: true,
|
|
820
1319
|
messages: [
|
|
821
1320
|
"Updated local Haven MCP entry with Claude Code.",
|
|
822
|
-
|
|
1321
|
+
...verified ? ["Verified Claude Code MCP entry."] : [],
|
|
1322
|
+
"After Haven approval, Haven tools should appear in your next Claude Code message. If they don't, restart the session to load them."
|
|
823
1323
|
]
|
|
824
1324
|
};
|
|
825
1325
|
} catch (err) {
|
|
@@ -840,11 +1340,12 @@ async function configureClaudeCode(input, deps) {
|
|
|
840
1340
|
}
|
|
841
1341
|
}
|
|
842
1342
|
async function defaultRunCommand(command, args) {
|
|
843
|
-
await
|
|
1343
|
+
await execFileAsync2(command, args, { timeout: 1e4 });
|
|
844
1344
|
}
|
|
845
|
-
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady) {
|
|
1345
|
+
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
|
|
846
1346
|
if (mode === "local_stdio") {
|
|
847
|
-
|
|
1347
|
+
if (localMcpReady) return "local_stdio_mcp_ready";
|
|
1348
|
+
return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
|
|
848
1349
|
}
|
|
849
1350
|
const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
|
|
850
1351
|
const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
|
|
@@ -879,25 +1380,46 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
|
879
1380
|
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
880
1381
|
return void 0;
|
|
881
1382
|
}
|
|
882
|
-
function
|
|
1383
|
+
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
883
1384
|
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
884
1385
|
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
1386
|
+
if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
|
|
885
1387
|
return void 0;
|
|
886
1388
|
}
|
|
887
1389
|
function nextAction(runtime, restartMode, errorCode) {
|
|
888
1390
|
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
889
1391
|
if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
|
|
890
|
-
if (runtime === "codex-cli") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
1392
|
+
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
891
1393
|
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
892
1394
|
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
893
1395
|
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
894
1396
|
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
895
1397
|
}
|
|
896
|
-
function localMcpPackageName2() {
|
|
897
|
-
return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
|
|
898
|
-
}
|
|
899
1398
|
function usesLocalMcp(runtime) {
|
|
900
|
-
return runtime === "codex-cli" || runtime === "claude-code";
|
|
1399
|
+
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
1400
|
+
}
|
|
1401
|
+
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
1402
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
|
|
1403
|
+
return prepare({
|
|
1404
|
+
credentialDirectory: input.credentialDirectory,
|
|
1405
|
+
identityPath: input.identityPath,
|
|
1406
|
+
signerPath: input.signerPath,
|
|
1407
|
+
homeDir: deps.homeDir
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
1411
|
+
const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
|
|
1412
|
+
try {
|
|
1413
|
+
return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
|
|
1414
|
+
} catch {
|
|
1415
|
+
return { status: "process_error" };
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function localRuntimePrepareErrorCode(err) {
|
|
1419
|
+
if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
|
|
1420
|
+
return "local_mcp_unsupported_node_version";
|
|
1421
|
+
}
|
|
1422
|
+
return "local_mcp_runtime_install_failed";
|
|
901
1423
|
}
|
|
902
1424
|
|
|
903
1425
|
// src/runtime.ts
|
|
@@ -997,7 +1519,11 @@ async function runConnect(options, deps = {}) {
|
|
|
997
1519
|
}
|
|
998
1520
|
log("Return to Haven to approve the agent rules.");
|
|
999
1521
|
if (runtimeInstall.restartRequired) {
|
|
1000
|
-
|
|
1522
|
+
if (runtimeRequiresHardRestart(runtimeInstall.runtime)) {
|
|
1523
|
+
log("After approval, restart this agent so it can load Haven tools.");
|
|
1524
|
+
} else {
|
|
1525
|
+
log("After approval, Haven tools should appear in your next message. If they don't, restart this agent to load them.");
|
|
1526
|
+
}
|
|
1001
1527
|
}
|
|
1002
1528
|
return {
|
|
1003
1529
|
setupId: registration.setup_id,
|
|
@@ -1096,7 +1622,7 @@ function helpText() {
|
|
|
1096
1622
|
"Options:",
|
|
1097
1623
|
" --setup <token> Short-lived setup token from Haven.",
|
|
1098
1624
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
1099
|
-
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, cursor, vscode, or claude-desktop.",
|
|
1625
|
+
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
|
|
1100
1626
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
1101
1627
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
1102
1628
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|