@haven_ai/connect 0.0.0-dev.202610071809.5460535 → 0.0.0-dev.202610071831.a279c0e

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
@@ -499,12 +499,24 @@ already-configured machine behaves as follows (characterized in
499
499
  consumed setup when Connect resolves it (or, in a rare concurrent-run race,
500
500
  at registration). The key pair minted for the attempt exists only in memory
501
501
  and is discarded. Start a fresh connection from the Haven dashboard instead.
502
- - **Running a fresh setup on a configured machine** writes the new agent's
503
- credentials into its own directory under `~/.haven/agents/<agent-id>/`,
502
+ - **Running a fresh setup on a configured machine writes the new agent's
503
+ credentials into its own directory under `~/.haven/agents/<slug-or-id>/`,
504
504
  alongside the previous agent's directory, which stays byte-identical.
505
- Nothing is rotated, revoked, or deleted locally.
505
+ Nothing is rotated, revoked, or deleted locally.**
506
+ - **A default setup names its pair from the agent's display name
507
+ ([#3737](https://github.com/d-hinders/Haven-AI/issues/3737)) and never
508
+ collides.** Without `--name`, `--bare` or `--replace`, the connector derives
509
+ the slug itself (`proposeServerSlug`: slugified display name, `agent`
510
+ fallback, numeric suffix when taken) and wires `haven-<slug>` /
511
+ `haven-signer-<slug>` — de-collided against **every** directory under the
512
+ credential root (retired and key-removed ones included, so a slug freed by
513
+ `--unwire` is never reused), and marked `-dev` on a non-production backend
514
+ (Base Sepolia or any chain that is not Base mainnet 8453; an explicit
515
+ `--name` is used verbatim and never altered). Nothing is displaced, so
516
+ nothing is asked. `--bare` opts back into the unnamed pair.
506
517
  - **A bare re-run over a live previous agent is a decision, not a default
507
- ([#2551](https://github.com/d-hinders/Haven-AI/issues/2551)).** Before
518
+ ([#2551](https://github.com/d-hinders/Haven-AI/issues/2551)).** Only a run
519
+ that targets the bare pair — `--bare`, or `--replace` — can collide; before
508
520
  minting a key or registering, Connect scans the credential root for a
509
521
  bare-pair directory that still holds a usable key — the same reading
510
522
  `--doctor` classifies as `wired` or `superseded`; `retired`, `orphaned`,
@@ -521,7 +533,9 @@ already-configured machine behaves as follows (characterized in
521
533
  flag itself. The setup prompt then permits one re-run with the flag the
522
534
  user picks (#3689), on top of `--json` and a `--runtime` retry.
523
535
  - **`--replace`** is the unattended answer "yes, replace". `--name <slug>`
524
- installs alongside. Passing both is a usage error — they contradict.
536
+ installs alongside. `--bare` is the human opt-in to the unnamed pair and
537
+ refuses with `--name`. Passing `--replace --name` is a usage error — they
538
+ contradict.
525
539
  - **Replacing re-points the bare pair, then retires the previous directory
526
540
  locally.** Connect owns the `haven` and `haven-signer` entries (and the
527
541
  managed Codex/Hermes equivalents) and re-points them at the new agent's
@@ -568,15 +582,17 @@ As above, `<channel>` is a placeholder like the rest of this line: take the
568
582
  package from the setup response's `connector_package`
569
583
  and add `--name` to the command the dashboard gave you.
570
584
 
571
- | | Without `--name` | With `--name research` |
572
- |---|---|---|
573
- | MCP entries | `haven`, `haven-signer` | `haven-research`, `haven-signer-research` |
574
- | Credentials | `~/.haven/agents/<agent-id>/` | `~/.haven/agents/research/` |
585
+ | | Default (no `--name`) | `--name research` | `--bare` |
586
+ |---|---|---|---|
587
+ | MCP entries | `haven-<slug>`, `haven-signer-<slug>` (slug from the agent's display name, `-dev`-marked on a non-production backend) | `haven-research`, `haven-signer-research` | `haven`, `haven-signer` |
588
+ | Credentials | `~/.haven/agents/<slug>/` | `~/.haven/agents/research/` | `~/.haven/agents/<agent-id>/` |
575
589
 
576
590
  A writer only ever touches the pair it owns, so adding a named agent cannot
577
- disturb the bare pair or another named one. Omitting `--name` is byte-identical
578
- to how the connector behaved before named pairs existed, so nothing already
579
- wired needs changing.
591
+ disturb the bare pair or another named one. **`--bare` wires the bare pair
592
+ byte-identically to how the connector behaved before named pairs existed**, so
593
+ nothing already wired needs changing — it is the one flag (besides
594
+ `--replace`) that reaches the unnamed pair, and it cannot be combined with
595
+ `--name`.
580
596
 
581
597
  The slug is **1–32 lowercase letters, digits and single hyphens**, validated
582
598
  before anything is written, and **immutable once wired** — it is the server name
@@ -616,6 +632,14 @@ npx -y @haven_ai/connect@<channel> --rekey-finish --api-key sk_agent_... \
616
632
  --runtime claude-code [--name research]
617
633
  ```
618
634
 
635
+ **No `--name` needed when exactly one live agent is on the machine.** Since
636
+ [#3737](https://github.com/d-hinders/Haven-AI/issues/3737), `--rekey` without
637
+ `--name` selects the sole agent directory that still holds a stored API key —
638
+ tombstoned and key-removed directories left by `--unwire` do not count — and
639
+ the printed phase-two command carries that agent's exact `--name` when it has
640
+ one. With **several** live agents it refuses and lists them, so you must pass
641
+ `--name <slug>`; a bare agent needs no `--name`, exactly as before.
642
+
619
643
  **Phase one prints the exact phase-two command — prefer it over the line above.**
620
644
  Since [#2423](https://github.com/d-hinders/Haven-AI/issues/2423) the connector
621
645
  builds that command from the npm dist-tag **it** was published under, so a build
package/dist/cli.cjs CHANGED
@@ -325,6 +325,19 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
325
325
  `No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
326
326
  );
327
327
  }
328
+ let discoveredSlug;
329
+ if (!key) {
330
+ const binding = await readJsonFile(path.join(directory, "mcp-server-binding.json"));
331
+ const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
332
+ if (hostedName && hostedName.startsWith("haven-")) {
333
+ const slug = hostedName.slice("haven-".length);
334
+ try {
335
+ assertValidServerSlug(slug);
336
+ discoveredSlug = slug;
337
+ } catch {
338
+ }
339
+ }
340
+ }
328
341
  const agent = await readJsonFile(path.join(directory, "agent.json")) ?? {};
329
342
  const signer = await readJsonFile(path.join(directory, "signer.json")) ?? {};
330
343
  const agentId = asString(identity.agent_id);
@@ -340,6 +353,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
340
353
  directory,
341
354
  agentId,
342
355
  apiKey,
356
+ ...discoveredSlug ? { discoveredSlug } : {},
343
357
  apiUrl,
344
358
  hostedMcpUrl,
345
359
  delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
@@ -368,7 +382,9 @@ async function discoverSoleAgentDirectory(baseDir) {
368
382
  const candidates = [];
369
383
  for (const entry of entries) {
370
384
  const directory = path.join(root, entry);
371
- if (!await readJsonFile(path.join(directory, "identity.json"))) continue;
385
+ const identity = await readJsonFile(path.join(directory, "identity.json"));
386
+ if (!identity) continue;
387
+ if (!asString(identity.api_key)) continue;
372
388
  if (await readJsonFile(path.join(directory, "TOMBSTONE.json"))) continue;
373
389
  candidates.push(directory);
374
390
  }
@@ -598,6 +614,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
598
614
  var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS, CONNECT_OUTCOME_FILENAME, MCP_SERVER_BINDING_FILENAME;
599
615
  var init_storage = __esm({
600
616
  "src/storage.ts"() {
617
+ init_server_names();
601
618
  REKEY_PENDING_FILENAME = "rekey-pending.json";
602
619
  REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
603
620
  CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
@@ -620,9 +637,9 @@ var init_runtime_manifest = __esm({
620
637
  mcpPackage: "@haven_ai/mcp",
621
638
  mcpVersion: mcp.MCP_VERSION,
622
639
  sdkPackage: "@haven_ai/sdk",
623
- sdkVersion: "0.0.0-dev.202610071809.5460535",
640
+ sdkVersion: "0.0.0-dev.202610071831.a279c0e",
624
641
  signerPackage: "@haven_ai/signer",
625
- signerVersion: "0.0.0-dev.202610071809.5460535",
642
+ signerVersion: "0.0.0-dev.202610071831.a279c0e",
626
643
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
627
644
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
628
645
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -1035,7 +1052,9 @@ async function writeHermesConfig(input, deps) {
1035
1052
  restartRequired: true,
1036
1053
  messages: [
1037
1054
  `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
1038
- "Restart Hermes (start a new session; gateway users: /restart), then verify with `hermes mcp list`, `hermes mcp test haven`, and `hermes mcp test haven-signer`.",
1055
+ // #3737: name the pair actually written — a bare run prints exactly the
1056
+ // historical sentence; a named run names its own pair.
1057
+ `Restart Hermes (start a new session; gateway users: /restart), then verify with \`hermes mcp list\`, \`hermes mcp test ${names.hosted}\`, and \`hermes mcp test ${names.signer}\`.`,
1039
1058
  "If several long-lived Hermes processes are running (a gateway plus TUI workers), restart EVERY one: each loads its MCP wiring at startup, so a process started before this setup keeps using its old snapshot.",
1040
1059
  "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
1041
1060
  ]
@@ -3392,17 +3411,45 @@ async function detectWiringCollision(input) {
3392
3411
  suggestedServerName: proposeServerSlug(input.agentName, taken)
3393
3412
  };
3394
3413
  }
3395
- function proposeServerSlug(agentName, taken) {
3396
- let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32).replace(/-+$/g, "");
3414
+ function proposeServerSlug(agentName, taken, maxBaseLength = 32) {
3415
+ let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, maxBaseLength).replace(/-+$/g, "");
3397
3416
  if (!slugIsValid(base)) base = "agent";
3398
3417
  if (!taken.has(base)) return base;
3399
3418
  for (let n = 2; n < 1e3; n += 1) {
3400
3419
  const suffix = `-${n}`;
3401
- const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
3420
+ const candidate = `${base.slice(0, maxBaseLength - suffix.length).replace(/-+$/g, "")}${suffix}`;
3402
3421
  if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
3403
3422
  }
3404
3423
  throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
3405
3424
  }
3425
+ function deriveServerSlug(agentName, taken, markNonProduction) {
3426
+ const mark = "-dev";
3427
+ if (!markNonProduction) return proposeServerSlug(agentName, taken);
3428
+ const base = proposeServerSlug(agentName, taken, 32 - mark.length);
3429
+ if (base.endsWith(mark)) return base;
3430
+ const marked = `${base}${mark}`;
3431
+ if (!taken.has(marked)) return marked;
3432
+ return proposeServerSlug(marked, taken);
3433
+ }
3434
+ async function listTakenDirectoryNames(credentialsDir) {
3435
+ const root = defaultCredentialRoot(credentialsDir);
3436
+ let entries = [];
3437
+ try {
3438
+ entries = await promises.readdir(root);
3439
+ } catch {
3440
+ return /* @__PURE__ */ new Set();
3441
+ }
3442
+ const taken = /* @__PURE__ */ new Set();
3443
+ for (const entry of entries) {
3444
+ try {
3445
+ if (!(await promises.stat(path.join(root, entry))).isDirectory()) continue;
3446
+ } catch {
3447
+ continue;
3448
+ }
3449
+ taken.add(entry);
3450
+ }
3451
+ return taken;
3452
+ }
3406
3453
  function slugIsValid(slug) {
3407
3454
  try {
3408
3455
  assertValidServerSlug(slug);
@@ -3802,7 +3849,16 @@ async function executeConnect(options, deps, trace) {
3802
3849
  log("Checked local credential storage \u2014 all clear.");
3803
3850
  let serverName = options.serverName;
3804
3851
  let replacing;
3805
- if (!serverName) {
3852
+ if (!serverName && options.deriveServerName) {
3853
+ const taken = await listTakenDirectoryNames(options.credentialsDir);
3854
+ const nonProduction = setup.haven_wallet.chain_id !== PRODUCTION_CHAIN_ID;
3855
+ serverName = deriveServerSlug(setup.agent.name, taken, nonProduction);
3856
+ await assertServerSlugAvailable(serverName, options.credentialsDir);
3857
+ const derivedNames = serverNamesFor(serverName);
3858
+ log(
3859
+ `Naming this agent's MCP pair from its display name: ${derivedNames.hosted} / ${derivedNames.signer}. Nothing was displaced; to wire the bare haven / haven-signer pair instead, re-run with --bare, or choose the name yourself with --name <slug>.`
3860
+ );
3861
+ } else if (!serverName) {
3806
3862
  const collision = await detectWiringCollision({
3807
3863
  credentialsDir: options.credentialsDir,
3808
3864
  agentName: setup.agent.name
@@ -4077,6 +4133,9 @@ async function executeConnect(options, deps, trace) {
4077
4133
  runtimeInstall,
4078
4134
  delegateAddress: registration.delegate_address,
4079
4135
  hostedMcpUrl: registration.hosted_mcp_url,
4136
+ // #3737: the resolved name — derived, explicit, or the bare pair — rides
4137
+ // the machine-readable outcome alongside the prose completion text.
4138
+ serverName,
4080
4139
  supersededAgentIds,
4081
4140
  supersededAgentsRetiredLocally,
4082
4141
  ...replacing ? { retiredAgentIds } : {},
@@ -4151,6 +4210,9 @@ function completionOutcome(input) {
4151
4210
  // #3122: always emitted on a completed run (empty list included), for the
4152
4211
  // same reason as superseded_agent_ids above.
4153
4212
  existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
4213
+ // #3737: the resolved hosted server name — always present on a completed
4214
+ // run, bare pair included, so a caller never has to parse the prose.
4215
+ server_name: serverNamesFor(input.serverName).hosted,
4154
4216
  ...input.serverNameReboundFrom ? { server_name_rebound_from: input.serverNameReboundFrom } : {},
4155
4217
  ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
4156
4218
  ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
@@ -4482,7 +4544,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
4482
4544
  function printNextSteps(result, log, approval, approvalUrl) {
4483
4545
  for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
4484
4546
  }
4485
- var CONNECTOR_VERSION, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
4547
+ var CONNECTOR_VERSION, PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY, CONNECT_OUTCOME_SCHEMA_VERSION, failureOutcomesByError, RERUN_HINT;
4486
4548
  var init_runtime = __esm({
4487
4549
  "src/runtime.ts"() {
4488
4550
  init_api();
@@ -4499,7 +4561,8 @@ var init_runtime = __esm({
4499
4561
  init_tombstone();
4500
4562
  init_local_mcp_runtime();
4501
4563
  init_runtime_manifest();
4502
- CONNECTOR_VERSION = "0.0.0-dev.202610071809.5460535";
4564
+ CONNECTOR_VERSION = "0.0.0-dev.202610071831.a279c0e";
4565
+ PRODUCTION_CHAIN_ID = 8453;
4503
4566
  CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${CONNECTOR_VERSION}`;
4504
4567
  CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4505
4568
  failureOutcomesByError = /* @__PURE__ */ new WeakMap();
@@ -4709,9 +4772,10 @@ async function startRekey(options, deps = {}) {
4709
4772
  started_at: startedAt,
4710
4773
  expires_at: expiresAt
4711
4774
  });
4775
+ const finishName = options.serverName ?? stored.discoveredSlug;
4712
4776
  const finishCommand = [
4713
4777
  sdk.connectorRerunCommand("--rekey-finish"),
4714
- options.serverName ? `--name ${options.serverName}` : void 0,
4778
+ finishName ? `--name ${finishName}` : void 0,
4715
4779
  "--api-key <the key the dashboard showed you>",
4716
4780
  options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
4717
4781
  ].filter(Boolean).join(" ");
@@ -5770,6 +5834,7 @@ function parseArgs(argv, env = process.env) {
5770
5834
  let dryRun = false;
5771
5835
  let unwireDir;
5772
5836
  let replace = false;
5837
+ let bare = false;
5773
5838
  for (let i = 0; i < argv.length; i += 1) {
5774
5839
  const arg = argv[i];
5775
5840
  if (arg === "--help" || arg === "-h") {
@@ -5817,6 +5882,8 @@ function parseArgs(argv, env = process.env) {
5817
5882
  options.credentialsDir = requireValue(argv, ++i, arg);
5818
5883
  } else if (arg === "--replace") {
5819
5884
  replace = true;
5885
+ } else if (arg === "--bare") {
5886
+ bare = true;
5820
5887
  } else if (arg === "--name") {
5821
5888
  options.serverName = requireValue(argv, ++i, arg);
5822
5889
  assertValidServerSlug(options.serverName);
@@ -5842,16 +5909,23 @@ function parseArgs(argv, env = process.env) {
5842
5909
  if (help) {
5843
5910
  return { options, help, json, doctor, repair, tombstone, rekey };
5844
5911
  }
5845
- if (replace) {
5912
+ if (replace || bare) {
5846
5913
  if (rekeyPhase || tombstoneDir || unwire || doctor || repair || pruneSignerRuntimes2) {
5847
- throw new Error("--replace belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.");
5914
+ throw new Error(`${replace ? "--replace" : "--bare"} belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.`);
5848
5915
  }
5849
- if (options.serverName) {
5916
+ if (bare && options.serverName) {
5850
5917
  throw new Error(
5851
- "--replace and --name contradict each other: --replace overwrites the bare haven / haven-signer wiring, --name installs alongside it under its own pair. Pass one of them."
5918
+ "--bare and --name contradict each other: --bare wires the bare haven / haven-signer pair, --name installs a named haven-<slug> pair. Pass one of them."
5852
5919
  );
5853
5920
  }
5854
- options.replaceExistingWiring = true;
5921
+ if (replace) {
5922
+ if (options.serverName) {
5923
+ throw new Error(
5924
+ "--replace and --name contradict each other: --replace overwrites the bare haven / haven-signer wiring, --name installs alongside it under its own pair. Pass one of them."
5925
+ );
5926
+ }
5927
+ options.replaceExistingWiring = true;
5928
+ }
5855
5929
  }
5856
5930
  if (destroyKeyMaterial && !unwire) {
5857
5931
  throw new Error("--destroy-key-material only applies to --unwire.");
@@ -5925,6 +5999,9 @@ function parseArgs(argv, env = process.env) {
5925
5999
  if (!options.apiBaseUrl) {
5926
6000
  throw new Error("Missing --api <Haven API URL>.");
5927
6001
  }
6002
+ if (!options.serverName && !bare && !replace) {
6003
+ options.deriveServerName = true;
6004
+ }
5928
6005
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
5929
6006
  return { options, help, json, doctor, repair, tombstone, rekey };
5930
6007
  }
@@ -5952,13 +6029,19 @@ function helpText() {
5952
6029
  " --replace When this machine is already wired to a different Haven agent on the bare",
5953
6030
  " haven / haven-signer pair, re-point that pair at the new agent and retire the",
5954
6031
  " previous agent directory locally (tombstoned, local key files removed).",
5955
- " Nothing is revoked \u2014 use Remove agent\u2026 on the Haven agent page. Without",
5956
- " this flag a non-interactive run REFUSES such a collision (wiring_collision)",
5957
- " and an interactive terminal is asked; --name installs alongside instead.",
6032
+ " Nothing is revoked \u2014 use Remove agent\u2026 on the Haven agent page. Implies",
6033
+ " the bare pair. Without it (or --bare) a non-interactive run REFUSES such a",
6034
+ " collision (wiring_collision) and an interactive terminal is asked.",
6035
+ " --bare Wire the bare haven / haven-signer pair. Without --name a setup now names",
6036
+ " its pair from the agent's display name (haven-<slug> /",
6037
+ " haven-signer-<slug>, de-collided against this machine, ending -dev on a",
6038
+ " non-production backend), so adding an agent never has to displace anyone;",
6039
+ " --bare is the explicit opt-in to the unnamed pair. --name overrides it.",
5958
6040
  " --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
5959
6041
  " MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
5960
6042
  " agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
5961
- " single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
6043
+ " single hyphens; immutable once wired. Without it the pair is named from the",
6044
+ " agent's display name; --bare wires the bare haven / haven-signer pair.",
5962
6045
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
5963
6046
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
5964
6047
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",