@haven_ai/connect 0.1.2-alpha → 0.1.3-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 +560 -55
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +563 -58
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +560 -55
- 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 +562 -57
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -207,9 +207,50 @@ 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.6",
|
|
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: [
|
|
220
|
+
"haven_quote_x402",
|
|
221
|
+
"haven_pay_x402_quote",
|
|
222
|
+
"haven_resume_x402_payment",
|
|
223
|
+
"haven_quote_mpp",
|
|
224
|
+
"haven_pay_mpp_challenge",
|
|
225
|
+
"haven_resume_mpp_payment",
|
|
226
|
+
"haven_get_payment_status",
|
|
227
|
+
"haven_get_resume_state",
|
|
228
|
+
"haven_get_agent",
|
|
229
|
+
"haven_get_allowances",
|
|
230
|
+
"haven_list_receipts"
|
|
231
|
+
]
|
|
232
|
+
};
|
|
233
|
+
function mcpPackageSpec() {
|
|
234
|
+
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
235
|
+
}
|
|
236
|
+
function sdkPackageSpec() {
|
|
237
|
+
return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
238
|
+
}
|
|
239
|
+
function signerPackageSpec() {
|
|
240
|
+
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/config-writers.ts
|
|
244
|
+
var InvalidCodexTomlError = class extends Error {
|
|
245
|
+
constructor(message) {
|
|
246
|
+
super(message);
|
|
247
|
+
this.name = "InvalidCodexTomlError";
|
|
248
|
+
}
|
|
249
|
+
};
|
|
210
250
|
async function writeRuntimeConfig(input) {
|
|
211
251
|
switch (input.runtime) {
|
|
212
252
|
case "codex-cli":
|
|
253
|
+
case "codex-desktop":
|
|
213
254
|
return writeCodexConfig(input);
|
|
214
255
|
case "cursor":
|
|
215
256
|
return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
|
|
@@ -264,18 +305,21 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
|
|
|
264
305
|
return `${JSON.stringify(config, null, 2)}
|
|
265
306
|
`;
|
|
266
307
|
}
|
|
267
|
-
function mergeCodexToml(existingToml,
|
|
268
|
-
let next =
|
|
308
|
+
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
309
|
+
let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
|
|
269
310
|
next = next.trimEnd();
|
|
270
311
|
const block = [
|
|
271
312
|
"[mcp_servers.haven]",
|
|
272
|
-
|
|
273
|
-
|
|
313
|
+
`command = ${tomlString(localMcpCommand)}`,
|
|
314
|
+
"args = []",
|
|
315
|
+
"startup_timeout_sec = 120"
|
|
274
316
|
].join("\n");
|
|
275
|
-
|
|
317
|
+
validateCodexToml(block, "Generated Codex Haven config");
|
|
318
|
+
const merged = `${next ? `${next}
|
|
276
319
|
|
|
277
320
|
` : ""}${block}
|
|
278
321
|
`;
|
|
322
|
+
return merged;
|
|
279
323
|
}
|
|
280
324
|
async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
281
325
|
try {
|
|
@@ -315,32 +359,36 @@ async function writeCodexConfig(input) {
|
|
|
315
359
|
const target = codexConfigPath(input.homeDir);
|
|
316
360
|
try {
|
|
317
361
|
const existing = await readOptional(target);
|
|
318
|
-
|
|
362
|
+
if (!input.localMcpCommand) {
|
|
363
|
+
throw new Error("local MCP wrapper command is required");
|
|
364
|
+
}
|
|
365
|
+
const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
319
366
|
await writeOwnerOnlyText(target, merged);
|
|
320
367
|
return {
|
|
321
368
|
hostedConfigured: false,
|
|
322
369
|
signerConfigured: true,
|
|
323
370
|
localMcpConfigured: true,
|
|
324
371
|
runtimeMcpMode: "local_stdio",
|
|
325
|
-
target:
|
|
372
|
+
target: configTargetLabel(input.runtime),
|
|
326
373
|
changed: existing !== merged,
|
|
327
374
|
restartRequired: true,
|
|
328
375
|
messages: [
|
|
329
|
-
|
|
376
|
+
`Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
|
|
330
377
|
"After Haven approval, restart Codex normally so it can load Haven tools."
|
|
331
378
|
]
|
|
332
379
|
};
|
|
333
380
|
} catch (err) {
|
|
381
|
+
const invalidToml = err instanceof InvalidCodexTomlError;
|
|
334
382
|
return {
|
|
335
383
|
hostedConfigured: false,
|
|
336
384
|
signerConfigured: false,
|
|
337
385
|
localMcpConfigured: false,
|
|
338
386
|
runtimeMcpMode: "local_stdio",
|
|
339
|
-
target:
|
|
387
|
+
target: configTargetLabel(input.runtime),
|
|
340
388
|
changed: false,
|
|
341
389
|
restartRequired: true,
|
|
342
|
-
messages: [`Could not update
|
|
343
|
-
errorCode: "runtime_config_write_failed"
|
|
390
|
+
messages: [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
391
|
+
errorCode: invalidToml ? "codex_config_invalid" : "runtime_config_write_failed"
|
|
344
392
|
};
|
|
345
393
|
}
|
|
346
394
|
}
|
|
@@ -364,24 +412,191 @@ function parseJsonObject(value) {
|
|
|
364
412
|
}
|
|
365
413
|
return parsed;
|
|
366
414
|
}
|
|
367
|
-
function
|
|
415
|
+
function removeTomlTableTree(toml, table) {
|
|
368
416
|
const lines = toml.split(/\r?\n/);
|
|
369
|
-
const start = `[${table}]`;
|
|
370
417
|
const kept = [];
|
|
371
418
|
let skipping = false;
|
|
372
419
|
for (const line of lines) {
|
|
373
420
|
const trimmed = line.trim();
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (skipping && trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
379
|
-
skipping = false;
|
|
421
|
+
const tableName = tomlTableName(trimmed);
|
|
422
|
+
if (tableName) {
|
|
423
|
+
skipping = tableName === table || tableName.startsWith(`${table}.`);
|
|
424
|
+
if (skipping) continue;
|
|
380
425
|
}
|
|
381
426
|
if (!skipping) kept.push(line);
|
|
382
427
|
}
|
|
383
428
|
return kept.join("\n");
|
|
384
429
|
}
|
|
430
|
+
function tomlTableName(line) {
|
|
431
|
+
if (line.startsWith("[[") && line.endsWith("]]")) return line.slice(2, -2).trim();
|
|
432
|
+
if (line.startsWith("[") && line.endsWith("]")) return line.slice(1, -1).trim();
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
function validateCodexToml(toml, label = "Codex config") {
|
|
436
|
+
const lines = toml.split(/\r?\n/);
|
|
437
|
+
let pendingValue = null;
|
|
438
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
439
|
+
const raw = lines[index];
|
|
440
|
+
const line = stripTomlComment(raw).trim();
|
|
441
|
+
if (!line) continue;
|
|
442
|
+
if (pendingValue) {
|
|
443
|
+
pendingValue.value = `${pendingValue.value}
|
|
444
|
+
${line}`;
|
|
445
|
+
if (hasBalancedTomlContainers(pendingValue.value)) {
|
|
446
|
+
if (!isTomlValue(pendingValue.value)) {
|
|
447
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
|
|
448
|
+
}
|
|
449
|
+
pendingValue = null;
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (isTomlTable(line)) continue;
|
|
454
|
+
const equalsIndex = line.indexOf("=");
|
|
455
|
+
if (equalsIndex <= 0) {
|
|
456
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
457
|
+
}
|
|
458
|
+
const key = line.slice(0, equalsIndex).trim();
|
|
459
|
+
const value = line.slice(equalsIndex + 1).trim();
|
|
460
|
+
if (!isTomlKey(key)) {
|
|
461
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
462
|
+
}
|
|
463
|
+
if (startsTomlContainer(value) && !hasBalancedTomlContainers(value)) {
|
|
464
|
+
pendingValue = { value, line: index + 1 };
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (!isTomlValue(value)) {
|
|
468
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (pendingValue) {
|
|
472
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function isTomlTable(line) {
|
|
476
|
+
const table = tomlTableName(line);
|
|
477
|
+
return Boolean(table && splitTomlDottedKey(table).every(isTomlKeyPart));
|
|
478
|
+
}
|
|
479
|
+
function isTomlKey(value) {
|
|
480
|
+
return splitTomlDottedKey(value).every(isTomlKeyPart);
|
|
481
|
+
}
|
|
482
|
+
function splitTomlDottedKey(value) {
|
|
483
|
+
const parts = [];
|
|
484
|
+
let current = "";
|
|
485
|
+
let quote = null;
|
|
486
|
+
let escaped = false;
|
|
487
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
488
|
+
const char = value[i];
|
|
489
|
+
if (quote) {
|
|
490
|
+
current += char;
|
|
491
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
492
|
+
escaped = true;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (char === quote && !escaped) quote = null;
|
|
496
|
+
escaped = false;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (char === '"' || char === "'") {
|
|
500
|
+
quote = char;
|
|
501
|
+
current += char;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (char === ".") {
|
|
505
|
+
parts.push(current.trim());
|
|
506
|
+
current = "";
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
current += char;
|
|
510
|
+
}
|
|
511
|
+
parts.push(current.trim());
|
|
512
|
+
return quote ? [] : parts;
|
|
513
|
+
}
|
|
514
|
+
function isTomlKeyPart(value) {
|
|
515
|
+
return isTomlBareKey(value) || isTomlQuotedString(value);
|
|
516
|
+
}
|
|
517
|
+
function isTomlBareKey(value) {
|
|
518
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
519
|
+
}
|
|
520
|
+
function isTomlValue(value) {
|
|
521
|
+
if (!value) return false;
|
|
522
|
+
if (isTomlQuotedString(value)) return true;
|
|
523
|
+
if (/^(true|false)$/i.test(value)) return true;
|
|
524
|
+
if (/^[+-]?(?:inf|nan)$/i.test(value)) return true;
|
|
525
|
+
if (/^[+-]?(?:0|[1-9][0-9_]*)(?:\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?$/.test(value)) return true;
|
|
526
|
+
if (/^\d{4}-\d{2}-\d{2}(?:[Tt ][0-9:.+-Zz]+)?$/.test(value)) return true;
|
|
527
|
+
if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
|
|
528
|
+
return hasBalancedTomlContainers(value);
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
function startsTomlContainer(value) {
|
|
533
|
+
return value.startsWith("[") || value.startsWith("{");
|
|
534
|
+
}
|
|
535
|
+
function isTomlQuotedString(value) {
|
|
536
|
+
if (value.startsWith('"""') || value.startsWith("'''")) {
|
|
537
|
+
const marker = value.slice(0, 3);
|
|
538
|
+
return value.length >= 6 && value.endsWith(marker);
|
|
539
|
+
}
|
|
540
|
+
if ((!value.startsWith('"') || !value.endsWith('"')) && (!value.startsWith("'") || !value.endsWith("'"))) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
return hasBalancedTomlContainers(value);
|
|
544
|
+
}
|
|
545
|
+
function stripTomlComment(value) {
|
|
546
|
+
let quote = null;
|
|
547
|
+
let escaped = false;
|
|
548
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
549
|
+
const char = value[i];
|
|
550
|
+
if (quote) {
|
|
551
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
552
|
+
escaped = true;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (char === quote && !escaped) quote = null;
|
|
556
|
+
escaped = false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (char === '"' || char === "'") {
|
|
560
|
+
quote = char;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (char === "#") return value.slice(0, i);
|
|
564
|
+
}
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
function hasBalancedTomlContainers(value) {
|
|
568
|
+
const stack = [];
|
|
569
|
+
let quote = null;
|
|
570
|
+
let escaped = false;
|
|
571
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
572
|
+
const char = value[i];
|
|
573
|
+
if (quote) {
|
|
574
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
575
|
+
escaped = true;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (char === quote && !escaped) quote = null;
|
|
579
|
+
escaped = false;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (char === '"' || char === "'") {
|
|
583
|
+
quote = char;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (char === "[" || char === "{") {
|
|
587
|
+
stack.push(char);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (char === "]") {
|
|
591
|
+
if (stack.pop() !== "[") return false;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (char === "}") {
|
|
595
|
+
if (stack.pop() !== "{") return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return stack.length === 0 && quote === null;
|
|
599
|
+
}
|
|
385
600
|
function tomlString(value) {
|
|
386
601
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
387
602
|
}
|
|
@@ -409,6 +624,10 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
|
|
|
409
624
|
}
|
|
410
625
|
function configTargetLabel(runtime) {
|
|
411
626
|
switch (runtime) {
|
|
627
|
+
case "codex-cli":
|
|
628
|
+
return "Codex CLI config";
|
|
629
|
+
case "codex-desktop":
|
|
630
|
+
return "Codex Desktop config";
|
|
412
631
|
case "cursor":
|
|
413
632
|
return "Cursor MCP config";
|
|
414
633
|
case "vscode":
|
|
@@ -420,10 +639,7 @@ function configTargetLabel(runtime) {
|
|
|
420
639
|
}
|
|
421
640
|
}
|
|
422
641
|
function signerPackageName() {
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
function localMcpPackageName() {
|
|
426
|
-
return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
|
|
642
|
+
return signerPackageSpec();
|
|
427
643
|
}
|
|
428
644
|
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
429
645
|
try {
|
|
@@ -537,6 +753,72 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
537
753
|
return false;
|
|
538
754
|
}
|
|
539
755
|
}
|
|
756
|
+
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
757
|
+
return new Promise((resolve6) => {
|
|
758
|
+
const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
759
|
+
let stdout = "";
|
|
760
|
+
let settled = false;
|
|
761
|
+
let sawInitialize = false;
|
|
762
|
+
const finish = (result) => {
|
|
763
|
+
if (settled) return;
|
|
764
|
+
settled = true;
|
|
765
|
+
clearTimeout(timeout);
|
|
766
|
+
child.kill();
|
|
767
|
+
resolve6(result);
|
|
768
|
+
};
|
|
769
|
+
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
770
|
+
child.on("error", () => finish({ status: "process_error" }));
|
|
771
|
+
child.on("exit", (code) => {
|
|
772
|
+
if (!settled && code !== 0) finish({ status: "process_error" });
|
|
773
|
+
});
|
|
774
|
+
child.stdout.on("data", (chunk) => {
|
|
775
|
+
stdout += chunk.toString("utf8");
|
|
776
|
+
const lines = stdout.split(/\r?\n/);
|
|
777
|
+
stdout = lines.pop() ?? "";
|
|
778
|
+
for (const line of lines) {
|
|
779
|
+
const trimmed = line.trim();
|
|
780
|
+
if (!trimmed) continue;
|
|
781
|
+
let payload;
|
|
782
|
+
try {
|
|
783
|
+
payload = JSON.parse(trimmed);
|
|
784
|
+
} catch {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (payload.error) {
|
|
788
|
+
finish({ status: "bad_response" });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (payload.id === 1 && !sawInitialize) {
|
|
792
|
+
sawInitialize = true;
|
|
793
|
+
writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
794
|
+
writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (payload.id === 2) {
|
|
798
|
+
const tools = payload.result?.tools;
|
|
799
|
+
const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
|
|
800
|
+
const missing = requiredTools.filter((name) => !toolNames.includes(name));
|
|
801
|
+
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
writeJsonRpc(child, {
|
|
807
|
+
jsonrpc: "2.0",
|
|
808
|
+
id: 1,
|
|
809
|
+
method: "initialize",
|
|
810
|
+
params: {
|
|
811
|
+
protocolVersion: "2025-06-18",
|
|
812
|
+
capabilities: {},
|
|
813
|
+
clientInfo: { name: "haven-connect-probe", version: "0.0.0" }
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function writeJsonRpc(child, payload) {
|
|
819
|
+
child.stdin?.write(`${JSON.stringify(payload)}
|
|
820
|
+
`);
|
|
821
|
+
}
|
|
540
822
|
function parseJsonRpcPayload(raw) {
|
|
541
823
|
const trimmed = raw.trim();
|
|
542
824
|
if (!trimmed) return null;
|
|
@@ -565,6 +847,161 @@ async function fetchWithTimeout(fetchImpl, url, init) {
|
|
|
565
847
|
clearTimeout(timeout);
|
|
566
848
|
}
|
|
567
849
|
}
|
|
850
|
+
var execFileAsync = util.promisify(child_process.execFile);
|
|
851
|
+
var UnsupportedNodeVersionError = class extends Error {
|
|
852
|
+
code = "local_mcp_unsupported_node_version";
|
|
853
|
+
constructor(nodeVersion, minimumNodeVersion) {
|
|
854
|
+
super(`Node.js ${nodeVersion} is not supported. Haven local MCP requires Node.js >=${minimumNodeVersion}.`);
|
|
855
|
+
this.name = "UnsupportedNodeVersionError";
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
859
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
860
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
861
|
+
const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
862
|
+
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
863
|
+
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
864
|
+
const messages = [];
|
|
865
|
+
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
866
|
+
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
867
|
+
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
868
|
+
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
869
|
+
if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
870
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
871
|
+
} else {
|
|
872
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps.runCommand);
|
|
873
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
874
|
+
}
|
|
875
|
+
await assertFileExists(cliPath, "local Haven MCP CLI");
|
|
876
|
+
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
|
|
877
|
+
await writeWrapper({
|
|
878
|
+
wrapperPath,
|
|
879
|
+
cliPath,
|
|
880
|
+
identityPath: input.identityPath,
|
|
881
|
+
signerPath: input.signerPath
|
|
882
|
+
});
|
|
883
|
+
await writeRuntimeSidecar({
|
|
884
|
+
path: path.join(input.credentialDirectory, "mcp-runtime.json"),
|
|
885
|
+
wrapperPath,
|
|
886
|
+
runtimeDirectory,
|
|
887
|
+
npmCacheDirectory,
|
|
888
|
+
cliPath
|
|
889
|
+
});
|
|
890
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
891
|
+
return {
|
|
892
|
+
command: wrapperPath,
|
|
893
|
+
args: [],
|
|
894
|
+
wrapperPath,
|
|
895
|
+
runtimeDirectory,
|
|
896
|
+
npmCacheDirectory,
|
|
897
|
+
cliPath,
|
|
898
|
+
messages
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion) {
|
|
902
|
+
if (compareNodeVersions(nodeVersion, minimumNodeVersion) < 0) {
|
|
903
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function compareNodeVersions(left, right) {
|
|
907
|
+
const leftParts = parseNodeVersion(left);
|
|
908
|
+
const rightParts = parseNodeVersion(right);
|
|
909
|
+
for (let i = 0; i < 3; i += 1) {
|
|
910
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
911
|
+
}
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
function parseNodeVersion(value) {
|
|
915
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
916
|
+
if (!match) return [0, 0, 0];
|
|
917
|
+
return [
|
|
918
|
+
Number(match[1] ?? 0),
|
|
919
|
+
Number(match[2] ?? 0),
|
|
920
|
+
Number(match[3] ?? 0)
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
|
|
924
|
+
const args = [
|
|
925
|
+
"install",
|
|
926
|
+
"--prefix",
|
|
927
|
+
runtimeDirectory,
|
|
928
|
+
"--cache",
|
|
929
|
+
npmCacheDirectory,
|
|
930
|
+
"--no-audit",
|
|
931
|
+
"--no-fund",
|
|
932
|
+
"--omit=dev",
|
|
933
|
+
mcpPackageSpec(),
|
|
934
|
+
sdkPackageSpec()
|
|
935
|
+
];
|
|
936
|
+
try {
|
|
937
|
+
if (runCommand) await runCommand("npm", args);
|
|
938
|
+
else await execFileAsync("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
|
|
939
|
+
} catch (err) {
|
|
940
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
944
|
+
try {
|
|
945
|
+
await assertFileExists(cliPath, "local Haven MCP CLI");
|
|
946
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
947
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
948
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
949
|
+
]);
|
|
950
|
+
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
951
|
+
} catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function readPackageJson(path) {
|
|
956
|
+
return JSON.parse(await promises.readFile(path, "utf8"));
|
|
957
|
+
}
|
|
958
|
+
async function writeWrapper(input) {
|
|
959
|
+
await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
960
|
+
await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
961
|
+
const source = [
|
|
962
|
+
"#!/usr/bin/env node",
|
|
963
|
+
"import { spawn } from 'node:child_process'",
|
|
964
|
+
"",
|
|
965
|
+
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
966
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
967
|
+
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
968
|
+
"",
|
|
969
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
970
|
+
" stdio: 'inherit',",
|
|
971
|
+
"})",
|
|
972
|
+
"",
|
|
973
|
+
"child.on('exit', (code, signal) => {",
|
|
974
|
+
" if (signal) process.kill(process.pid, signal)",
|
|
975
|
+
" else process.exit(code ?? 1)",
|
|
976
|
+
"})",
|
|
977
|
+
""
|
|
978
|
+
].join("\n");
|
|
979
|
+
await promises.writeFile(input.wrapperPath, source, { mode: 448 });
|
|
980
|
+
await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
981
|
+
}
|
|
982
|
+
async function writeRuntimeSidecar(input) {
|
|
983
|
+
const value = {
|
|
984
|
+
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
985
|
+
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
986
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
987
|
+
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
988
|
+
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
989
|
+
wrapper_path: input.wrapperPath,
|
|
990
|
+
runtime_directory: input.runtimeDirectory,
|
|
991
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
992
|
+
cli_path: input.cliPath
|
|
993
|
+
};
|
|
994
|
+
await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
995
|
+
`, { mode: 384 });
|
|
996
|
+
await promises.chmod(input.path, 384).catch(() => void 0);
|
|
997
|
+
}
|
|
998
|
+
async function assertFileExists(path, label) {
|
|
999
|
+
try {
|
|
1000
|
+
await promises.access(path);
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
568
1005
|
|
|
569
1006
|
// src/runtime-registry.ts
|
|
570
1007
|
var RUNTIME_PROFILES = {
|
|
@@ -580,6 +1017,12 @@ var RUNTIME_PROFILES = {
|
|
|
580
1017
|
restartMode: "restart-session",
|
|
581
1018
|
canWriteRuntimeConfig: true
|
|
582
1019
|
},
|
|
1020
|
+
"codex-desktop": {
|
|
1021
|
+
id: "codex-desktop",
|
|
1022
|
+
label: "Codex Desktop",
|
|
1023
|
+
restartMode: "restart-session",
|
|
1024
|
+
canWriteRuntimeConfig: true
|
|
1025
|
+
},
|
|
583
1026
|
cursor: {
|
|
584
1027
|
id: "cursor",
|
|
585
1028
|
label: "Cursor",
|
|
@@ -614,6 +1057,12 @@ var RUNTIME_ALIASES = {
|
|
|
614
1057
|
"codex-cli": "codex-cli",
|
|
615
1058
|
codexcli: "codex-cli",
|
|
616
1059
|
"codex_cli": "codex-cli",
|
|
1060
|
+
"codex-desktop": "codex-desktop",
|
|
1061
|
+
"codex_desktop": "codex-desktop",
|
|
1062
|
+
codexdesktop: "codex-desktop",
|
|
1063
|
+
"codex-app": "codex-desktop",
|
|
1064
|
+
"codex_app": "codex-desktop",
|
|
1065
|
+
codexapp: "codex-desktop",
|
|
617
1066
|
cursor: "cursor",
|
|
618
1067
|
vscode: "vscode",
|
|
619
1068
|
"vs-code": "vscode",
|
|
@@ -720,7 +1169,7 @@ function writeLogChunk2(log, chunk) {
|
|
|
720
1169
|
}
|
|
721
1170
|
|
|
722
1171
|
// src/runtime-install.ts
|
|
723
|
-
var
|
|
1172
|
+
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
724
1173
|
async function installRuntime(input, deps = {}) {
|
|
725
1174
|
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
726
1175
|
const profile = runtimeProfile(runtime, deps.env);
|
|
@@ -750,31 +1199,66 @@ async function installRuntime(input, deps = {}) {
|
|
|
750
1199
|
]
|
|
751
1200
|
};
|
|
752
1201
|
}
|
|
753
|
-
|
|
1202
|
+
let localRuntimeInstall;
|
|
1203
|
+
let localRuntimeError;
|
|
1204
|
+
if (localRuntime) {
|
|
1205
|
+
try {
|
|
1206
|
+
localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
localRuntimeError = err;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (localRuntimeError) {
|
|
1212
|
+
const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
|
|
1213
|
+
return {
|
|
1214
|
+
runtime,
|
|
1215
|
+
runtimeMcpMode: "local_stdio",
|
|
1216
|
+
hostedMcpConfigured: false,
|
|
1217
|
+
localSignerConfigured: false,
|
|
1218
|
+
localMcpConfigured: false,
|
|
1219
|
+
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
1220
|
+
restartRequired: true,
|
|
1221
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
1222
|
+
errorCode: errorCode2,
|
|
1223
|
+
configTarget: profile.label,
|
|
1224
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
1225
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
1226
|
+
activationCommand: void 0,
|
|
1227
|
+
messages: [
|
|
1228
|
+
...consentMessages,
|
|
1229
|
+
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
1230
|
+
]
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
|
|
754
1234
|
runtime,
|
|
755
1235
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
756
1236
|
apiKey: input.apiKey,
|
|
757
1237
|
identityPath: input.identityPath,
|
|
758
1238
|
signerPath: input.signerPath,
|
|
759
1239
|
credentialDirectory: input.credentialDirectory,
|
|
1240
|
+
localMcpCommand: localRuntimeInstall?.command,
|
|
760
1241
|
homeDir: deps.homeDir
|
|
761
1242
|
});
|
|
762
|
-
const
|
|
1243
|
+
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
1244
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
|
|
763
1245
|
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
764
|
-
probeLocalSignerCredential(input.signerPath)
|
|
1246
|
+
probeLocalSignerCredential(input.signerPath),
|
|
1247
|
+
localProbePromise
|
|
765
1248
|
]);
|
|
766
1249
|
const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
|
|
767
|
-
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged);
|
|
1250
|
+
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
768
1251
|
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
|
|
769
1252
|
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
770
|
-
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ?
|
|
1253
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
1254
|
+
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
1255
|
return {
|
|
772
1256
|
runtime,
|
|
773
1257
|
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
774
1258
|
hostedMcpConfigured: hostedOk,
|
|
775
1259
|
localSignerConfigured: signerOk,
|
|
776
1260
|
localMcpConfigured: localMcpOk,
|
|
777
|
-
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk),
|
|
1261
|
+
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
|
|
778
1262
|
restartRequired,
|
|
779
1263
|
nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
|
|
780
1264
|
errorCode,
|
|
@@ -782,7 +1266,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
782
1266
|
signerAcknowledged: signerConsent?.acknowledged,
|
|
783
1267
|
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
784
1268
|
activationCommand: configResult.activationCommand,
|
|
785
|
-
messages: [...consentMessages, ...configResult.messages]
|
|
1269
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
|
|
786
1270
|
};
|
|
787
1271
|
}
|
|
788
1272
|
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
@@ -792,23 +1276,21 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
792
1276
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
793
1277
|
};
|
|
794
1278
|
}
|
|
795
|
-
async function configureClaudeCode(
|
|
1279
|
+
async function configureClaudeCode(deps, localMcpCommand) {
|
|
796
1280
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
1281
|
+
const serverJson = JSON.stringify({
|
|
1282
|
+
type: "stdio",
|
|
1283
|
+
command: localMcpCommand,
|
|
1284
|
+
args: [],
|
|
1285
|
+
env: {}
|
|
1286
|
+
});
|
|
797
1287
|
try {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
"add",
|
|
801
|
-
|
|
802
|
-
"--",
|
|
803
|
-
"npx",
|
|
804
|
-
"-y",
|
|
805
|
-
localMcpPackageName2(),
|
|
806
|
-
"--identity",
|
|
807
|
-
input.identityPath,
|
|
808
|
-
"--signer",
|
|
809
|
-
input.signerPath
|
|
810
|
-
]);
|
|
1288
|
+
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
1289
|
+
await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
|
|
1290
|
+
await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
|
|
1291
|
+
});
|
|
811
1292
|
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
1293
|
+
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
812
1294
|
return {
|
|
813
1295
|
hostedConfigured: false,
|
|
814
1296
|
signerConfigured: true,
|
|
@@ -819,6 +1301,7 @@ async function configureClaudeCode(input, deps) {
|
|
|
819
1301
|
restartRequired: true,
|
|
820
1302
|
messages: [
|
|
821
1303
|
"Updated local Haven MCP entry with Claude Code.",
|
|
1304
|
+
...verified ? ["Verified Claude Code MCP entry."] : [],
|
|
822
1305
|
"After Haven approval, restart Claude Code normally so it can load Haven tools."
|
|
823
1306
|
]
|
|
824
1307
|
};
|
|
@@ -840,11 +1323,12 @@ async function configureClaudeCode(input, deps) {
|
|
|
840
1323
|
}
|
|
841
1324
|
}
|
|
842
1325
|
async function defaultRunCommand(command, args) {
|
|
843
|
-
await
|
|
1326
|
+
await execFileAsync2(command, args, { timeout: 1e4 });
|
|
844
1327
|
}
|
|
845
|
-
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady) {
|
|
1328
|
+
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
|
|
846
1329
|
if (mode === "local_stdio") {
|
|
847
|
-
|
|
1330
|
+
if (localMcpReady) return "local_stdio_mcp_ready";
|
|
1331
|
+
return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
|
|
848
1332
|
}
|
|
849
1333
|
const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
|
|
850
1334
|
const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
|
|
@@ -879,25 +1363,46 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
|
879
1363
|
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
880
1364
|
return void 0;
|
|
881
1365
|
}
|
|
882
|
-
function
|
|
1366
|
+
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
883
1367
|
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
884
1368
|
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
1369
|
+
if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
|
|
885
1370
|
return void 0;
|
|
886
1371
|
}
|
|
887
1372
|
function nextAction(runtime, restartMode, errorCode) {
|
|
888
1373
|
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
889
1374
|
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";
|
|
1375
|
+
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
891
1376
|
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
892
1377
|
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
893
1378
|
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
894
1379
|
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
895
1380
|
}
|
|
896
|
-
function localMcpPackageName2() {
|
|
897
|
-
return `@haven_ai/mcp@${mcp.MCP_VERSION}`;
|
|
898
|
-
}
|
|
899
1381
|
function usesLocalMcp(runtime) {
|
|
900
|
-
return runtime === "codex-cli" || runtime === "claude-code";
|
|
1382
|
+
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
1383
|
+
}
|
|
1384
|
+
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
1385
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
|
|
1386
|
+
return prepare({
|
|
1387
|
+
credentialDirectory: input.credentialDirectory,
|
|
1388
|
+
identityPath: input.identityPath,
|
|
1389
|
+
signerPath: input.signerPath,
|
|
1390
|
+
homeDir: deps.homeDir
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
1394
|
+
const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
|
|
1395
|
+
try {
|
|
1396
|
+
return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return { status: "process_error" };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function localRuntimePrepareErrorCode(err) {
|
|
1402
|
+
if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
|
|
1403
|
+
return "local_mcp_unsupported_node_version";
|
|
1404
|
+
}
|
|
1405
|
+
return "local_mcp_runtime_install_failed";
|
|
901
1406
|
}
|
|
902
1407
|
|
|
903
1408
|
// src/runtime.ts
|
|
@@ -1096,7 +1601,7 @@ function helpText() {
|
|
|
1096
1601
|
"Options:",
|
|
1097
1602
|
" --setup <token> Short-lived setup token from Haven.",
|
|
1098
1603
|
" --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.",
|
|
1604
|
+
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
|
|
1100
1605
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
1101
1606
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
1102
1607
|
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|