@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/README.md CHANGED
@@ -8,7 +8,7 @@ public signing address, proof signature, and API-key hash. Haven never receives
8
8
  the private key or plaintext API key.
9
9
 
10
10
  ```sh
11
- npx -y @haven_ai/connect@0.1.3-alpha --setup hv_setup_... --api https://api.haven.example --ack-local-tools --runtime claude-code
11
+ npx -y @haven_ai/connect@alpha --setup hv_setup_... --api https://api.haven.example --ack-local-tools --runtime claude-code
12
12
  ```
13
13
 
14
14
  The connector writes owner-only credential files outside the project by default:
package/dist/cli.cjs CHANGED
@@ -60,6 +60,7 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
60
60
  signer_acknowledged: input.signerAcknowledged,
61
61
  local_mcp_acknowledged: input.localMcpAcknowledged,
62
62
  activation_command_available: input.activationCommandAvailable,
63
+ skill_installed: input.skillInstalled,
63
64
  probe_result: input.probeResult,
64
65
  restart_required: input.restartRequired,
65
66
  next_user_action: input.nextUserAction,
@@ -212,9 +213,9 @@ var MCP_RUNTIME_MANIFEST = {
212
213
  mcpPackage: "@haven_ai/mcp",
213
214
  mcpVersion: mcp.MCP_VERSION,
214
215
  sdkPackage: "@haven_ai/sdk",
215
- sdkVersion: "0.1.9",
216
+ sdkVersion: "0.1.11-alpha.0",
216
217
  signerPackage: "@haven_ai/signer",
217
- signerVersion: "0.1.3-alpha",
218
+ signerVersion: "0.1.11-alpha.0",
218
219
  minimumNodeVersion: "20.0.0",
219
220
  supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
220
221
  requiredTools: mcp.registeredToolNames()
@@ -419,6 +420,26 @@ function mergeCodexToml(existingToml, localMcpCommand) {
419
420
  validateCodexToml(block, "Generated Codex Haven config");
420
421
  const merged = `${next ? `${next}
421
422
 
423
+ ` : ""}${block}
424
+ `;
425
+ return merged;
426
+ }
427
+ function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerPath) {
428
+ let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
429
+ next = next.trimEnd();
430
+ const block = [
431
+ "[mcp_servers.haven]",
432
+ `url = ${tomlString(hostedMcpUrl)}`,
433
+ `http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
434
+ "",
435
+ "[mcp_servers.haven_signer]",
436
+ 'command = "npx"',
437
+ `args = ["-y", ${tomlString(signerPackageSpec())}, "--credentials", ${tomlString(signerPath)}]`,
438
+ "startup_timeout_sec = 120"
439
+ ].join("\n");
440
+ validateCodexToml(block, "Generated Codex Haven config");
441
+ const merged = `${next ? `${next}
442
+
422
443
  ` : ""}${block}
423
444
  `;
424
445
  return merged;
@@ -459,27 +480,46 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
459
480
  }
460
481
  async function writeCodexConfig(input) {
461
482
  const target = codexConfigPath(input.homeDir);
483
+ const local = input.mode === "local";
484
+ 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.";
462
485
  try {
463
486
  const existing = await readOptional(target);
464
- if (!input.localMcpCommand) {
465
- throw new Error("local MCP wrapper command is required");
487
+ if (local) {
488
+ if (!input.localMcpCommand) {
489
+ throw new Error("local MCP wrapper command is required");
490
+ }
491
+ const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
492
+ await writeOwnerOnlyText(target, merged2);
493
+ return {
494
+ hostedConfigured: false,
495
+ signerConfigured: true,
496
+ localMcpConfigured: true,
497
+ runtimeMcpMode: "local_stdio",
498
+ target: configTargetLabel(input.runtime),
499
+ changed: existing !== merged2,
500
+ restartRequired: true,
501
+ messages: [
502
+ `Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
503
+ // Codex Desktop loads MCP servers at app launch; Codex CLI typically
504
+ // picks them up in the next session. Branch the copy so desktop users
505
+ // get the unambiguous instruction.
506
+ restartMessage
507
+ ]
508
+ };
466
509
  }
467
- const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
510
+ const merged = mergeCodexTomlHosted(existing ?? "", input.hostedMcpUrl, input.apiKey, input.signerPath);
468
511
  await writeOwnerOnlyText(target, merged);
