@haven_ai/connect 0.1.6-alpha → 0.1.11-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
@@ -54,6 +54,7 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
54
54
  signer_acknowledged: input.signerAcknowledged,
55
55
  local_mcp_acknowledged: input.localMcpAcknowledged,
56
56
  activation_command_available: input.activationCommandAvailable,
57
+ skill_installed: input.skillInstalled,
57
58
  probe_result: input.probeResult,
58
59
  restart_required: input.restartRequired,
59
60
  next_user_action: input.nextUserAction,
@@ -206,9 +207,9 @@ var MCP_RUNTIME_MANIFEST = {
206
207
  mcpPackage: "@haven_ai/mcp",
207
208
  mcpVersion: MCP_VERSION,
208
209
  sdkPackage: "@haven_ai/sdk",
209
- sdkVersion: "0.1.9",
210
+ sdkVersion: "0.1.11-alpha.0",
210
211
  signerPackage: "@haven_ai/signer",
211
- signerVersion: "0.1.3-alpha",
212
+ signerVersion: "0.1.11-alpha.0",
212
213
  minimumNodeVersion: "20.0.0",
213
214
  supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
214
215
  requiredTools: registeredToolNames()
@@ -413,6 +414,26 @@ function mergeCodexToml(existingToml, localMcpCommand) {
413
414
  validateCodexToml(block, "Generated Codex Haven config");
414
415
  const merged = `${next ? `${next}
415
416
 
417
+ ` : ""}${block}
418
+ `;
419
+ return merged;
420
+ }
421
+ function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerPath) {
422
+ let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
423
+ next = next.trimEnd();
424
+ const block = [
425
+ "[mcp_servers.haven]",
426
+ `url = ${tomlString(hostedMcpUrl)}`,
427
+ `http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
428
+ "",
429
+ "[mcp_servers.haven_signer]",
430
+ 'command = "npx"',
431
+ `args = ["-y", ${tomlString(signerPackageSpec())}, "--credentials", ${tomlString(signerPath)}]`,
432
+ "startup_timeout_sec = 120"
433
+ ].join("\n");
434
+ validateCodexToml(block, "Generated Codex Haven config");
435
+ const merged = `${next ? `${next}
436
+
416
437
  ` : ""}${block}
417
438
  `;
418
439
  return merged;
@@ -453,27 +474,46 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
453
474
  }
454
475
  async function writeCodexConfig(input) {
455
476
  const target = codexConfigPath(input.homeDir);
477
+ const local = input.mode === "local";
478
+ 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
479
  try {
457
480
  const existing = await readOptional(target);
458
- if (!input.localMcpCommand) {
459
- throw new Error("local MCP wrapper command is required");
481
+ if (local) {
482
+ if (!input.localMcpCommand) {
483
+ throw new Error("local MCP wrapper command is required");
484
+ }
485
+ const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
486
+ await writeOwnerOnlyText(target, merged2);
487
+ return {
488
+ hostedConfigured: false,
489
+ signerConfigured: true,
490
+ localMcpConfigured: true,
491
+ runtimeMcpMode: "local_stdio",
492
+ target: configTargetLabel(input.runtime),
493
+ changed: existing !== merged2,
494
+ restartRequired: true,
495
+ messages: [
496
+ `Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
497
+ // Codex Desktop loads MCP servers at app launch; Codex CLI typically
498
+ // picks them up in the next session. Branch the copy so desktop users
499
+ // get the unambiguous instruction.
500
+ restartMessage
501
+ ]
502
+ };
460
503
  }
461
- const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
504
+ const merged = mergeCodexTomlHosted(existing ?? "", input.hostedMcpUrl, input.apiKey, input.signerPath);
462
505
  await writeOwnerOnlyText(target, merged);
463
506
  return {
464
- hostedConfigured: false,
507
+ hostedConfigured: true,
465
508
  signerConfigured: true,
466
- localMcpConfigured: true,
467
- runtimeMcpMode: "local_stdio",
509
+ localMcpConfigured: false,
510
+ runtimeMcpMode: "hosted_plus_signer",
468
511
  target: configTargetLabel(input.runtime),
469
512
  changed: existing !== merged,
470
513
  restartRequired: true,
471
514
  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."
515
+ `Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`,
516
+ restartMessage
477
517
  ]
478
518
  };
