@haven_ai/connect 0.1.5-alpha → 0.1.10-alpha.0

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/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { join, resolve, dirname } from 'path';
7
7
  import { execFile, spawn } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import { registeredToolNames, MCP_VERSION, ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient } from '@haven_ai/mcp';
10
+ import { SKILL_FOLDER_NAME, HAVEN_SKILL_MD } from '@haven_ai/sdk';
10
11
  import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
11
12
 
12
13
  // src/api.ts
@@ -54,6 +55,7 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
54
55
  signer_acknowledged: input.signerAcknowledged,
55
56
  local_mcp_acknowledged: input.localMcpAcknowledged,
56
57
  activation_command_available: input.activationCommandAvailable,
58
+ skill_installed: input.skillInstalled,
57
59
  probe_result: input.probeResult,
58
60
  restart_required: input.restartRequired,
59
61
  next_user_action: input.nextUserAction,
@@ -206,9 +208,9 @@ var MCP_RUNTIME_MANIFEST = {
206
208
  mcpPackage: "@haven_ai/mcp",
207
209
  mcpVersion: MCP_VERSION,
208
210
  sdkPackage: "@haven_ai/sdk",
209
- sdkVersion: "0.1.8",
211
+ sdkVersion: "0.1.10-alpha.0",
210
212
  signerPackage: "@haven_ai/signer",
211
- signerVersion: "0.1.2-alpha",
213
+ signerVersion: "0.1.10-alpha.0",
212
214
  minimumNodeVersion: "20.0.0",
213
215
  supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
214
216
  requiredTools: registeredToolNames()
@@ -413,6 +415,26 @@ function mergeCodexToml(existingToml, localMcpCommand) {
413
415
  validateCodexToml(block, "Generated Codex Haven config");
414
416
  const merged = `${next ? `${next}
415
417
 
418
+ ` : ""}${block}
419
+ `;
420
+ return merged;
421
+ }
422
+ function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerPath) {
423
+ let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
424
+ next = next.trimEnd();
425
+ const block = [
426
+ "[mcp_servers.haven]",
427
+ `url = ${tomlString(hostedMcpUrl)}`,
428
+ `http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
429
+ "",
430
+ "[mcp_servers.haven_signer]",
431
+ 'command = "npx"',
432
+ `args = ["-y", ${tomlString(signerPackageSpec())}, "--credentials", ${tomlString(signerPath)}]`,
433
+ "startup_timeout_sec = 120"
434
+ ].join("\n");
435
+ validateCodexToml(block, "Generated Codex Haven config");
436
+ const merged = `${next ? `${next}
437
+
416
438
  ` : ""}${block}
417
439
  `;
418
440
  return merged;
@@ -453,27 +475,46 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
453
475
  }
454
476
  async function writeCodexConfig(input) {
455
477
  const target = codexConfigPath(input.homeDir);
478
+ const local = input.mode === "local";
479
+ const restartMessage = 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.";
456
480
  try {
457
481
  const existing = await readOptional(target);
458
- if (!input.localMcpCommand) {
459
- throw new Error("local MCP wrapper command is required");
482
+ if (local) {
483
+ if (!input.localMcpCommand) {
484
+ throw new Error("local MCP wrapper command is required");
485
+ }
486
+ const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
487
+ await writeOwnerOnlyText(target, merged2);
488
+ return {
489
+ hostedConfigured: false,
490
+ signerConfigured: true,
491
+ localMcpConfigured: true,
492
+ runtimeMcpMode: "local_stdio",
493
+ target: configTargetLabel(input.runtime),
494
+ changed: existing !== merged2,
495
+ restartRequired: true,
496
+ messages: [
497
+ `Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
498
+ // Codex Desktop loads MCP servers at app launch; Codex CLI typically
499
+ // picks them up in the next session. Branch the copy so desktop users
500
+ // get the unambiguous instruction.
501
+ restartMessage
502
+ ]
503
+ };
460
504
  }
461
- const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
505
+ const merged = mergeCodexTomlHosted(existing ?? "", input.hostedMcpUrl, input.apiKey, input.signerPath);
462
506
  await writeOwnerOnlyText(target, merged);
463
507
  return {
464
- hostedConfigured: false,
508
+ hostedConfigured: true,
465
509
  signerConfigured: true,
466
- localMcpConfigured: true,
467
- runtimeMcpMode: "local_stdio",
510
+ localMcpConfigured: false,
511
+ runtimeMcpMode: "hosted_plus_signer",
468
512
  target: configTargetLabel(input.runtime),
469
513
  changed: existing !== merged,
470
514
  restartRequired: true,
471
515
  messages: [
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."
516
+ `Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`,
517
+ restartMessage
477
518
  ]
478
519
  };
