@haven_ai/connect 0.1.22-alpha.0 → 0.1.23-alpha.1

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
@@ -33,14 +33,54 @@ identity) plus a separate local signer. The API key identifies the agent; the
33
33
  locally held signer key and the user's approved Haven wallet rules remain the
34
34
  spending authority.
35
35
 
36
- | Runtime | Configuration written by setup | Reload behaviour |
36
+ | Runtime | Configuration written by setup | Activate the new entry |
37
37
  | --- | --- | --- |
38
- | Claude Code | User MCP registry | Start a new session |
39
- | Codex CLI / Codex Desktop | `~/.codex/config.toml` | Start a new session / restart the app |
40
- | Cursor | Cursor MCP configuration | Reloads automatically |
41
- | VS Code / VS Code Insiders | VS Code MCP configuration | Reloads automatically |
42
- | Claude Desktop | Claude Desktop MCP configuration | Restart the app |
43
- | Hermes Agent | `$HERMES_HOME/config.yaml` + `.env`, or `~/.hermes/config.yaml` + `.env` | Start a new session; gateway users run `/restart` |
38
+ | Claude Code | User MCP registry | Start a new Claude Code session. |
39
+ | Codex CLI | `~/.codex/config.toml` | Start a fresh session, for example `codex resume --last`. |
40
+ | Codex Desktop | `~/.codex/config.toml` | Quit and reopen the app. |
41
+ | Cursor | Cursor MCP configuration | Wait for hot reload; no app restart is required. |
42
+ | VS Code / VS Code Insiders | VS Code MCP configuration | Wait for hot reload; no app restart is required. |
43
+ | Claude Desktop | Claude Desktop MCP configuration | Quit and reopen the app. |
44
+ | Hermes Agent | `$HERMES_HOME/config.yaml` + `.env`, or `~/.hermes/config.yaml` + `.env` | Start a new session; gateway users run `/restart`. |
45
+
46
+ ## After setup
47
+
48
+ 1. Return to Haven and approve the agent rules. Approval — not restarting —
49
+ unlocks the Haven tools.
50
+ 2. Activate the runtime using the table above.
51
+ 3. In the activated runtime, run the read-only `haven_get_agent` and
52
+ `haven_get_allowances` tools to confirm the Haven wallet and live budget.
53
+ Do not sign, fund, or create a payment to verify setup.
54
+
55
+ ### Structured output for automation
56
+
57
+ Pass `--json` when a launcher needs a machine-readable completion record. Connect
58
+ writes progress and human recovery notes to stderr and exactly one JSON object
59
+ to stdout, with `schema_version: 1` and `outcome` set to `complete`,
60
+ `action_required`, or `failed`. The object includes runtime/topology status,
61
+ probe result, activation and next-action guidance, approval state/expiry (null
62
+ when the backend does not provide an approval expiry), and the two
63
+ read-only verification tools. It contains no API key, private key, credential
64
+ contents, full credential paths, or full delegate address. The same redacted
65
+ object is available to library callers as `runConnect(...).outcome`; the older
66
+ fields remain for additive compatibility.
67
+
68
+ For a recoverable install, configuration, probe, consent, or manual-runtime
69
+ condition, inspect `error.code` and `error.next_action`, then follow the safe
70
+ next action. A failed setup emits `outcome: "failed"` with a stable error code;
71
+ it never presents credential material as a recovery diagnostic.
72
+
73
+ If the setup challenge expires, return to Haven to start a fresh connection and
74
+ rerun Connect. If a runtime write, installation, or probe fails, follow the
75
+ structured `error.next_action` (or its human equivalent). The `other` runtime
76
+ is the manual exception: finish the secret-free file-reference setup it prints,
77
+ then start a fresh runtime session. Do not manually edit managed runtime
78
+ configuration or paste credentials into prompts, logs, or configuration files.
79
+
80
+ Connect abbreviates the public delegate address in normal output. Operators who
81
+ need its full public identifier can inspect the owner-only, non-secret
82
+ `agent.json` orientation file that Connect reports; do not inspect or share
83
+ `identity.json` or `signer.json` for diagnostics because they contain secrets.
44
84
 
45
85
  For Hermes, Connect stores the hosted-MCP API key in the matching owner-only
