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