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