@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.
- package/README.md +6 -5
- package/dist/index.js +102 -44
- 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=
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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://
|
|
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
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
771
|
-
|
|
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
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|