46
86
  `.env` file and keeps only `Bearer ${MCP_HAVEN_API_KEY}` in `config.yaml`.
package/dist/cli.cjs CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ var fs = require('fs');
5
+ var url = require('url');
6
+ var sdk = require('@haven_ai/sdk');
4
7
  var crypto = require('crypto');
5
8
  var ethers = require('ethers');
6
9
  var promises = require('fs/promises');
@@ -10,9 +13,9 @@ var child_process = require('child_process');
10
13
  var util = require('util');
11
14
  var yaml = require('yaml');
12
15
  var mcp = require('@haven_ai/mcp');
13
- var sdk = require('@haven_ai/sdk');
14
16
  var signer = require('@haven_ai/signer');
15
17
 
18
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
16
19
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
20
 
18
21
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
@@ -47,6 +50,10 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
47
50
  }
48
51
  })
49
52
  }),
53
+ getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
54
+ method: "GET",
55
+ headers: { Authorization: `Bearer ${apiKey}` }
56
+ }),
50
57
  updateInstallStatus: async (setupId, apiKey, input) => {
51
58
  await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
52
59
  method: "POST",
@@ -73,6 +80,14 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
73
80
  }
74
81
  };
75
82
  }
83
+ var ConnectRequestError = class extends Error {
84
+ constructor(message, status) {
85
+ super(message);
86
+ this.status = status;
87
+ this.name = "ConnectRequestError";
88
+ }
89
+ status;
90
+ };
76
91
  async function request(fetchImpl, url, init) {
77
92
  const response = await fetchImpl(url, {
78
93
  ...init,
@@ -85,7 +100,7 @@ async function request(fetchImpl, url, init) {
85
100
  const body = text ? JSON.parse(text) : null;
86
101
  if (!response.ok) {
87
102
  const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
88
- throw new Error(`Haven setup request failed: ${message}`);
103
+ throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
89
104
  }
90
105
  return body;
91
106
  }
@@ -181,11 +196,12 @@ async function writeCredentialFiles(input) {
181
196
  agentPath,
182
197
  {
183
198
  agent_id: input.agentId,
199
+ delegate_address: input.delegateAddress,
184
200
  safe_address: input.safeAddress,
185
201
  chain_id: input.chainId,
186
202
  network: input.network,
187
203
  agent_budget: input.agentBudget,
188
- note: "Non-secret orientation for the agent: identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
204
+ note: "Non-secret orientation for the agent: public delegate/Haven wallet identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
189
205
  },
190
206
  input.warn
191
207
  );
@@ -237,9 +253,9 @@ var MCP_RUNTIME_MANIFEST = {
237
253
  mcpPackage: "@haven_ai/mcp",
238
254
  mcpVersion: mcp.MCP_VERSION,
239
255
  sdkPackage: "@haven_ai/sdk",
240
- sdkVersion: "0.1.22-alpha.0",
256
+ sdkVersion: "0.1.23-alpha.1",
241
257
  signerPackage: "@haven_ai/signer",
242
- signerVersion: "0.1.22-alpha.0",
258
+ signerVersion: "0.1.23-alpha.1",
243
259
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
244
260
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
245
261
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -1401,55 +1417,64 @@ var RUNTIME_PROFILES = {
1401
1417
  id: "claude-code",
1402
1418
  label: "Claude Code",
1403
1419
  restartMode: "restart-session",
1404
- canWriteRuntimeConfig: true
1420
+ canWriteRuntimeConfig: true,
1421
+ activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
1405
1422
  },
1406
1423
  "codex-cli": {
1407
1424
  id: "codex-cli",
1408
1425
  label: "Codex CLI",
1409
1426
  restartMode: "restart-session",
1410
- canWriteRuntimeConfig: true
1427
+ canWriteRuntimeConfig: true,
1428
+ activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
1411
1429
  },
1412
1430
  "codex-desktop": {
1413
1431
  id: "codex-desktop",
1414
1432
  label: "Codex Desktop",
1415
1433
  restartMode: "restart-session",
1416
- canWriteRuntimeConfig: true
1434
+ canWriteRuntimeConfig: true,
1435
+ activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
1417
1436
  },
1418
1437
  cursor: {
1419
1438
  id: "cursor",
1420
1439
  label: "Cursor",
1421
1440
  restartMode: "hot-reload",
1422
- canWriteRuntimeConfig: true
1441
+ canWriteRuntimeConfig: true,
1442
+ activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
1423
1443
  },
1424
1444
  vscode: {
1425
1445
  id: "vscode",
1426
1446
  label: "VS Code",
1427
1447
  restartMode: "hot-reload",
1428
- canWriteRuntimeConfig: true
1448
+ canWriteRuntimeConfig: true,
1449
+ activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
1429
1450
  },
1430
1451
  "vscode-insiders": {
1431
1452
  id: "vscode-insiders",
1432
1453
  label: "VS Code Insiders",
1433
1454
  restartMode: "hot-reload",
1434
- canWriteRuntimeConfig: true
1455
+ canWriteRuntimeConfig: true,
1456
+ activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
1435
1457
  },
1436
1458
  "claude-desktop": {
1437
1459
  id: "claude-desktop",
1438
1460
  label: "Claude Desktop",
1439
1461
  restartMode: "restart-app",
1440
- canWriteRuntimeConfig: true
1462
+ canWriteRuntimeConfig: true,
1463
+ activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
1441
1464
  },
1442
1465
  hermes: {
1443
1466
  id: "hermes",
1444
1467
  label: "Hermes Agent",
1445
1468
  restartMode: "restart-session",
1446
- canWriteRuntimeConfig: true
1469
+ canWriteRuntimeConfig: true,
1470
+ activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
1447
1471
  },
1448
1472
  other: {
1449
1473
  id: "other",
1450
1474
  label: "Other agent runtime",
1451
1475
  restartMode: "manual",
1452
- canWriteRuntimeConfig: false
1476
+ canWriteRuntimeConfig: false,
1477
+ activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
1453
1478
  }
1454
1479
  };
1455
1480
  var RUNTIME_ALIASES = {
@@ -1501,8 +1526,9 @@ function restartRequiredForRuntime(runtime, env = process.env) {
1501
1526
  const mode = runtimeProfile(runtime, env).restartMode;
1502
1527
  return mode === "restart-session" || mode === "restart-app";
1503
1528
  }
1504
- function runtimeRequiresHardRestart(runtime) {
1505
- return runtime === "claude-desktop" || runtime === "codex-desktop";
1529
+ function runtimeVerificationInstruction(runtime) {
1530
+ const label = RUNTIME_PROFILES[runtime].label;
1531
+ return `In ${label}, run the read-only \`haven_get_agent\` and \`haven_get_allowances\` tools to confirm the Haven wallet and live budget. Do not sign, fund, or create a payment to verify setup.`;
1506
1532
  }
1507
1533
  function normalizeRuntimeName(runtime) {
1508
1534
  const key = runtime?.trim().toLowerCase();
@@ -1688,11 +1714,12 @@ async function installRuntime(input, deps = {}) {
1688
1714
  probeLocalSignerCredential(input.signerPath),
1689
1715
  localProbePromise
1690
1716
  ]);
1691
- const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
1717
+ const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
1692
1718
  const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
1693
1719
  const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
1694
1720
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1695
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1721
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
1722
+ const hostedProbeMessages = configResult.hostedConfigured && hostedProbe.status !== "ok" ? [`Hosted Haven MCP probe failed: ${hostedProbe.status}.`] : configResult.hostedConfigured ? ["Verified hosted Haven MCP tools with a read-only handshake."] : [];
1696
1723
  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."] : [];
1697
1724
  const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1698
1725
  return {
@@ -1711,7 +1738,7 @@ async function installRuntime(input, deps = {}) {
1711
1738
  activationCommand: configResult.activationCommand,
1712
1739
  skillInstalled: skillInstall?.installed,
1713
1740
  signerRuntimePrepared,
1714
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1741
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1715
1742
  };
1716
1743
  }
1717
1744
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1872,6 +1899,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
1872
1899
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
1873
1900
  return void 0;
1874
1901
  }
1902
+ function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
1903
+ if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
1904
+ return `hosted_mcp_probe_${hostedProbeStatus}`;
1905
+ }
1875
1906
  function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
1876
1907
  if (!signerCredentialReady) return "local_signer_credential_unavailable";
1877
1908
  if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
@@ -1923,13 +1954,17 @@ function localRuntimePrepareErrorCode(err) {
1923
1954
  }
1924
1955
 
1925
1956
  // src/runtime.ts
1926
- var CONNECTOR_VERSION = "0.1.22-alpha.0";
1957
+ var CONNECTOR_VERSION = "0.1.23-alpha.1";
1958
+ var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
1927
1959
  async function runConnect(options, deps = {}) {
1928
1960
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
1929
1961
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
1930
1962
  const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
1931
- const log = secureLogger(deps.log ?? ((message) => process.stdout.write(`${message}
1932
- `)));
1963
+ const log = secureLogger(
1964
+ deps.log ?? ((message) => process.stdout.write(`${message}
1965
+ `)),
1966
+ deps.redactPaths === true
1967
+ );
1933
1968
  const writeCredentials = deps.writeCredentials ?? writeCredentialFiles;
1934
1969
  const preflightStorage = deps.preflightStorage ?? preflightCredentialStorage;
1935
1970
  const runRuntimeInstall = deps.installRuntime ?? installRuntime;
@@ -1950,6 +1985,7 @@ async function runConnect(options, deps = {}) {
1950
1985
  connectorVersion,
1951
1986
  runtime: options.runtime
1952
1987
  });
1988
+ assertSetupChallengeIsUsable(setup.challenge.expires_at);
1953
1989
  printSetupSummary(setup, log);
1954
1990
  await preflightStorage({ baseDir: options.credentialsDir, warn: log });
1955
1991
  log("Checked local credential storage \u2014 all clear.");
@@ -1958,21 +1994,31 @@ async function runConnect(options, deps = {}) {
1958
1994
  log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
1959
1995
  const proofSignature = await localKey.signChallenge(setup.challenge.message);
1960
1996
  log("Introducing your agent to Haven\u2026");
1961
- const registration = await api.registerSetup({
1962
- setupToken: options.setupToken,
1963
- connectorVersion,
1964
- runtime: options.runtime,
1965
- challengeId: setup.challenge.id,
1966
- delegateAddress: localKey.address,
1967
- proofSignature,
1968
- apiKeyHash: hashAgentApiKey(localApiKey),
1969
- apiKeyPrefix: agentApiKeyPrefix(localApiKey),
1970
- connectorContext: {
1971
- environment_label: options.environmentLabel ?? "Local workspace",
1972
- config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
1973
- },
1974
- installCapabilities
1975
- });
1997
+ let registration;
1998
+ try {
1999
+ registration = await api.registerSetup({
2000
+ setupToken: options.setupToken,
2001
+ connectorVersion,
2002
+ runtime: options.runtime,
2003
+ challengeId: setup.challenge.id,
2004
+ delegateAddress: localKey.address,
2005
+ proofSignature,
2006
+ apiKeyHash: hashAgentApiKey(localApiKey),
2007
+ apiKeyPrefix: agentApiKeyPrefix(localApiKey),
2008
+ connectorContext: {
2009
+ environment_label: options.environmentLabel ?? "Local workspace",
2010
+ config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
2011
+ },
2012
+ installCapabilities
2013
+ });
2014
+ } catch (err) {
2015
+ if (isExpiredSetupChallenge(err)) {
2016
+ throw new Error(
2017
+ "The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new Connect command. Do not reuse or paste credentials."
2018
+ );
2019
+ }
2020
+ throw err;
2021
+ }
1976
2022
  log(`Registered signing address with Haven: ${shortAddress(registration.delegate_address)}.`);
1977
2023
  log("Tucking your credentials away safely on disk\u2026");
1978
2024
  const credentialPaths = await writeCredentials({
@@ -2022,6 +2068,9 @@ async function runConnect(options, deps = {}) {
2022
2068
  } else {
2023
2069
  log("Haven setup on this machine is complete.");
2024
2070
  }
2071
+ if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
2072
+ await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
2073
+ }
2025
2074
  printNextSteps(runtimeInstall, log);
2026
2075
  try {
2027
2076
  await api.updateInstallStatus(registration.setup_id, localApiKey, {
@@ -2049,7 +2098,69 @@ async function runConnect(options, deps = {}) {
2049
2098
  setupId: registration.setup_id,
2050
2099
  agentId: registration.agent_id,
2051
2100
  delegateAddress: registration.delegate_address,
2052
- credentialPaths
2101
+ credentialPaths,
2102
+ outcome: completionOutcome({
2103
+ runtimeInstall,
2104
+ delegateAddress: registration.delegate_address,
2105
+ setupChallengeExpiresAt: setup.challenge.expires_at,
2106
+ approvalRequired: registration.agent_status === "pending_approval"
2107
+ })
2108
+ };
2109
+ }
2110
+ function completionOutcome(input) {
2111
+ const { runtimeInstall } = input;
2112
+ const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
2113
+ const nextAction2 = runtimeInstall.nextUserAction;
2114
+ const outcome = {
2115
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2116
+ outcome: runtimeInstall.errorCode ? "action_required" : "complete",
2117
+ runtime: runtimeInstall.runtime,
2118
+ topology: runtimeInstall.runtimeMcpMode,
2119
+ configuration: {
2120
+ hosted_mcp: runtimeInstall.hostedMcpConfigured,
2121
+ local_signer: runtimeInstall.localSignerConfigured,
2122
+ local_mcp: runtimeInstall.localMcpConfigured
2123
+ },
2124
+ probe: { result: runtimeInstall.probeResult },
2125
+ activation: {
2126
+ restart_required: runtimeInstall.restartRequired,
2127
+ instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
2128
+ },
2129
+ next_action: nextAction2,
2130
+ approval: { required: input.approvalRequired, expires_at: null },
2131
+ verification: {
2132
+ tools: ["haven_get_agent", "haven_get_allowances"],
2133
+ instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
2134
+ },
2135
+ // The backend contract supplies a 20-byte address. If an unexpected
2136
+ // malformed value arrives, do not echo it into an automation-facing
2137
+ // record; the human log has already been redacted separately.
2138
+ delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
2139
+ ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
2140
+ ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
2141
+ };
2142
+ return outcome;
2143
+ }
2144
+ function failedConnectOutcome(runtimeHint, error) {
2145
+ const message = error instanceof Error ? error.message : "";
2146
+ const code = /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
2147
+ const runtime = normalizeRuntime(runtimeHint);
2148
+ const nextAction2 = code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
2149
+ return {
2150
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2151
+ outcome: "failed",
2152
+ runtime,
2153
+ topology: "unknown",
2154
+ configuration: { hosted_mcp: false, local_signer: false, local_mcp: false },
2155
+ probe: { result: "not_run" },
2156
+ activation: { restart_required: false, instruction: "Resolve the reported problem before activating Haven tools." },
2157
+ next_action: nextAction2,
2158
+ approval: { required: false, expires_at: null },
2159
+ verification: {
2160
+ tools: ["haven_get_agent", "haven_get_allowances"],
2161
+ instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
2162
+ },
2163
+ error: { code, next_action: nextAction2 }
2053
2164
  };
2054
2165
  }
2055
2166
  function printSetupSummary(setup, log) {
@@ -2062,10 +2173,26 @@ function printSetupSummary(setup, log) {
2062
2173
  );
2063
2174
  }
2064
2175
  }
2065
- log(`Setup challenge expires at ${setup.challenge.expires_at}.`);
2176
+ log(`Setup challenge expires at ${setup.challenge.expires_at}. If it expires, return to Haven for a fresh setup and rerun Connect \u2014 do not reuse or paste credentials.`);
2177
+ }
2178
+ function assertSetupChallengeIsUsable(expiresAt) {
2179
+ const expiresAtMs = Date.parse(expiresAt);
2180
+ if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
2181
+ throw new Error(
2182
+ "This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
2183
+ );
2066
2184
  }
2067
- function secureLogger(log) {
2068
- return (message) => log(redactSecrets(message));
2185
+ function isExpiredSetupChallenge(err) {
2186
+ return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
2187
+ }
2188
+ function secureLogger(log, redactPaths = false) {
2189
+ return (message) => {
2190
+ let safe = redactSecrets(message);
2191
+ if (redactPaths) {
2192
+ safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
2193
+ }
2194
+ log(safe);
2195
+ };
2069
2196
  }
2070
2197
  function printRuntimeInstall(result, log) {
2071
2198
  for (const message of result.messages) log(message);
@@ -2082,17 +2209,85 @@ function printRuntimeInstall(result, log) {
2082
2209
  log("Local Haven signer still needs runtime setup.");
2083
2210
  }
2084
2211
  }
2085
- function printNextSteps(result, log) {
2086
- log("Next: approve the agent rules in Haven. No Haven tools appear until you approve \u2014 restart or not.");
2087
- if (result.restartRequired) {
2088
- if (runtimeRequiresHardRestart(result.runtime)) {
2089
- log("After you approve, restart this agent so it can load Haven tools.");
2090
- } else {
2091
- log("After you approve, restart this agent \u2014 your current session won't load the Haven tools until you do.");
2212
+ function formatAtomicAmount(atomic, decimals) {
2213
+ const s = atomic.toString().padStart(decimals + 1, "0");
2214
+ const intPart = s.slice(0, s.length - decimals) || "0";
2215
+ const fracPart = s.slice(s.length - decimals).replace(/0+$/, "");
2216
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
2217
+ }
2218
+ function describeResetPeriod(resetPeriodMin) {
2219
+ if (resetPeriodMin === 1440) return "per day";
2220
+ if (resetPeriodMin === 60) return "per hour";
2221
+ if (resetPeriodMin === 0) return "with no automatic reset";
2222
+ return `per ${resetPeriodMin} minutes`;
2223
+ }
2224
+ function describeApprovedBudget(budget) {
2225
+ const token = sdk.resolveTokenFromAddress(budget.token_address);
2226
+ const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
2227
+ return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
2228
+ }
2229
+ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2230
+ const intervalMs = options.intervalMs ?? 5e3;
2231
+ const timeoutMs = options.timeoutMs ?? 18e4;
2232
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
2233
+ const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
2234
+ const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
2235
+ log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
2236
+ for (let i = 0; i < maxPolls; i++) {
2237
+ await sleep(intervalMs);
2238
+ let status;
2239
+ try {
2240
+ status = await api.getConnectorStatus(setupId, apiKey);
2241
+ } catch (err) {
2242
+ if (err instanceof ConnectRequestError && (err.status === 401 || err.status === 404)) {
2243
+ log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
2244
+ return "ended";
2245
+ }
2246
+ continue;
2247
+ }
2248
+ if (status.status === "active") {
2249
+ log(
2250
+ status.approved_budget ? `Budget approved \u{1F389} \u2014 I can now spend up to ${describeApprovedBudget(status.approved_budget)} from your Haven wallet.` : "Budget approved \u{1F389} \u2014 the agent can now spend within its Haven rules."
2251
+ );
2252
+ return "approved";
2253
+ }
2254
+ if (status.status === "cancelled" || status.status === "expired" || status.status === "failed") {
2255
+ log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
2256
+ return "ended";
2257
+ }
2258
+ if ((i + 1) % remindEvery === 0) {
2259
+ log("Still waiting for budget approval in Haven\u2026");
2092
2260
  }
2093
- } else {
2094
- log("After you approve, the Haven tools should appear in this runtime shortly.");
2095
2261
  }
2262
+ log(
2263
+ "Budget approval is still pending in Haven. Approve it in the dashboard whenever you are ready \u2014 the agent tools unlock the moment you do. Verify later with the read-only haven_get_agent tool."
2264
+ );
2265
+ return "pending";
2266
+ }
2267
+ function completionHandoffLines(result) {
2268
+ if (result.errorCode === "manual_runtime_setup_required") {
2269
+ return [
2270
+ "Next steps:",
2271
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2272
+ "2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
2273
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2274
+ ];
2275
+ }
2276
+ if (result.errorCode) {
2277
+ return [
2278
+ "Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new Connect command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
2279
+ ];
2280
+ }
2281
+ const profile = runtimeProfile(result.runtime);
2282
+ return [
2283
+ "Next steps:",
2284
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2285
+ `2. ${profile.activationInstruction}`,
2286
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2287
+ ];
2288
+ }
2289
+ function printNextSteps(result, log) {
2290
+ for (const line of completionHandoffLines(result)) log(line);
2096
2291
  }
2097
2292
 
2098
2293
  // src/args.ts
@@ -2102,10 +2297,13 @@ function parseArgs(argv, env = process.env) {
2102
2297
  connectorVersion: CONNECTOR_VERSION
2103
2298
  };
2104
2299
  let help = false;
2300
+ let json = false;
2105
2301
  for (let i = 0; i < argv.length; i += 1) {
2106
2302
  const arg = argv[i];
2107
2303
  if (arg === "--help" || arg === "-h") {
2108
2304
  help = true;
2305
+ } else if (arg === "--json") {
2306
+ json = true;
2109
2307
  } else if (arg === "--setup" || arg === "--setup-token") {
2110
2308
  options.setupToken = requireValue(argv, ++i, arg);
2111
2309
  } else if (arg === "--api" || arg === "--api-url") {
@@ -2132,7 +2330,7 @@ function parseArgs(argv, env = process.env) {
2132
2330
  }
2133
2331
  }
2134
2332
  if (help) {
2135
- return { options, help };
2333
+ return { options, help, json };
2136
2334
  }
2137
2335
  if (!options.setupToken) {
2138
2336
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -2141,7 +2339,7 @@ function parseArgs(argv, env = process.env) {
2141
2339
  throw new Error("Missing --api <Haven API URL>.");
2142
2340
  }
2143
2341
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
2144
- return { options, help };
2342
+ return { options, help, json };
2145
2343
  }
2146
2344
  function helpText() {
2147
2345
  return [
@@ -2163,9 +2361,10 @@ function helpText() {
2163
2361
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
2164
2362
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
2165
2363
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
2364
+ " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
2166
2365
  " --help Show this help.",
2167
2366
  "",
2168
- "The connector never prints the private key and never sends it to Haven."
2367
+ "The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
2169
2368
  ].join("\n");
2170
2369
  }
2171
2370
  function requireValue(argv, index, option) {
@@ -2177,19 +2376,67 @@ function requireValue(argv, index, option) {
2177
2376
  }
2178
2377
 
2179
2378
  // src/cli.ts
2180
- async function main() {
2181
- const parsed = parseArgs(process.argv.slice(2));
2379
+ async function runCli(argv, io = {
2380
+ stdout: (message) => process.stdout.write(message),
2381
+ stderr: (message) => process.stderr.write(message)
2382
+ }) {
2383
+ const wantsJson = argv.includes("--json");
2384
+ let parsed;
2385
+ try {
2386
+ parsed = parseArgs(argv);
2387
+ } catch (err) {
2388
+ if (wantsJson) {
2389
+ io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
2390
+ `);
2391
+ } else {
2392
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2393
+ `);
2394
+ }
2395
+ return 1;
2396
+ }
2182
2397
  if (parsed.help) {
2183
- process.stdout.write(`${helpText()}
2398
+ io.stdout(`${helpText()}
2184
2399
  `);
2185
- return;
2400
+ return 0;
2186
2401
  }
2187
- await runConnect(parsed.options);
2188
- }
2189
- main().catch((err) => {
2190
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
2402
+ try {
2403
+ const result = await runConnect(
2404
+ { ...parsed.options, waitForApproval: !parsed.json },
2405
+ {
2406
+ log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
2407
+ `),
2408
+ redactPaths: parsed.json
2409
+ }
2410
+ );
2411
+ if (parsed.json) io.stdout(`${JSON.stringify(result.outcome)}
2412
+ `);
2413
+ return 0;
2414
+ } catch (err) {
2415
+ if (parsed.json) {
2416
+ io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
2417
+ `);
2418
+ } else {
2419
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2191
2420
  `);
2192
- process.exit(1);
2193
- });
2421
+ }
2422
+ return 1;
2423
+ }
2424
+ }
2425
+ async function main() {
2426
+ const exitCode = await runCli(process.argv.slice(2));
2427
+ if (exitCode !== 0) process.exitCode = exitCode;
2428
+ }
2429
+ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))) {
2430
+ if (!argvPath) return false;
2431
+ try {
2432
+ return fs.realpathSync(argvPath) === fs.realpathSync(url.fileURLToPath(moduleUrl));
2433
+ } catch {
2434
+ return url.pathToFileURL(argvPath).href === moduleUrl;
2435
+ }
2436
+ }
2437
+ if (isCliEntrypoint()) void main();
2438
+
2439
+ exports.isCliEntrypoint = isCliEntrypoint;
2440
+ exports.runCli = runCli;
2194
2441
  //# sourceMappingURL=cli.cjs.map
2195
2442
  //# sourceMappingURL=cli.cjs.map