479
520
  } catch (err) {
@@ -482,7 +523,7 @@ async function writeCodexConfig(input) {
482
523
  hostedConfigured: false,
483
524
  signerConfigured: false,
484
525
  localMcpConfigured: false,
485
- runtimeMcpMode: "local_stdio",
526
+ runtimeMcpMode: local ? "local_stdio" : "hosted_plus_signer",
486
527
  target: configTargetLabel(input.runtime),
487
528
  changed: false,
488
529
  restartRequired: true,
@@ -862,7 +903,7 @@ async function probeLocalSignerCredential(signerPath) {
862
903
  }
863
904
  }
864
905
  async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
865
- return new Promise((resolve6) => {
906
+ return new Promise((resolve7) => {
866
907
  const child = spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
867
908
  let stdout = "";
868
909
  let settled = false;
@@ -872,7 +913,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
872
913
  settled = true;
873
914
  clearTimeout(timeout);
874
915
  child.kill();
875
- resolve6(result);
916
+ resolve7(result);
876
917
  };
877
918
  const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
878
919
  child.on("error", () => finish({ status: "process_error" }));
@@ -1185,7 +1226,7 @@ var execFileAsync2 = promisify(execFile);
1185
1226
  async function installRuntime(input, deps = {}) {
1186
1227
  const runtime = normalizeRuntime(input.runtime, deps.env);
1187
1228
  const profile = runtimeProfile(runtime, deps.env);
1188
- const localRuntime = usesLocalMcp(runtime);
1229
+ const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
1189
1230
  const consentMessages = [];
1190
1231
  const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
1191
1232
  const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
@@ -1242,7 +1283,7 @@ async function installRuntime(input, deps = {}) {
1242
1283
  ]
1243
1284
  };
1244
1285
  }
1245
- const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
1286
+ const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input) : await writeRuntimeConfig({
1246
1287
  runtime,
1247
1288
  hostedMcpUrl: input.hostedMcpUrl,
1248
1289
  apiKey: input.apiKey,
@@ -1250,7 +1291,8 @@ async function installRuntime(input, deps = {}) {
1250
1291
  signerPath: input.signerPath,
1251
1292
  credentialDirectory: input.credentialDirectory,
1252
1293
  localMcpCommand: localRuntimeInstall?.command,
1253
- homeDir: deps.homeDir
1294
+ homeDir: deps.homeDir,
1295
+ mode: localRuntime ? "local" : "hosted"
1254
1296
  });
1255
1297
  const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
1256
1298
  const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
@@ -1264,6 +1306,7 @@ async function installRuntime(input, deps = {}) {
1264
1306
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1265
1307
  const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1266
1308
  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."] : [];
1309
+ const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1267
1310
  return {
1268
1311
  runtime,
1269
1312
  runtimeMcpMode: configResult.runtimeMcpMode,
@@ -1278,7 +1321,8 @@ async function installRuntime(input, deps = {}) {
1278
1321
  signerAcknowledged: signerConsent?.acknowledged,
1279
1322
  localMcpAcknowledged: localMcpConsent?.acknowledged,
1280
1323
  activationCommand: configResult.activationCommand,
1281
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
1324
+ skillInstalled: skillInstall?.installed,
1325
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1282
1326
  };
1283
1327
  }
1284
1328
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1334,9 +1378,75 @@ async function configureClaudeCode(deps, localMcpCommand) {
1334
1378
  };
1335
1379
  }
1336
1380
  }
1381
+ async function configureClaudeCodeHosted(deps, input) {
1382
+ const runCommand = deps.runCommand ?? defaultRunCommand;
1383
+ const hostedJson = JSON.stringify({
1384
+ type: "http",
1385
+ url: input.hostedMcpUrl,
1386
+ headers: { Authorization: `Bearer ${input.apiKey}` }
1387
+ });
1388
+ const signerJson = JSON.stringify({
1389
+ type: "stdio",
1390
+ command: "npx",
1391
+ args: ["-y", signerPackageSpec(), "--credentials", input.signerPath],
1392
+ env: {}
1393
+ });
1394
+ try {
1395
+ await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
1396
+ await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1397
+ await runCommand("claude", ["mcp", "add-json", "haven", hostedJson, "--scope", "user"]);
1398
+ await runCommand("claude", ["mcp", "add-json", "haven-signer", signerJson, "--scope", "user"]);
1399
+ const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
1400
+ return {
1401
+ hostedConfigured: true,
1402
+ signerConfigured: true,
1403
+ localMcpConfigured: false,
1404
+ runtimeMcpMode: "hosted_plus_signer",
1405
+ target: "Claude Code MCP config",
1406
+ changed: true,
1407
+ restartRequired: true,
1408
+ messages: [
1409
+ "Updated hosted Haven MCP and local signer entries with Claude Code.",
1410
+ ...verified ? ["Verified Claude Code MCP entry."] : [],
1411
+ "After Haven approval, Haven tools should appear in your next Claude Code message. If they don't, restart the session to load them."
1412
+ ]
1413
+ };
1414
+ } catch (err) {
1415
+ return {
1416
+ hostedConfigured: false,
1417
+ signerConfigured: false,
1418
+ localMcpConfigured: false,
1419
+ runtimeMcpMode: "hosted_plus_signer",
1420
+ target: "Claude Code MCP config",
1421
+ changed: false,
1422
+ restartRequired: true,
1423
+ messages: [
1424
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
1425
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
1426
+ ],
1427
+ errorCode: "claude_code_config_failed"
1428
+ };
1429
+ }
1430
+ }
1337
1431
  async function defaultRunCommand(command, args) {
1338
1432
  await execFileAsync2(command, args, { timeout: 1e4 });
1339
1433
  }
1434
+ async function installClaudeSkill(homeDir) {
1435
+ try {
1436
+ const skillDir = resolve(homeDir ?? homedir(), ".claude", "skills", SKILL_FOLDER_NAME);
1437
+ await mkdir(skillDir, { recursive: true });
1438
+ await writeFile(join(skillDir, "SKILL.md"), HAVEN_SKILL_MD, "utf8");
1439
+ return {
1440
+ installed: true,
1441
+ messages: ["Installed the generic Haven payment skill (~/.claude/skills/haven-pay). It contains no secrets."]
1442
+ };
1443
+ } catch (err) {
1444
+ return {
1445
+ installed: false,
1446
+ messages: [`Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`]
1447
+ };
1448
+ }
1449
+ }
1340
1450
  function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
1341
1451
  if (mode === "local_stdio") {
1342
1452
  if (localMcpReady) return "local_stdio_mcp_ready";
@@ -1390,7 +1500,7 @@ function nextAction(runtime, restartMode, errorCode) {
1390
1500
  if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
1391
1501
  return "return_to_haven_for_wallet_approval_then_configure_runtime";
1392
1502
  }
1393
- function usesLocalMcp(runtime) {
1503
+ function supportsLocalMcp(runtime) {
1394
1504
  return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
1395
1505
  }
1396
1506
  async function prepareRuntimeForLocalMcp(input, deps) {
@@ -1430,6 +1540,14 @@ async function runConnect(options, deps = {}) {
1430
1540
  const generateKey = deps.generateKey ?? generateDelegateKey;
1431
1541
  const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
1432
1542
  const installCapabilities = runtimeInstallCapabilities(options.runtime);
1543
+ if (options.localMcp) {
1544
+ const resolvedRuntime = normalizeRuntime(options.runtime);
1545
+ if (!supportsLocalMcp(resolvedRuntime)) {
1546
+ throw new Error(
1547
+ `--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(resolvedRuntime).label}. Re-run without --local to use the default hosted MCP + local signer setup.`
1548
+ );
1549
+ }
1550
+ }
1433
1551
  const setup = await api.resolveSetup({
1434
1552
  setupToken: options.setupToken,
1435
1553
  connectorVersion,
@@ -1488,7 +1606,8 @@ async function runConnect(options, deps = {}) {
1488
1606
  credentialDirectory: credentialPaths.directory,
1489
1607
  environmentLabel: options.environmentLabel ?? "Local workspace",
1490
1608
  ackSigner: options.ackSigner,
1491
- ackLocalTools: options.ackLocalTools
1609
+ ackLocalTools: options.ackLocalTools,
1610
+ localMcp: options.localMcp
1492
1611
  });
1493
1612
  printRuntimeInstall(runtimeInstall, log);
1494
1613
  try {
@@ -1503,6 +1622,7 @@ async function runConnect(options, deps = {}) {
1503
1622
  signerAcknowledged: runtimeInstall.signerAcknowledged,
1504
1623
  localMcpAcknowledged: runtimeInstall.localMcpAcknowledged,
1505
1624
  activationCommandAvailable: Boolean(runtimeInstall.activationCommand),
1625
+ skillInstalled: runtimeInstall.skillInstalled,
1506
1626
  probeResult: runtimeInstall.probeResult,
1507
1627
  restartRequired: runtimeInstall.restartRequired,
1508
1628
  nextUserAction: runtimeInstall.nextUserAction,
@@ -1584,6 +1704,8 @@ function parseArgs(argv, env = process.env) {
1584
1704
  } else if (arg === "--ack-signer") {
1585
1705
  options.ackSigner = true;
1586
1706
  options.ackLocalTools = true;
1707
+ } else if (arg === "--local" || arg === "--local-mcp") {
1708
+ options.localMcp = true;
1587
1709
  } else if (arg === "--version") {
1588
1710
  process.stdout.write(`${CONNECTOR_VERSION}
1589
1711
  `);
@@ -1622,6 +1744,8 @@ function helpText() {
1622
1744
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1623
1745
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
1624
1746
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
1747
+ " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
1748
+ " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
1625
1749
  " --help Show this help.",
1626
1750
  "",
1627
1751
  "The connector never prints the private key and never sends it to Haven."