469
512
  return {
470
- hostedConfigured: false,
513
+ hostedConfigured: true,
471
514
  signerConfigured: true,
472
- localMcpConfigured: true,
473
- runtimeMcpMode: "local_stdio",
515
+ localMcpConfigured: false,
516
+ runtimeMcpMode: "hosted_plus_signer",
474
517
  target: configTargetLabel(input.runtime),
475
518
  changed: existing !== merged,
476
519
  restartRequired: true,
477
520
  messages: [
478
- `Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
479
- // Codex Desktop loads MCP servers at app launch; Codex CLI typically
480
- // picks them up in the next session. Branch the copy so desktop users
481
- // get the unambiguous instruction.
482
- 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."
521
+ `Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`,
522
+ restartMessage
483
523
  ]
484
524
  };
485
525
  } catch (err) {
@@ -488,7 +528,7 @@ async function writeCodexConfig(input) {
488
528
  hostedConfigured: false,
489
529
  signerConfigured: false,
490
530
  localMcpConfigured: false,
491
- runtimeMcpMode: "local_stdio",
531
+ runtimeMcpMode: local ? "local_stdio" : "hosted_plus_signer",
492
532
  target: configTargetLabel(input.runtime),
493
533
  changed: false,
494
534
  restartRequired: true,
@@ -868,7 +908,7 @@ async function probeLocalSignerCredential(signerPath) {
868
908
  }
869
909
  }
870
910
  async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
871
- return new Promise((resolve6) => {
911
+ return new Promise((resolve7) => {
872
912
  const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
873
913
  let stdout = "";
874
914
  let settled = false;
@@ -878,7 +918,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
878
918
  settled = true;
879
919
  clearTimeout(timeout);
880
920
  child.kill();
881
- resolve6(result);
921
+ resolve7(result);
882
922
  };
883
923
  const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
884
924
  child.on("error", () => finish({ status: "process_error" }));
@@ -1116,6 +1156,75 @@ async function assertFileExists(path, label) {
1116
1156
  throw new Error(`Missing ${label}: ${path}`);
1117
1157
  }
1118
1158
  }
1159
+
1160
+ // src/skill-content.ts
1161
+ var HAVEN_SKILL_MD = `---
1162
+ name: haven-pay
1163
+ 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.
1164
+ ---
1165
+
1166
+ # Haven: pay from a Haven wallet
1167
+
1168
+ This skill lets the agent make payments from the user's Haven wallet through
1169
+ the Haven MCP tools. Every payment is checked against the agent's on-chain
1170
+ budget before money moves; payments above the remaining budget wait for the
1171
+ user's approval in Haven.
1172
+
1173
+ ## When to use this skill
1174
+
1175
+ - The user asks to send money, pay someone, tip, donate, or transfer tokens.
1176
+ - A request returns HTTP 402 (x402): use the Haven pay tools to settle it,
1177
+ then retry the original request.
1178
+
1179
+ ## Identity and budget come from the tools \u2014 never assume them
1180
+
1181
+ Do not guess the wallet address, network, or budget. Read them live:
1182
+
1183
+ - \`haven_get_agent\` \u2014 agent identity, Haven wallet address, network.
1184
+ - \`haven_get_allowances\` \u2014 current per-token budgets and what remains.
1185
+
1186
+ Budgets reset on a period the user chose. If a payment exceeds the remaining
1187
+ budget it is queued for the user to approve in the Haven dashboard \u2014 this is
1188
+ normal, not an error.
1189
+
1190
+ ## Paying
1191
+
1192
+ - **Direct transfer:** \`haven_pay\` with recipient, amount, and token.
1193
+ - **x402 paywall:** \`haven_quote_x402\` to get a quote, then
1194
+ \`haven_pay_x402_quote\`. In the hosted setup the signing step happens in
1195
+ the local Haven signer; follow the tool results \u2014 they tell you the next
1196
+ action at every step. Retry the original request only when the result says
1197
+ \`retry_original_x402_request\`.
1198
+ - **Status:** \`haven_get_payment_status\` with a \`payment_id\` to check on
1199
+ queued or in-flight payments. Do not poll in a tight loop.
1200
+
1201
+ ## Approval semantics
1202
+
1203
+ - A result with \`pending_approval\` means the payment exceeded the remaining
1204
+ budget and is waiting for the user in Haven. Tell the user, then check
1205
+ status later.
1206
+ - Never ask the user for private keys and never try to sign anything
1207
+ yourself \u2014 Haven signs. If a tool reports a missing or invalid credential,
1208
+ tell the user to re-run the Haven setup command.
1209
+
1210
+ ## Failure handling
1211
+
1212
+ Haven errors are shaped \`{ error, status, details? }\` and written for
1213
+ humans \u2014 surface the message verbatim. Common cases:
1214
+
1215
+ - \`pending_approval\`: queued for the user's approval (see above).
1216
+ - \`insufficient_funds\`: the Haven wallet doesn't hold enough of that token.
1217
+ Suggest the user add funds in the Haven dashboard.
1218
+ - Budget exceeded: tell the user how much remains (from
1219
+ \`haven_get_allowances\`) and that they can raise the budget in Haven.
1220
+
1221
+ ## Revoke
1222
+
1223
+ If this agent's credential may have leaked, tell the user to pause or revoke
1224
+ the agent in the Haven dashboard under Agents. New requests stop immediately
1225
+ for that credential.
1226
+ `;
1227
+ var SKILL_FOLDER_NAME = "haven-pay";
1119
1228
  async function acknowledgeLocalSignerConsent(signerPath, log) {
1120
1229
  try {
1121
1230
  const input = await buildSignerConsentInput(signerPath);
@@ -1191,7 +1300,7 @@ var execFileAsync2 = util.promisify(child_process.execFile);
1191
1300
  async function installRuntime(input, deps = {}) {
1192
1301
  const runtime = normalizeRuntime(input.runtime, deps.env);
1193
1302
  const profile = runtimeProfile(runtime, deps.env);
1194
- const localRuntime = usesLocalMcp(runtime);
1303
+ const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
1195
1304
  const consentMessages = [];
1196
1305
  const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
1197
1306
  const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
@@ -1248,7 +1357,7 @@ async function installRuntime(input, deps = {}) {
1248
1357
  ]
1249
1358
  };
1250
1359
  }
1251
- const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
1360
+ const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input) : await writeRuntimeConfig({
1252
1361
  runtime,
1253
1362
  hostedMcpUrl: input.hostedMcpUrl,
1254
1363
  apiKey: input.apiKey,
@@ -1256,7 +1365,8 @@ async function installRuntime(input, deps = {}) {
1256
1365
  signerPath: input.signerPath,
1257
1366
  credentialDirectory: input.credentialDirectory,
1258
1367
  localMcpCommand: localRuntimeInstall?.command,
1259
- homeDir: deps.homeDir
1368
+ homeDir: deps.homeDir,
1369
+ mode: localRuntime ? "local" : "hosted"
1260
1370
  });
1261
1371
  const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
1262
1372
  const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
@@ -1270,6 +1380,7 @@ async function installRuntime(input, deps = {}) {
1270
1380
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1271
1381
  const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1272
1382
  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."] : [];
1383
+ const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1273
1384
  return {
1274
1385
  runtime,
1275
1386
  runtimeMcpMode: configResult.runtimeMcpMode,
@@ -1284,7 +1395,8 @@ async function installRuntime(input, deps = {}) {
1284
1395
  signerAcknowledged: signerConsent?.acknowledged,
1285
1396
  localMcpAcknowledged: localMcpConsent?.acknowledged,
1286
1397
  activationCommand: configResult.activationCommand,
1287
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
1398
+ skillInstalled: skillInstall?.installed,
1399
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1288
1400
  };
1289
1401
  }
1290
1402
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1340,9 +1452,75 @@ async function configureClaudeCode(deps, localMcpCommand) {
1340
1452
  };
1341
1453
  }
1342
1454
  }
1455
+ async function configureClaudeCodeHosted(deps, input) {
1456
+ const runCommand = deps.runCommand ?? defaultRunCommand;
1457
+ const hostedJson = JSON.stringify({
1458
+ type: "http",
1459
+ url: input.hostedMcpUrl,
1460
+ headers: { Authorization: `Bearer ${input.apiKey}` }
1461
+ });
1462
+ const signerJson = JSON.stringify({
1463
+ type: "stdio",
1464
+ command: "npx",
1465
+ args: ["-y", signerPackageSpec(), "--credentials", input.signerPath],
1466
+ env: {}
1467
+ });
1468
+ try {
1469
+ await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
1470
+ await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1471
+ await runCommand("claude", ["mcp", "add-json", "haven", hostedJson, "--scope", "user"]);
1472
+ await runCommand("claude", ["mcp", "add-json", "haven-signer", signerJson, "--scope", "user"]);
1473
+ const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
1474
+ return {
1475
+ hostedConfigured: true,
1476
+ signerConfigured: true,
1477
+ localMcpConfigured: false,
1478
+ runtimeMcpMode: "hosted_plus_signer",
1479
+ target: "Claude Code MCP config",
1480
+ changed: true,
1481
+ restartRequired: true,
1482
+ messages: [
1483
+ "Updated hosted Haven MCP and local signer entries with Claude Code.",
1484
+ ...verified ? ["Verified Claude Code MCP entry."] : [],
1485
+ "After Haven approval, Haven tools should appear in your next Claude Code message. If they don't, restart the session to load them."
1486
+ ]
1487
+ };
1488
+ } catch (err) {
1489
+ return {
1490
+ hostedConfigured: false,
1491
+ signerConfigured: false,
1492
+ localMcpConfigured: false,
1493
+ runtimeMcpMode: "hosted_plus_signer",
1494
+ target: "Claude Code MCP config",
1495
+ changed: false,
1496
+ restartRequired: true,
1497
+ messages: [
1498
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
1499
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
1500
+ ],
1501
+ errorCode: "claude_code_config_failed"
1502
+ };
1503
+ }
1504
+ }
1343
1505
  async function defaultRunCommand(command, args) {
1344
1506
  await execFileAsync2(command, args, { timeout: 1e4 });
1345
1507
  }
1508
+ async function installClaudeSkill(homeDir) {
1509
+ try {
1510
+ const skillDir = path.resolve(homeDir ?? os.homedir(), ".claude", "skills", SKILL_FOLDER_NAME);
1511
+ await promises.mkdir(skillDir, { recursive: true });
1512
+ await promises.writeFile(path.join(skillDir, "SKILL.md"), HAVEN_SKILL_MD, "utf8");
1513
+ return {
1514
+ installed: true,
1515
+ messages: ["Installed the generic Haven payment skill (~/.claude/skills/haven-pay). It contains no secrets."]
1516
+ };
1517
+ } catch (err) {
1518
+ return {
1519
+ installed: false,
1520
+ messages: [`Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`]
1521
+ };
1522
+ }
1523
+ }
1346
1524
  function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
1347
1525
  if (mode === "local_stdio") {
1348
1526
  if (localMcpReady) return "local_stdio_mcp_ready";
@@ -1396,7 +1574,7 @@ function nextAction(runtime, restartMode, errorCode) {
1396
1574
  if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
1397
1575
  return "return_to_haven_for_wallet_approval_then_configure_runtime";
1398
1576
  }
1399
- function usesLocalMcp(runtime) {
1577
+ function supportsLocalMcp(runtime) {
1400
1578
  return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
1401
1579
  }
1402
1580
  async function prepareRuntimeForLocalMcp(input, deps) {
@@ -1424,7 +1602,7 @@ function localRuntimePrepareErrorCode(err) {
1424
1602
  }
1425
1603
 
1426
1604
  // src/runtime.ts
1427
- var CONNECTOR_VERSION = "0.1.2";
1605
+ var CONNECTOR_VERSION = "0.1.11-alpha.0";
1428
1606
  async function runConnect(options, deps = {}) {
1429
1607
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
1430
1608
  const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
@@ -1436,6 +1614,14 @@ async function runConnect(options, deps = {}) {
1436
1614
  const generateKey = deps.generateKey ?? generateDelegateKey;
1437
1615
  const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
1438
1616
  const installCapabilities = runtimeInstallCapabilities(options.runtime);
1617
+ if (options.localMcp) {
1618
+ const resolvedRuntime = normalizeRuntime(options.runtime);
1619
+ if (!supportsLocalMcp(resolvedRuntime)) {
1620
+ throw new Error(
1621
+ `--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.`
1622
+ );
1623
+ }
1624
+ }
1439
1625
  const setup = await api.resolveSetup({
1440
1626
  setupToken: options.setupToken,
1441
1627
  connectorVersion,
@@ -1494,7 +1680,8 @@ async function runConnect(options, deps = {}) {
1494
1680
  credentialDirectory: credentialPaths.directory,
1495
1681
  environmentLabel: options.environmentLabel ?? "Local workspace",
1496
1682
  ackSigner: options.ackSigner,
1497
- ackLocalTools: options.ackLocalTools
1683
+ ackLocalTools: options.ackLocalTools,
1684
+ localMcp: options.localMcp
1498
1685
  });
1499
1686
  printRuntimeInstall(runtimeInstall, log);
1500
1687
  try {
@@ -1509,6 +1696,7 @@ async function runConnect(options, deps = {}) {
1509
1696
  signerAcknowledged: runtimeInstall.signerAcknowledged,
1510
1697
  localMcpAcknowledged: runtimeInstall.localMcpAcknowledged,
1511
1698
  activationCommandAvailable: Boolean(runtimeInstall.activationCommand),
1699
+ skillInstalled: runtimeInstall.skillInstalled,
1512
1700
  probeResult: runtimeInstall.probeResult,
1513
1701
  restartRequired: runtimeInstall.restartRequired,
1514
1702
  nextUserAction: runtimeInstall.nextUserAction,
@@ -1590,6 +1778,8 @@ function parseArgs(argv, env = process.env) {
1590
1778
  } else if (arg === "--ack-signer") {
1591
1779
  options.ackSigner = true;
1592
1780
  options.ackLocalTools = true;
1781
+ } else if (arg === "--local" || arg === "--local-mcp") {
1782
+ options.localMcp = true;
1593
1783
  } else if (arg === "--version") {
1594
1784
  process.stdout.write(`${CONNECTOR_VERSION}
1595
1785
  `);
@@ -1628,6 +1818,8 @@ function helpText() {
1628
1818
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
1629
1819
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
1630
1820
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
1821
+ " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
1822
+ " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
1631
1823
  " --help Show this help.",
1632
1824
  "",
1633
1825
  "The connector never prints the private key and never sends it to Haven."