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