@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/dist/cli.d.cts CHANGED
@@ -1 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ interface CliIo {
3
+ stdout: (message: string) => void;
4
+ stderr: (message: string) => void;
5
+ }
6
+ declare function runCli(argv: string[], io?: CliIo): Promise<number>;
7
+ /**
8
+ * npm executes package bins through a `node_modules/.bin` symlink. Node keeps
9
+ * that symlink in `process.argv[1]`, whereas `import.meta.url` identifies the
10
+ * real module path. Resolve both sides before comparing so a published
11
+ * `haven-connect` bin starts, while an ordinary `runCli` import remains inert.
12
+ */
13
+ declare function isCliEntrypoint(argvPath?: string | undefined, moduleUrl?: string): boolean;
14
+
15
+ export { type CliIo, isCliEntrypoint, runCli };
package/dist/cli.d.ts CHANGED
@@ -1 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ interface CliIo {
3
+ stdout: (message: string) => void;
4
+ stderr: (message: string) => void;
5
+ }
6
+ declare function runCli(argv: string[], io?: CliIo): Promise<number>;
7
+ /**
8
+ * npm executes package bins through a `node_modules/.bin` symlink. Node keeps
9
+ * that symlink in `process.argv[1]`, whereas `import.meta.url` identifies the
10
+ * real module path. Resolve both sides before comparing so a published
11
+ * `haven-connect` bin starts, while an ordinary `runCli` import remains inert.
12
+ */
13
+ declare function isCliEntrypoint(argvPath?: string | undefined, moduleUrl?: string): boolean;
14
+
15
+ export { type CliIo, isCliEntrypoint, runCli };
package/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'fs';
3
+ import { fileURLToPath, pathToFileURL } from 'url';
4
+ import { HAVEN_MINIMUM_NODE_VERSION, isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, HAVEN_SKILL_MD, resolveTokenFromAddress } from '@haven_ai/sdk';
2
5
  import crypto from 'crypto';
3
6
  import { Wallet } from 'ethers';
4
7
  import { mkdir, rm, chmod, access, writeFile, readFile, unlink } from 'fs/promises';
@@ -8,7 +11,6 @@ import { execFile, spawn } from 'child_process';
8
11
  import { promisify } from 'util';
9
12
  import { parseDocument, isMap, stringify } from 'yaml';
10
13
  import { registeredToolNames, MCP_VERSION, ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient } from '@haven_ai/mcp';
11
- import { HAVEN_MINIMUM_NODE_VERSION, isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, HAVEN_SKILL_MD } from '@haven_ai/sdk';
12
14
  import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
13
15
 
14
16
  // src/api.ts
@@ -41,6 +43,10 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
41
43
  }
42
44
  })
43
45
  }),
46
+ getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
47
+ method: "GET",
48
+ headers: { Authorization: `Bearer ${apiKey}` }
49
+ }),
44
50
  updateInstallStatus: async (setupId, apiKey, input) => {
45
51
  await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
46
52
  method: "POST",
@@ -67,6 +73,14 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
67
73
  }
68
74
  };
69
75
  }
