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