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