@bondsmith/bondsmithd 0.1.1-staging.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +6 -5
  2. package/dist/index.js +102 -44
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ Environment variables (all optional):
37
37
  ```
38
38
  PORT=4577 # defaults to 4577
39
39
  ALLOWED_ORIGINS=https://app.bondsmith.dev
40
- BONDSMITH_RELAY_URL=wss://relay.bondsmith.dev # defaults to a local-only address; set this for a real install
40
+ BONDSMITH_RELAY_URL=ws://127.0.0.1:8787 # defaults to production; set this when developing bondsmithd against a local apps/relay
41
41
  ```
42
42
 
43
43
  `BONDSMITH_DEV_TUNNEL_TOKEN` also exists but only applies to the legacy direct-connection path
@@ -54,10 +54,11 @@ BONDSMITH_RELAY_URL=wss://relay.bondsmith.dev # defaults to a local-only address
54
54
 
55
55
  ## Production connectivity (current status)
56
56
 
57
- `bondsmithd` connects outbound to the Bondsmith relay by default (`BONDSMITH_RELAY_ENABLED=true`),
58
- which is what a production account actually uses — no dev-tunnel token involved. Point
59
- `BONDSMITH_RELAY_URL` at `wss://relay.bondsmith.dev` for a real install (see "Environment
60
- variables" above); it defaults to a local-only address for monorepo development.
57
+ `bondsmithd` connects outbound to the Bondsmith relay by default (`BONDSMITH_RELAY_ENABLED=true`,
58
+ `BONDSMITH_RELAY_URL=wss://relay.bondsmith.dev`), which is what a production account actually
59
+ uses — no dev-tunnel token involved, no config needed for a real install. Override
60
+ `BONDSMITH_RELAY_URL` to a local apps/relay instance (see "Environment variables" above) only when
61
+ developing bondsmithd itself.
61
62
 
62
63
  The dev-tunnel-token requirement only applies to the legacy direct-connection path
63
64
  (`BONDSMITH_RELAY_ENABLED=false`), which still 404s the token-handoff route in production by
package/dist/index.js CHANGED
@@ -10,6 +10,9 @@ import { fileURLToPath } from "url";
10
10
  // ../../packages/local-agent-protocol/src/session.ts
11
11
  var SESSION_STATUSES = ["running", "exited"];
12
12
 
13
+ // ../../packages/local-agent-protocol/src/messages.ts
14
+ var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
15
+
13
16
  // ../../packages/local-agent-protocol/src/schemas.ts
14
17
  import { z as z2 } from "zod";
15
18
 
@@ -494,7 +497,7 @@ var environmentSchema = z8.object({
494
497
  // (BD-432) — defaults to production so a real install works with zero
495
498
  // config; overridden to the local dev server (e.g. http://localhost:4000)
496
499
  // when developing bondsmithd itself against a local web app.
497
- BONDSMITH_API_URL: z8.string().url().default("https://staging.bondsmith.dev"),
500
+ BONDSMITH_API_URL: z8.string().url().default("https://app.bondsmith.dev"),
498
501
  // Interface the WebSocket/HTTP server binds to. Defaults to loopback-only
499
502
  // (never reachable from outside this machine) — real access control is
500
503
  // ALLOWED_ORIGINS + the dev-tunnel token, not the bind address, but
@@ -505,18 +508,19 @@ var environmentSchema = z8.object({
505
508
  // install needs to touch.
506
509
  BONDSMITH_BIND_HOST: z8.string().min(1).default("127.0.0.1"),
507
510
  // BD-450/BD-483/BD-484: connect outbound to the Bondsmith relay instead of
508
- // (or alongside) listening inbound. ON by default — local dev is meant to
509
- // mirror production's actual topology (browser -> relay -> daemon), not
510
- // diverge from it, so plain `pnpm --filter @bondsmith/bondsmithd dev`
511
- // connects outbound to a locally-running apps/relay (its own `pnpm dev`
512
- // on the default port below) exactly like a real deployment would.
513
- // BONDSMITH_RELAY_URL defaults to that same local apps/relay instance for
514
- // the same reason. The existing inbound listener is completely
515
- // unaffected either way — it keeps running regardless, so direct-
516
- // connection debugging (bypassing the relay entirely) is still possible
517
- // by setting BONDSMITH_RELAY_ENABLED=false explicitly.
511
+ // (or alongside) listening inbound. ON by default. The existing inbound
512
+ // listener is completely unaffected either way — it keeps running
513
+ // regardless, so direct-connection debugging (bypassing the relay
514
+ // entirely) is still possible by setting BONDSMITH_RELAY_ENABLED=false
515
+ // explicitly.
518
516
  BONDSMITH_RELAY_ENABLED: z8.string().default("true").transform((value) => value === "true" || value === "1"),
519
- BONDSMITH_RELAY_URL: z8.string().url().default("wss://relay-staging.bondsmith.dev")
517
+ // Defaults to production, mirroring BONDSMITH_API_URL above, so a real
518
+ // install works with zero config — a bare `bondsmithd` used to silently
519
+ // connect outbound to loopback (nothing listening there) with no error,
520
+ // looking healthy while never reaching relay at all. Override to a local
521
+ // apps/relay instance (its own `pnpm dev` default port) when developing
522
+ // bondsmithd itself against a local relay.
523
+ BONDSMITH_RELAY_URL: z8.string().url().default("wss://relay.bondsmith.dev")
520
524
  });
521
525
  function getDaemonConfig(env = process.env) {
522
526
  const parsed = environmentSchema.parse(env);
@@ -760,6 +764,27 @@ var TmuxNoServerRunningError = class extends Error {
760
764
  this.name = "TmuxNoServerRunningError";
761
765
  }
762
766
  };
767
+ var TmuxNotFoundError = class extends Error {
768
+ constructor() {
769
+ super(
770
+ [
771
+ "bondsmithd requires tmux, and it was not found on PATH.",
772
+ "",
773
+ "Install it:",
774
+ " macOS (Homebrew): brew install tmux",
775
+ " Debian/Ubuntu: sudo apt-get install tmux",
776
+ " Fedora/RHEL: sudo dnf install tmux",
777
+ " Arch: sudo pacman -S tmux",
778
+ "",
779
+ "If tmux is already installed, start bondsmithd from a normal interactive shell \u2014",
780
+ "not a LaunchAgent, Docker container, or other minimal-PATH context. Those often",
781
+ "don't inherit the PATH tmux was installed into (e.g. Homebrew's /opt/homebrew/bin",
782
+ "on Apple Silicon)."
783
+ ].join("\n")
784
+ );
785
+ this.name = "TmuxNotFoundError";
786
+ }
787
+ };
763
788
  var SENSITIVE_ENV_KEYS = ["ANTHROPIC_API_KEY", "BONDSMITH_DEV_TUNNEL_TOKEN"];
764
789
  function filterSpawnEnv(env) {
765
790
  const filtered = { ...env };
@@ -767,8 +792,13 @@ function filterSpawnEnv(env) {
767
792
  return filtered;
768
793
  }
769
794
  async function runTmux(args, options) {
770
- const { stdout } = await execFile("tmux", args, { ...options, encoding: "utf8" });
771
- return stdout;
795
+ try {
796
+ const { stdout } = await execFile("tmux", args, { ...options, encoding: "utf8" });
797
+ return stdout;
798
+ } catch (error2) {
799
+ if (error2?.code === "ENOENT") throw new TmuxNotFoundError();
800
+ throw error2;
801
+ }
772
802
  }
773
803
  async function sendKeysLiteral(tmuxSessionName, text) {
774
804
  await runTmux(["send-keys", "-t", tmuxSessionName, "-l", text]);
@@ -991,7 +1021,7 @@ function verifyRejectionMessage(reason) {
991
1021
  case "expired":
992
1022
  return "command envelope expired";
993
1023
  case "replayed":
994
- return "command already used (replay rejected)";
1024
+ return REPLAYED_COMMAND_MESSAGE;
995
1025
  case "wrong_daemon":
996
1026
  return "command not addressed to this daemon";
997
1027
  case "wrong_connection":
@@ -1005,6 +1035,7 @@ var BRACKETED_PASTE_START = "\x1B[200~";
1005
1035
  var BRACKETED_PASTE_END = "\x1B[201~";
1006
1036
  function createRouterConnection(connectionAuth, send, markControllerId, ptyAttachments) {
1007
1037
  const outputListeners = /* @__PURE__ */ new Map();
1038
+ const attachLocks = /* @__PURE__ */ new Map();
1008
1039
  const conn = {
1009
1040
  subscriptions: /* @__PURE__ */ new Set(),
1010
1041
  send,
@@ -1013,7 +1044,18 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
1013
1044
  },
1014
1045
  getOutputListener: (sessionId) => outputListeners.get(sessionId),
1015
1046
  connectionAuth,
1016
- markControllerId
1047
+ markControllerId,
1048
+ withAttachLock: (sessionId, fn) => {
1049
+ const previous = attachLocks.get(sessionId) ?? Promise.resolve();
1050
+ const next = previous.catch(() => {
1051
+ }).then(fn);
1052
+ attachLocks.set(
1053
+ sessionId,
1054
+ next.catch(() => {
1055
+ })
1056
+ );
1057
+ return next;
1058
+ }
1017
1059
  };
1018
1060
  function releaseAllSubscriptions() {
1019
1061
  for (const sessionId of conn.subscriptions) {
@@ -1117,36 +1159,38 @@ async function dispatch(conn, message, {
1117
1159
  }
1118
1160
  const session = getSessionOrError(sessionManager, conn, message.sessionId);
1119
1161
  if (!session) return;
1120
- const previousListener = conn.getOutputListener(message.sessionId);
1121
- const onData = (data) => {
1122
- conn.send({ type: "terminal.output", sessionId: message.sessionId, data });
1123
- };
1124
- if (previousListener && await ptyAttachments.swapListener(
1125
- message.sessionId,
1126
- session.tmuxSessionName,
1127
- previousListener,
1128
- onData
1129
- )) {
1162
+ await conn.withAttachLock(message.sessionId, async () => {
1163
+ const previousListener = conn.getOutputListener(message.sessionId);
1164
+ const onData = (data) => {
1165
+ conn.send({ type: "terminal.output", sessionId: message.sessionId, data });
1166
+ };
1167
+ if (previousListener && await ptyAttachments.swapListener(
1168
+ message.sessionId,
1169
+ session.tmuxSessionName,
1170
+ previousListener,
1171
+ onData
1172
+ )) {
1173
+ conn.subscriptions.add(message.sessionId);
1174
+ conn.registerOutputListener(message.sessionId, onData);
1175
+ conn.send({ type: "session.attached", sessionId: message.sessionId });
1176
+ return;
1177
+ }
1178
+ if (previousListener) {
1179
+ ptyAttachments.release(message.sessionId, previousListener);
1180
+ }
1181
+ const attached = await ptyAttachments.ensureAttached(
1182
+ message.sessionId,
1183
+ session.tmuxSessionName,
1184
+ onData
1185
+ );
1186
+ if (!attached) {
1187
+ conn.send(error("failed to attach to session"));
1188
+ return;
1189
+ }
1130
1190
  conn.subscriptions.add(message.sessionId);
1131
1191
  conn.registerOutputListener(message.sessionId, onData);
1132
1192
  conn.send({ type: "session.attached", sessionId: message.sessionId });
1133
- return;
1134
- }
1135
- if (previousListener) {
1136
- ptyAttachments.release(message.sessionId, previousListener);
1137
- }
1138
- const attached = await ptyAttachments.ensureAttached(
1139
- message.sessionId,
1140
- session.tmuxSessionName,
1141
- onData
1142
- );
1143
- if (!attached) {
1144
- conn.send(error("failed to attach to session"));
1145
- return;
1146
- }
1147
- conn.subscriptions.add(message.sessionId);
1148
- conn.registerOutputListener(message.sessionId, onData);
1149
- conn.send({ type: "session.attached", sessionId: message.sessionId });
1193
+ });
1150
1194
  return;
1151
1195
  }
1152
1196
  case "session.input": {
@@ -2329,7 +2373,12 @@ var PtyAttachmentManager = class {
2329
2373
  cwd: process.env.HOME,
2330
2374
  env: filterSpawnEnv(process.env)
2331
2375
  });
2332
- } catch {
2376
+ } catch (error2) {
2377
+ if (error2?.code === "ENOENT") {
2378
+ console.error(`
2379
+ ${new TmuxNotFoundError().message}
2380
+ `);
2381
+ }
2333
2382
  return false;
2334
2383
  }
2335
2384
  handle = { process: ptyProcess, refCount: 0, dataListeners: /* @__PURE__ */ new Set() };
@@ -2488,6 +2537,12 @@ async function discoverSessions() {
2488
2537
  return Promise.all(sessions.map((session) => enrichSessionWithGitMetadata(session)));
2489
2538
  } catch (error2) {
2490
2539
  if (error2 instanceof TmuxNoServerRunningError) return [];
2540
+ if (error2 instanceof TmuxNotFoundError) {
2541
+ console.error(`
2542
+ ${error2.message}
2543
+ `);
2544
+ process.exit(1);
2545
+ }
2491
2546
  throw error2;
2492
2547
  }
2493
2548
  }
@@ -2551,6 +2606,9 @@ var SessionManager = class {
2551
2606
  const output = await listTmuxSessions("#{session_name}");
2552
2607
  liveSessionNames = new Set(output.split("\n").filter((name) => name.length > 0));
2553
2608
  } catch (error2) {
2609
+ if (error2 instanceof TmuxNotFoundError) console.error(`
2610
+ ${error2.message}
2611
+ `);
2554
2612
  if (!(error2 instanceof TmuxNoServerRunningError)) return;
2555
2613
  }
2556
2614
  await Promise.all(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.1-staging.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Local daemon for Bondsmith agent sessions — runs on the user's own machine, pairs with a Bondsmith account, and exposes tmux-backed terminal sessions to the Bondsmith web app over a signed WebSocket protocol.",
6
6
  "homepage": "https://bondsmith.dev",