479
519
  } catch (err) {
@@ -482,7 +522,7 @@ async function writeCodexConfig(input) {
482
522
  hostedConfigured: false,
483
523
  signerConfigured: false,
484
524
  localMcpConfigured: false,
485
- runtimeMcpMode: "local_stdio",
525
+ runtimeMcpMode: local ? "local_stdio" : "hosted_plus_signer",
486
526
  target: configTargetLabel(input.runtime),
487
527
  changed: false,
488
528
  restartRequired: true,
@@ -862,7 +902,7 @@ async function probeLocalSignerCredential(signerPath) {
862
902
  }
863
903
  }
864
904
  async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
865
- return new Promise((resolve6) => {
905
+ return new Promise((resolve7) => {
866
906
  const child = spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
867
907
  let stdout = "";
868
908
  let settled = false;
@@ -872,7 +912,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
872
912
  settled = true;
873
913
  clearTimeout(timeout);
874
914
  child.kill();
875
- resolve6(result);
915
+ resolve7(result);
876
916
  };
877
917
  const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
878
918
  child.on("error", () => finish({ status: "process_error" }));
@@ -1110,6 +1150,75 @@ async function assertFileExists(path, label) {
1110
1150
  throw new Error(`Missing ${label}: ${path}`);
1111
1151
  }
1112
1152
  }
1153
+
1154
+ // src/skill-content.ts
1155
+ var HAVEN_SKILL_MD = `---
1156
+ name: haven-pay
1157
+ description: Pay for things from the user's Haven wallet within their agent rules. Use when the user asks to send, pay, tip, or transfer crypto \u2014 or when a request hits an HTTP 402 (x402) paywall.
1158
+ ---
1159
+
1160
+ # Haven: pay from a Haven wallet
1161
+
1162
+ This skill lets the agent make payments from the user's Haven wallet through
1163
+ the Haven MCP tools. Every payment is checked against the agent's on-chain
1164
+ budget before money moves; payments above the remaining budget wait for the
1165
+ user's approval in Haven.
1166
+
1167
+ ## When to use this skill
1168
+
1169
+ - The user asks to send money, pay someone, tip, donate, or transfer tokens.
1170
+ - A request returns HTTP 402 (x402): use the Haven pay tools to settle it,
1171
+ then retry the original request.
1172
+
1173
+ ## Identity and budget come from the tools \u2014 never assume them
1174
+
1175
+ Do not guess the wallet address, network, or budget. Read them live:
1176
+
1177
+ - \`haven_get_agent\` \u2014 agent identity, Haven wallet address, network.
1178
+ - \`haven_get_allowances\` \u2014 current per-token budgets and what remains.
1179
+
1180
+ Budgets reset on a period the user chose. If a payment exceeds the remaining
1181
+ budget it is queued for the user to approve in the Haven dashboard \u2014 this is
1182
+ normal, not an error.
1183
+
1184
+ ## Paying
1185
+
1186
+ - **Direct transfer:** \`haven_pay\` with recipient, amount, and token.
1187
+ - **x402 paywall:** \`haven_quote_x402\` to get a quote, then
1188
+ \`haven_pay_x402_quote\`. In the hosted setup the signing step happens in
1189
+ the local Haven signer; follow the tool results \u2014 they tell you the next
1190
+ action at every step. Retry the original request only when the result says
1191
+ \`retry_original_x402_request\`.
1192
+ - **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
1193
+ queued or in-flight payments. Do not poll in a tight loop.
1194
+
1195
+ ## Approval semantics
1196
+
1197
+ - A result with \`pending_approval\` means the payment exceeded the remaining
1198
+ budget and is waiting for the user in Haven. Tell the user, then check
1199
+ status later.
1200
+ - Never ask the user for private keys and never try to sign anything
1201
+ yourself \u2014 Haven signs. If a tool reports a missing or invalid credential,
1202
+ tell the user to re-run the Haven setup command.
1203
+
1204
+ ## Failure handling
1205
+
1206
+ Haven errors are shaped \`{ error, status, details? }\` and written for
1207
+ humans \u2014 surface the message verbatim. Common cases:
1208
+
1209
+ - \`pending_approval\`: queued for the user's approval (see above).
1210
+ - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
1211
+ Suggest the user add funds in the Haven dashboard.
1212
+ - Budget exceeded: tell the user how much remains (from
1213
+ \`haven_get_allowances\`) and that they can raise the budget in Haven.
1214
+
1215
+ ## Revoke
1216
+
1217
+ If this agent's credential may have leaked, tell the user to pause or revoke
1218
+ the agent in the Haven dashboard under Agents. New requests stop immediately
1219
+ for that credential.
1220
+ `;
1221
+ var SKILL_FOLDER_NAME = "haven-pay";
1113
1222
  async function acknowledgeLocalSignerConsent(signerPath, log) {
1114
1223
  try {
1115
1224
  const input = await buildSignerConsentInput(signerPath);
@@ -1185,7 +1294,7 @@ var execFileAsync2 = promisify(execFile);
1185
1294
  async function installRuntime(input, deps = {}) {
1186
1295
  const runtime = normalizeRuntime(input.runtime, deps.env);
1187
1296
  const profile = runtimeProfile(runtime, deps.env);
1188
- const localRuntime = usesLocalMcp(runtime);
1297
+ const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
1189
1298
  const consentMessages = [];
1190
1299
  const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
1191
1300
  const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
@@ -1242,7 +1351,7 @@ async function installRuntime(input, deps = {}) {
1242
1351
  ]
1243
1352
  };
1244
1353
  }
1245
- const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
1354
+ const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input) : await writeRuntimeConfig({
1246
1355
  runtime,
1247
1356
  hostedMcpUrl: input.hostedMcpUrl,
1248
1357
  apiKey: input.apiKey,
@@ -1250,7 +1359,8 @@ async function installRuntime(input, deps = {}) {
1250
1359
  signerPath: input.signerPath,
1251
1360
  credentialDirectory: input.credentialDirectory,
1252
1361
  localMcpCommand: localRuntimeInstall?.command,
1253
- homeDir: deps.homeDir
1362
+ homeDir: deps.homeDir,
1363
+ mode: localRuntime ? "local" : "hosted"
1254
1364
  });
1255
1365
  const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
1256
1366
  const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
@@ -1264,6 +1374,7 @@ async function installRuntime(input, deps = {}) {
1264
1374
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1265
1375
  const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1266
1376
  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."] : [];
1377
+ const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1267
1378
  return {
1268
1379
  runtime,
1269
1380
  runtimeMcpMode: configResult.runtimeMcpMode,
@@ -1278,7 +1389,8 @@ async function installRuntime(input, deps = {}) {
1278
1389
  signerAcknowledged: signerConsent?.acknowledged,
1279
1390
  localMcpAcknowledged: localMcpConsent?.acknowledged,
1280
1391
  activationCommand: configResult.activationCommand,
1281
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
1392
+ skillInstalled: skillInstall?.installed,
1393
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1282
1394
  };
1283
1395
  }
1284
1396
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1334,9 +1446,75 @@ async function configureClaudeCode(deps, localMcpCommand) {
1334
1446
  };
1335
1447
  }
1336
1448
  }
1449
+ async function configureClaudeCodeHosted(deps, input) {
1450
+ const runCommand = deps.runCommand ?? defaultRunCommand;
1451
+ const hostedJson = JSON.stringify({
1452
+ type: "http",
1453
+ url: input.hostedMcpUrl,
1454
+ headers: { Authorization: `Bearer ${input.apiKey}` }
1455
+ });
1456
+ const signerJson = JSON.stringify({
1457
+ type: "stdio",
1458
+ command: "npx",
1459
+ args: ["-y", signerPackageSpec(), "--credentials", input.signerPath],
1460
+ env: {}
1461
+ });
1462
+ try {
1463
+ await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
1464
+ await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1465
+ await runCommand("claude", ["mcp", "add-json", "haven", hostedJson, "--scope", "user"]);
1466
+ await runCommand("claude", ["mcp", "add-json", "haven-signer", signerJson, "--scope", "user"]);
1467
+ const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
1468
+ return {
1469
+ hostedConfigured: true,
1470
+ signerConfigured: true,
1471
+ localMcpConfigured: false,
1472
+ runtimeMcpMode: "hosted_plus_signer",
1473
+ target: "Claude Code MCP config",
1474
+ changed: true,
1475
+ restartRequired: true,
1476
+ messages: [
1477
+ "Updated hosted Haven MCP and local signer entries with Claude Code.",
1478
+ ...verified ? ["Verified Claude Code MCP entry."] : [],
1479
+ "After Haven approval, Haven tools should appear in your next Claude Code message. If they don't, restart the session to load them."
1480
+ ]
1481
+ };
1482
+ } catch (err) {
1483
+ return {
1484
+ hostedConfigured: false,
1485
+ signerConfigured: false,
1486
+ localMcpConfigured: false,
1487
+ runtimeMcpMode: "hosted_plus_signer",
1488
+ target: "Claude Code MCP config",
1489
+ changed: false,
1490
+ restartRequired: true,
1491
+ messages: [
1492
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
1493
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
1494
+ ],
1495
+ errorCode: "claude_code_config_failed"
1496
+ };
1497
+ }
1498
+ }
1337
1499
  async function defaultRunCommand(command, args) {
1338
1500
  await execFileAsync2(command, args, { timeout: 1e4 });
1339
1501
  }
1502
+ async function installClaudeSkill(homeDir) {
1503
+ try {
1504
+ const skillDir = resolve(homeDir ?? homedir(), ".claude", "skills", SKILL_FOLDER_NAME);
1505
+ await mkdir(skillDir, { recursive: true });
1506
+ await writeFile(join(skillDir, "SKILL.md"), HAVEN_SKILL_MD, "utf8");
1507
+ return {
1508
+ installed: true,
1509
+ messages: ["Installed the generic Haven payment skill (~/.claude/skills/haven-pay). It contains no secrets."]
1510
+ };
1511
+ } catch (err) {
1512
+ return {
1513
+ installed: false,
1514
+ messages: [`Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`]
1515
+ };
1516
+ }
1517
+ }
1340
1518
  function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
1341
1519
  if (mode === "local_stdio") {
1342
1520
  if (localMcpReady) return "local_stdio_mcp_ready";
@@ -1390,7 +1568,7 @@ function nextAction(runtime, restartMode, errorCode) {
1390
1568
  if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
1391
1569
  return "return_to_haven_for_wallet_approval_then_configure_runtime";
1392
1570
  }
1393
- function usesLocalMcp(runtime) {
1571
+ function supportsLocalMcp(runtime) {
1394
1572
  return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
1395
1573
  }
1396
1574
  async function prepareRuntimeForLocalMcp(input, deps) {
@@ -1418,7 +1596,7 @@ function localRuntimePrepareErrorCode(err) {
1418
1596
  }
1419
1597
 
1420
1598
  // src/runtime.ts
1421
- var CONNECTOR_VERSION = "0.1.2";
1599
+ var CONNECTOR_VERSION = "0.1.11-alpha.0";
1422
1600
  async function runConnect(options, deps = {}) {
1423
1601
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
1424
1602
  const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
@@ -1430,6 +1608,14 @@ async function runConnect(options, deps = {}) {
1430
1608
  const generateKey = deps.generateKey ?? generateDelegateKey;
1431
1609
  const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
1432
1610
  const installCapabilities = runtimeInstallCapabilities(options.runtime);
1611
+ if (options.localMcp) {
1612
+ const resolvedRuntime = normalizeRuntime(options.runtime);
1613
+ if (!supportsLocalMcp(resolvedRuntime)) {
1614
+ throw new Error(
1615
+ `--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.`
1616
+ );
1617
+ }
1618
+ }
1433
1619
  const setup = await api.resolveSetup({
1434
1620
  setupToken: options.setupToken,
1435
1621
  connectorVersion,
@@ -1488,7 +1674,8 @@ async function runConnect(options, deps = {}) {
1488
1674
  credentialDirectory: credentialPaths.directory,
1489
1675
  environmentLabel: options.environmentLabel ?? "Local workspace",
1490
1676
  ackSigner: options.ackSigner,
1491
- ackLocalTools: options.ackLocalTools
1677
+ ackLocalTools: options.ackLocalTools,
1678
+ localMcp: options.localMcp
1492
1679
  });
1493
1680
  printRuntimeInstall(runtimeInstall, log);
1494
1681
  try {
@@ -1503,6 +1690,7 @@ async function runConnect(options, deps = {}) {
1503
1690
  signerAcknowledged: runtimeInstall.signerAcknowledged,
1504
1691
  localMcpAcknowledged: runtimeInstall.localMcpAcknowledged,
1505
1692
  activationCommandAvailable: Boolean(runtimeInstall.activationCommand),
1693
+ skillInstalled: runtimeInstall.skillInstalled,
1506
1694
  probeResult: runtimeInstall.probeResult,
1507
1695
  restartRequired: runtimeInstall.restartRequired,
1508
1696
  nextUserAction: runtimeInstall.nextUserAction,
@@ -1584,6 +1772,8 @@ function parseArgs(argv, env = process.env) {
1584
1772
  } else if (arg === "--ack-signer") {
1585
1773
  options.ackSigner = true;
1586
1774
  options.ackLocalTools = true;
1775
+ } else if (arg === "--local" || arg === "--local-mcp") {
1776
+ options.localMcp = true;
1587
1777
  } else if (arg === "--version") {
1588
1778
  process.stdout.write(`${CONNECTOR_VERSION}
1589
1779
  `);
@@ -1622,6 +1812,8 @@ function helpText() {
1622
1812
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1623
1813
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
1624
1814
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
1815
+ " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
1816
+ " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
1625
1817
  " --help Show this help.",
1626
1818
  "",
1627
1819
  "The connector never prints the private key and never sends it to Haven."