76
+ var ConnectRequestError = class extends Error {
77
+ constructor(message, status) {
78
+ super(message);
79
+ this.status = status;
80
+ this.name = "ConnectRequestError";
81
+ }
82
+ status;
83
+ };
70
84
  async function request(fetchImpl, url, init) {
71
85
  const response = await fetchImpl(url, {
72
86
  ...init,
@@ -79,7 +93,7 @@ async function request(fetchImpl, url, init) {
79
93
  const body = text ? JSON.parse(text) : null;
80
94
  if (!response.ok) {
81
95
  const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
82
- throw new Error(`Haven setup request failed: ${message}`);
96
+ throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
83
97
  }
84
98
  return body;
85
99
  }
@@ -175,11 +189,12 @@ async function writeCredentialFiles(input) {
175
189
  agentPath,
176
190
  {
177
191
  agent_id: input.agentId,
192
+ delegate_address: input.delegateAddress,
178
193
  safe_address: input.safeAddress,
179
194
  chain_id: input.chainId,
180
195
  network: input.network,
181
196
  agent_budget: input.agentBudget,
182
- 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."
197
+ 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."
183
198
  },
184
199
  input.warn
185
200
  );
@@ -231,9 +246,9 @@ var MCP_RUNTIME_MANIFEST = {
231
246
  mcpPackage: "@haven_ai/mcp",
232
247
  mcpVersion: MCP_VERSION,
233
248
  sdkPackage: "@haven_ai/sdk",
234
- sdkVersion: "0.1.22-alpha.0",
249
+ sdkVersion: "0.1.23-alpha.1",
235
250
  signerPackage: "@haven_ai/signer",
236
- signerVersion: "0.1.22-alpha.0",
251
+ signerVersion: "0.1.23-alpha.1",
237
252
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
238
253
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
239
254
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -1395,55 +1410,64 @@ var RUNTIME_PROFILES = {
1395
1410
  id: "claude-code",
1396
1411
  label: "Claude Code",
1397
1412
  restartMode: "restart-session",
1398
- canWriteRuntimeConfig: true
1413
+ canWriteRuntimeConfig: true,
1414
+ activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
1399
1415
  },
1400
1416
  "codex-cli": {
1401
1417
  id: "codex-cli",
1402
1418
  label: "Codex CLI",
1403
1419
  restartMode: "restart-session",
1404
- canWriteRuntimeConfig: true
1420
+ canWriteRuntimeConfig: true,
1421
+ activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
1405
1422
  },
1406
1423
  "codex-desktop": {
1407
1424
  id: "codex-desktop",
1408
1425
  label: "Codex Desktop",
1409
1426
  restartMode: "restart-session",
1410
- canWriteRuntimeConfig: true
1427
+ canWriteRuntimeConfig: true,
1428
+ activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
1411
1429
  },
1412
1430
  cursor: {
1413
1431
  id: "cursor",
1414
1432
  label: "Cursor",
1415
1433
  restartMode: "hot-reload",
1416
- canWriteRuntimeConfig: true
1434
+ canWriteRuntimeConfig: true,
1435
+ activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
1417
1436
  },
1418
1437
  vscode: {
1419
1438
  id: "vscode",
1420
1439
  label: "VS Code",
1421
1440
  restartMode: "hot-reload",
1422
- canWriteRuntimeConfig: true
1441
+ canWriteRuntimeConfig: true,
1442
+ activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
1423
1443
  },
1424
1444
  "vscode-insiders": {
1425
1445
  id: "vscode-insiders",
1426
1446
  label: "VS Code Insiders",
1427
1447
  restartMode: "hot-reload",
1428
- canWriteRuntimeConfig: true
1448
+ canWriteRuntimeConfig: true,
1449
+ activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
1429
1450
  },
1430
1451
  "claude-desktop": {
1431
1452
  id: "claude-desktop",
1432
1453
  label: "Claude Desktop",
1433
1454
  restartMode: "restart-app",
1434
- canWriteRuntimeConfig: true
1455
+ canWriteRuntimeConfig: true,
1456
+ activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
1435
1457
  },
1436
1458
  hermes: {
1437
1459
  id: "hermes",
1438
1460
  label: "Hermes Agent",
1439
1461
  restartMode: "restart-session",
1440
- canWriteRuntimeConfig: true
1462
+ canWriteRuntimeConfig: true,
1463
+ activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
1441
1464
  },
1442
1465
  other: {
1443
1466
  id: "other",
1444
1467
  label: "Other agent runtime",
1445
1468
  restartMode: "manual",
1446
- canWriteRuntimeConfig: false
1469
+ canWriteRuntimeConfig: false,
1470
+ activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
1447
1471
  }
1448
1472
  };
1449
1473
  var RUNTIME_ALIASES = {
@@ -1495,8 +1519,9 @@ function restartRequiredForRuntime(runtime, env = process.env) {
1495
1519
  const mode = runtimeProfile(runtime, env).restartMode;
1496
1520
  return mode === "restart-session" || mode === "restart-app";
1497
1521
  }
1498
- function runtimeRequiresHardRestart(runtime) {
1499
- return runtime === "claude-desktop" || runtime === "codex-desktop";
1522
+ function runtimeVerificationInstruction(runtime) {
1523
+ const label = RUNTIME_PROFILES[runtime].label;
1524
+ 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.`;
1500
1525
  }
1501
1526
  function normalizeRuntimeName(runtime) {
1502
1527
  const key = runtime?.trim().toLowerCase();
@@ -1682,11 +1707,12 @@ async function installRuntime(input, deps = {}) {
1682
1707
  probeLocalSignerCredential(input.signerPath),
1683
1708
  localProbePromise
1684
1709
  ]);
1685
- const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
1710
+ const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
1686
1711
  const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
1687
1712
  const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
1688
1713
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1689
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
1714
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
1715
+ 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."] : [];
1690
1716
  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."] : [];
1691
1717
  const skillInstall = runtime === "claude-code" && !configResult.errorCode ? await installClaudeSkill(deps.homeDir) : void 0;
1692
1718
  return {
@@ -1705,7 +1731,7 @@ async function installRuntime(input, deps = {}) {
1705
1731
  activationCommand: configResult.activationCommand,
1706
1732
  skillInstalled: skillInstall?.installed,
1707
1733
  signerRuntimePrepared,
1708
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1734
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1709
1735
  };
1710
1736
  }
1711
1737
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1866,6 +1892,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
1866
1892
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
1867
1893
  return void 0;
1868
1894
  }
1895
+ function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
1896
+ if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
1897
+ return `hosted_mcp_probe_${hostedProbeStatus}`;
1898
+ }
1869
1899
  function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
1870
1900
  if (!signerCredentialReady) return "local_signer_credential_unavailable";
1871
1901
  if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
@@ -1917,13 +1947,17 @@ function localRuntimePrepareErrorCode(err) {
1917
1947
  }
1918
1948
 
1919
1949
  // src/runtime.ts
1920
- var CONNECTOR_VERSION = "0.1.22-alpha.0";
1950
+ var CONNECTOR_VERSION = "0.1.23-alpha.1";
1951
+ var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
1921
1952
  async function runConnect(options, deps = {}) {
1922
1953
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
1923
1954
  const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
1924
1955
  const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
1925
- const log = secureLogger(deps.log ?? ((message) => process.stdout.write(`${message}
1926
- `)));
1956
+ const log = secureLogger(
1957
+ deps.log ?? ((message) => process.stdout.write(`${message}
1958
+ `)),
1959
+ deps.redactPaths === true
1960
+ );
1927
1961
  const writeCredentials = deps.writeCredentials ?? writeCredentialFiles;
1928
1962
  const preflightStorage = deps.preflightStorage ?? preflightCredentialStorage;
1929
1963
  const runRuntimeInstall = deps.installRuntime ?? installRuntime;
@@ -1944,6 +1978,7 @@ async function runConnect(options, deps = {}) {
1944
1978
  connectorVersion,
1945
1979
  runtime: options.runtime
1946
1980
  });
1981
+ assertSetupChallengeIsUsable(setup.challenge.expires_at);
1947
1982
  printSetupSummary(setup, log);
1948
1983
  await preflightStorage({ baseDir: options.credentialsDir, warn: log });
1949
1984
  log("Checked local credential storage \u2014 all clear.");
@@ -1952,21 +1987,31 @@ async function runConnect(options, deps = {}) {
1952
1987
  log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
1953
1988
  const proofSignature = await localKey.signChallenge(setup.challenge.message);
1954
1989
  log("Introducing your agent to Haven\u2026");
1955
- const registration = await api.registerSetup({
1956
- setupToken: options.setupToken,
1957
- connectorVersion,
1958
- runtime: options.runtime,
1959
- challengeId: setup.challenge.id,
1960
- delegateAddress: localKey.address,
1961
- proofSignature,
1962
- apiKeyHash: hashAgentApiKey(localApiKey),
1963
- apiKeyPrefix: agentApiKeyPrefix(localApiKey),
1964
- connectorContext: {
1965
- environment_label: options.environmentLabel ?? "Local workspace",
1966
- config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
1967
- },
1968
- installCapabilities
1969
- });
1990
+ let registration;
1991
+ try {
1992
+ registration = await api.registerSetup({
1993
+ setupToken: options.setupToken,
1994
+ connectorVersion,
1995
+ runtime: options.runtime,
1996
+ challengeId: setup.challenge.id,
1997
+ delegateAddress: localKey.address,
1998
+ proofSignature,
1999
+ apiKeyHash: hashAgentApiKey(localApiKey),
2000
+ apiKeyPrefix: agentApiKeyPrefix(localApiKey),
2001
+ connectorContext: {
2002
+ environment_label: options.environmentLabel ?? "Local workspace",
2003
+ config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
2004
+ },
2005
+ installCapabilities
2006
+ });
2007
+ } catch (err) {
2008
+ if (isExpiredSetupChallenge(err)) {
2009
+ throw new Error(
2010
+ "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."
2011
+ );
2012
+ }
2013
+ throw err;
2014
+ }
1970
2015
  log(`Registered signing address with Haven: ${shortAddress(registration.delegate_address)}.`);
1971
2016
  log("Tucking your credentials away safely on disk\u2026");
1972
2017
  const credentialPaths = await writeCredentials({
@@ -2016,6 +2061,9 @@ async function runConnect(options, deps = {}) {
2016
2061
  } else {
2017
2062
  log("Haven setup on this machine is complete.");
2018
2063
  }
2064
+ if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
2065
+ await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
2066
+ }
2019
2067
  printNextSteps(runtimeInstall, log);
2020
2068
  try {
2021
2069
  await api.updateInstallStatus(registration.setup_id, localApiKey, {
@@ -2043,7 +2091,69 @@ async function runConnect(options, deps = {}) {
2043
2091
  setupId: registration.setup_id,
2044
2092
  agentId: registration.agent_id,
2045
2093
  delegateAddress: registration.delegate_address,
2046
- credentialPaths
2094
+ credentialPaths,
2095
+ outcome: completionOutcome({
2096
+ runtimeInstall,
2097
+ delegateAddress: registration.delegate_address,
2098
+ setupChallengeExpiresAt: setup.challenge.expires_at,
2099
+ approvalRequired: registration.agent_status === "pending_approval"
2100
+ })
2101
+ };
2102
+ }
2103
+ function completionOutcome(input) {
2104
+ const { runtimeInstall } = input;
2105
+ const manualSetup = runtimeInstall.errorCode === "manual_runtime_setup_required";
2106
+ const nextAction2 = runtimeInstall.nextUserAction;
2107
+ const outcome = {
2108
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2109
+ outcome: runtimeInstall.errorCode ? "action_required" : "complete",
2110
+ runtime: runtimeInstall.runtime,
2111
+ topology: runtimeInstall.runtimeMcpMode,
2112
+ configuration: {
2113
+ hosted_mcp: runtimeInstall.hostedMcpConfigured,
2114
+ local_signer: runtimeInstall.localSignerConfigured,
2115
+ local_mcp: runtimeInstall.localMcpConfigured
2116
+ },
2117
+ probe: { result: runtimeInstall.probeResult },
2118
+ activation: {
2119
+ restart_required: runtimeInstall.restartRequired,
2120
+ 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
2121
+ },
2122
+ next_action: nextAction2,
2123
+ approval: { required: input.approvalRequired, expires_at: null },
2124
+ verification: {
2125
+ tools: ["haven_get_agent", "haven_get_allowances"],
2126
+ instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
2127
+ },
2128
+ // The backend contract supplies a 20-byte address. If an unexpected
2129
+ // malformed value arrives, do not echo it into an automation-facing
2130
+ // record; the human log has already been redacted separately.
2131
+ delegate_address: /^0x[0-9a-fA-F]{40}$/.test(input.delegateAddress) ? shortAddress(input.delegateAddress) : "[delegate-address-redacted]",
2132
+ ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
2133
+ ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
2134
+ };
2135
+ return outcome;
2136
+ }
2137
+ function failedConnectOutcome(runtimeHint, error) {
2138
+ const message = error instanceof Error ? error.message : "";
2139
+ 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";
2140
+ const runtime = normalizeRuntime(runtimeHint);
2141
+ 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";
2142
+ return {
2143
+ schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2144
+ outcome: "failed",
2145
+ runtime,
2146
+ topology: "unknown",
2147
+ configuration: { hosted_mcp: false, local_signer: false, local_mcp: false },
2148
+ probe: { result: "not_run" },
2149
+ activation: { restart_required: false, instruction: "Resolve the reported problem before activating Haven tools." },
2150
+ next_action: nextAction2,
2151
+ approval: { required: false, expires_at: null },
2152
+ verification: {
2153
+ tools: ["haven_get_agent", "haven_get_allowances"],
2154
+ instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
2155
+ },
2156
+ error: { code, next_action: nextAction2 }
2047
2157
  };
2048
2158
  }
2049
2159
  function printSetupSummary(setup, log) {
@@ -2056,10 +2166,26 @@ function printSetupSummary(setup, log) {
2056
2166
  );
2057
2167
  }
2058
2168
  }
2059
- log(`Setup challenge expires at ${setup.challenge.expires_at}.`);
2169
+ 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.`);
2170
+ }
2171
+ function assertSetupChallengeIsUsable(expiresAt) {
2172
+ const expiresAtMs = Date.parse(expiresAt);
2173
+ if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
2174
+ throw new Error(
2175
+ "This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
2176
+ );
2177
+ }
2178
+ function isExpiredSetupChallenge(err) {
2179
+ return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
2060
2180
  }
2061
- function secureLogger(log) {
2062
- return (message) => log(redactSecrets(message));
2181
+ function secureLogger(log, redactPaths = false) {
2182
+ return (message) => {
2183
+ let safe = redactSecrets(message);
2184
+ if (redactPaths) {
2185
+ safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
2186
+ }
2187
+ log(safe);
2188
+ };
2063
2189
  }
2064
2190
  function printRuntimeInstall(result, log) {
2065
2191
  for (const message of result.messages) log(message);
@@ -2076,17 +2202,85 @@ function printRuntimeInstall(result, log) {
2076
2202
  log("Local Haven signer still needs runtime setup.");
2077
2203
  }
2078
2204
  }
2079
- function printNextSteps(result, log) {
2080
- log("Next: approve the agent rules in Haven. No Haven tools appear until you approve \u2014 restart or not.");
2081
- if (result.restartRequired) {
2082
- if (runtimeRequiresHardRestart(result.runtime)) {
2083
- log("After you approve, restart this agent so it can load Haven tools.");
2084
- } else {
2085
- log("After you approve, restart this agent \u2014 your current session won't load the Haven tools until you do.");
2205
+ function formatAtomicAmount(atomic, decimals) {
2206
+ const s = atomic.toString().padStart(decimals + 1, "0");
2207
+ const intPart = s.slice(0, s.length - decimals) || "0";
2208
+ const fracPart = s.slice(s.length - decimals).replace(/0+$/, "");
2209
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
2210
+ }
2211
+ function describeResetPeriod(resetPeriodMin) {
2212
+ if (resetPeriodMin === 1440) return "per day";
2213
+ if (resetPeriodMin === 60) return "per hour";
2214
+ if (resetPeriodMin === 0) return "with no automatic reset";
2215
+ return `per ${resetPeriodMin} minutes`;
2216
+ }
2217
+ function describeApprovedBudget(budget) {
2218
+ const token = resolveTokenFromAddress(budget.token_address);
2219
+ const amount = token ? `${formatAtomicAmount(BigInt(budget.amount), token.decimals)} ${budget.token_symbol}` : `${budget.amount} ${budget.token_symbol} (atomic units)`;
2220
+ return `${amount} ${describeResetPeriod(budget.reset_period_min)}`;
2221
+ }
2222
+ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2223
+ const intervalMs = options.intervalMs ?? 5e3;
2224
+ const timeoutMs = options.timeoutMs ?? 18e4;
2225
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
2226
+ const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
2227
+ const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
2228
+ log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
2229
+ for (let i = 0; i < maxPolls; i++) {
2230
+ await sleep(intervalMs);
2231
+ let status;
2232
+ try {
2233
+ status = await api.getConnectorStatus(setupId, apiKey);
2234
+ } catch (err) {
2235
+ if (err instanceof ConnectRequestError && (err.status === 401 || err.status === 404)) {
2236
+ log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
2237
+ return "ended";
2238
+ }
2239
+ continue;
2240
+ }
2241
+ if (status.status === "active") {
2242
+ log(
2243
+ 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."
2244
+ );
2245
+ return "approved";
2246
+ }
2247
+ if (status.status === "cancelled" || status.status === "expired" || status.status === "failed") {
2248
+ log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
2249
+ return "ended";
2250
+ }
2251
+ if ((i + 1) % remindEvery === 0) {
2252
+ log("Still waiting for budget approval in Haven\u2026");
2086
2253
  }
2087
- } else {
2088
- log("After you approve, the Haven tools should appear in this runtime shortly.");
2089
2254
  }
2255
+ log(
2256
+ "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."
2257
+ );
2258
+ return "pending";
2259
+ }
2260
+ function completionHandoffLines(result) {
2261
+ if (result.errorCode === "manual_runtime_setup_required") {
2262
+ return [
2263
+ "Next steps:",
2264
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2265
+ "2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
2266
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2267
+ ];
2268
+ }
2269
+ if (result.errorCode) {
2270
+ return [
2271
+ "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."
2272
+ ];
2273
+ }
2274
+ const profile = runtimeProfile(result.runtime);
2275
+ return [
2276
+ "Next steps:",
2277
+ "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2278
+ `2. ${profile.activationInstruction}`,
2279
+ `3. ${runtimeVerificationInstruction(result.runtime)}`
2280
+ ];
2281
+ }
2282
+ function printNextSteps(result, log) {
2283
+ for (const line of completionHandoffLines(result)) log(line);
2090
2284
  }
2091
2285
 
2092
2286
  // src/args.ts
@@ -2096,10 +2290,13 @@ function parseArgs(argv, env = process.env) {
2096
2290
  connectorVersion: CONNECTOR_VERSION
2097
2291
  };
2098
2292
  let help = false;
2293
+ let json = false;
2099
2294
  for (let i = 0; i < argv.length; i += 1) {
2100
2295
  const arg = argv[i];
2101
2296
  if (arg === "--help" || arg === "-h") {
2102
2297
  help = true;
2298
+ } else if (arg === "--json") {
2299
+ json = true;
2103
2300
  } else if (arg === "--setup" || arg === "--setup-token") {
2104
2301
  options.setupToken = requireValue(argv, ++i, arg);
2105
2302
  } else if (arg === "--api" || arg === "--api-url") {
@@ -2126,7 +2323,7 @@ function parseArgs(argv, env = process.env) {
2126
2323
  }
2127
2324
  }
2128
2325
  if (help) {
2129
- return { options, help };
2326
+ return { options, help, json };
2130
2327
  }
2131
2328
  if (!options.setupToken) {
2132
2329
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -2135,7 +2332,7 @@ function parseArgs(argv, env = process.env) {
2135
2332
  throw new Error("Missing --api <Haven API URL>.");
2136
2333
  }
2137
2334
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
2138
- return { options, help };
2335
+ return { options, help, json };
2139
2336
  }
2140
2337
  function helpText() {
2141
2338
  return [
@@ -2157,9 +2354,10 @@ function helpText() {
2157
2354
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
2158
2355
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
2159
2356
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
2357
+ " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
2160
2358
  " --help Show this help.",
2161
2359
  "",
2162
- "The connector never prints the private key and never sends it to Haven."
2360
+ "The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
2163
2361
  ].join("\n");
2164
2362
  }
2165
2363
  function requireValue(argv, index, option) {
@@ -2171,19 +2369,66 @@ function requireValue(argv, index, option) {
2171
2369
  }
2172
2370
 
2173
2371
  // src/cli.ts
2174
- async function main() {
2175
- const parsed = parseArgs(process.argv.slice(2));
2372
+ async function runCli(argv, io = {
2373
+ stdout: (message) => process.stdout.write(message),
2374
+ stderr: (message) => process.stderr.write(message)
2375
+ }) {
2376
+ const wantsJson = argv.includes("--json");
2377
+ let parsed;
2378
+ try {
2379
+ parsed = parseArgs(argv);
2380
+ } catch (err) {
2381
+ if (wantsJson) {
2382
+ io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
2383
+ `);
2384
+ } else {
2385
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2386
+ `);
2387
+ }
2388
+ return 1;
2389
+ }
2176
2390
  if (parsed.help) {
2177
- process.stdout.write(`${helpText()}
2391
+ io.stdout(`${helpText()}
2178
2392
  `);
2179
- return;
2393
+ return 0;
2180
2394
  }
2181
- await runConnect(parsed.options);
2182
- }
2183
- main().catch((err) => {
2184
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
2395
+ try {
2396
+ const result = await runConnect(
2397
+ { ...parsed.options, waitForApproval: !parsed.json },
2398
+ {
2399
+ log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
2400
+ `),
2401
+ redactPaths: parsed.json
2402
+ }
2403
+ );
2404
+ if (parsed.json) io.stdout(`${JSON.stringify(result.outcome)}
2405
+ `);
2406
+ return 0;
2407
+ } catch (err) {
2408
+ if (parsed.json) {
2409
+ io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
2410
+ `);
2411
+ } else {
2412
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
2185
2413
  `);
2186
- process.exit(1);
2187
- });
2414
+ }
2415
+ return 1;
2416
+ }
2417
+ }
2418
+ async function main() {
2419
+ const exitCode = await runCli(process.argv.slice(2));
2420
+ if (exitCode !== 0) process.exitCode = exitCode;
2421
+ }
2422
+ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta.url) {
2423
+ if (!argvPath) return false;
2424
+ try {
2425
+ return realpathSync(argvPath) === realpathSync(fileURLToPath(moduleUrl));
2426
+ } catch {
2427
+ return pathToFileURL(argvPath).href === moduleUrl;
2428
+ }
2429
+ }
2430
+ if (isCliEntrypoint()) void main();
2431
+
2432
+ export { isCliEntrypoint, runCli };
2188
2433
  //# sourceMappingURL=cli.js.map
2189
2434
  //# sourceMappingURL=cli.js.map