baychat 0.25.3 → 0.27.0

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.
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ // One relay connection per Bay login (docs/features/MULTI_BAY_DEVICE_LOGIN.md).
3
+ //
4
+ // THE DESIGN, AND WHY IT IS THIS ONE. The relay daemon (daemon.ts) holds one device login's
5
+ // event feed and keys everything it knows — registry, held messages, carried briefs, the
6
+ // attach socket, the mailbox — by SESSION NAME. A second Bay on the same computer may well
7
+ // have a session with the same name. Re-keying ~1,900 lines of daemon by (Bay, name) would
8
+ // touch every delivery path at once.
9
+ //
10
+ // Instead each NON-default Bay gets its own, unmodified daemon, in a child process whose whole
11
+ // world is scoped to that Bay by environment:
12
+ //
13
+ // BAYCHAT_CONFIG_DIR <config>/bays/<bayId> its registry, held store, cursors, pid
14
+ // BAYCHAT_RELAY_SOCKET …baychat-relay-<id>.sock its own attach socket (named pipe on Windows)
15
+ // BAYCHAT_MAILBOX_DIR <config>/bays/<bayId>/mailbox
16
+ // BAYCHAT_DEVICE_TOKEN that Bay's token so its feed is that Bay's, and only that Bay's
17
+ //
18
+ // So the registry is keyed by (Bay, session name) by construction: two Bays, two registries.
19
+ // The DEFAULT Bay's daemon is the one that always ran, in the parent process, with the paths
20
+ // it always used — a one-Bay computer runs exactly what it ran before and spawns nothing.
21
+ //
22
+ // A session that joined another Bay (`baychat join --bay Harbor`) applies the same scope to
23
+ // its own process before it attaches, so it finds that Bay's socket and mailbox.
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.BayRelaySupervisor = exports.BAY_RELAY_PARENT_ENV = exports.BAY_RELAY_CHILD_ENV = void 0;
59
+ exports.bayRelayScope = bayRelayScope;
60
+ exports.bayClientEnv = bayClientEnv;
61
+ exports.bayRelayEnv = bayRelayEnv;
62
+ exports.isBayRelayChild = isBayRelayChild;
63
+ const node_child_process_1 = require("node:child_process");
64
+ const path = __importStar(require("node:path"));
65
+ const socket_1 = require("./socket");
66
+ /** Set in a per-Bay child relay: it must never supervise children of its own. */
67
+ exports.BAY_RELAY_CHILD_ENV = "BAYCHAT_RELAY_BAY";
68
+ /** The supervising relay's pid, so an orphaned child can stop itself. */
69
+ exports.BAY_RELAY_PARENT_ENV = "BAYCHAT_RELAY_PARENT_PID";
70
+ /** What a Bay id may contribute to a path or pipe name. */
71
+ function safeId(bayId) {
72
+ return bayId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 40) || "bay";
73
+ }
74
+ /**
75
+ * Where one Bay's relay keeps everything, derived from the DEFAULT relay's own locations so a
76
+ * custom `BAYCHAT_CONFIG_DIR` or `BAYCHAT_RELAY_SOCKET` carries over.
77
+ */
78
+ function bayRelayScope(bayId, baseConfigDir, baseSocket) {
79
+ const id = safeId(bayId);
80
+ const configDir = path.join(baseConfigDir, "bays", id);
81
+ let socket;
82
+ if ((0, socket_1.isNamedPipe)(baseSocket))
83
+ socket = `${baseSocket}-${id}`;
84
+ else if (baseSocket.endsWith(".sock"))
85
+ socket = `${baseSocket.slice(0, -".sock".length)}-${id}.sock`;
86
+ else
87
+ socket = `${baseSocket}-${id}`;
88
+ return { bayId, configDir, socket, mailboxDir: path.join(configDir, "mailbox") };
89
+ }
90
+ /** The environment that points a CLIENT (attach, status) at one Bay's relay. */
91
+ function bayClientEnv(scope) {
92
+ return {
93
+ BAYCHAT_CONFIG_DIR: scope.configDir,
94
+ BAYCHAT_RELAY_SOCKET: scope.socket,
95
+ BAYCHAT_MAILBOX_DIR: scope.mailboxDir,
96
+ };
97
+ }
98
+ /** The environment a per-Bay CHILD relay runs with. Holds the Bay's token: never logged. */
99
+ function bayRelayEnv(bay, baseUrl, scope, parentPid) {
100
+ return {
101
+ ...bayClientEnv(scope),
102
+ BAYCHAT_DEVICE_TOKEN: bay.token,
103
+ BAYCHAT_API_URL: baseUrl,
104
+ BAYCHAT_NO_RELAY_AUTOSTART: "1",
105
+ [exports.BAY_RELAY_CHILD_ENV]: bay.bayId,
106
+ [exports.BAY_RELAY_PARENT_ENV]: String(parentPid),
107
+ };
108
+ }
109
+ /** Is THIS process a per-Bay child relay? */
110
+ function isBayRelayChild(env = process.env) {
111
+ return Boolean(env[exports.BAY_RELAY_CHILD_ENV]);
112
+ }
113
+ /**
114
+ * Keeps one child relay running per NON-default Bay login, and none for a one-Bay computer.
115
+ *
116
+ * `sync()` is the whole policy: wanted = the non-default Bays (only with two or more); a child
117
+ * whose Bay is no longer wanted, or whose token changed (a renewal), is stopped; a wanted Bay
118
+ * without a child gets one. A child that dies is restarted on a later sync, after a delay —
119
+ * never in a tight loop.
120
+ */
121
+ class BayRelaySupervisor {
122
+ deps;
123
+ running = new Map();
124
+ lastExit = new Map();
125
+ timer;
126
+ stopped = false;
127
+ log;
128
+ spawnChild;
129
+ now;
130
+ constructor(deps) {
131
+ this.deps = deps;
132
+ this.log = deps.log ?? ((line) => console.error(`[relay] ${line}`));
133
+ this.spawnChild = deps.spawn ?? defaultSpawn;
134
+ this.now = deps.now ?? Date.now;
135
+ }
136
+ start() {
137
+ this.sync();
138
+ this.timer = setInterval(() => this.sync(), this.deps.intervalMs ?? 30_000);
139
+ this.timer.unref?.();
140
+ }
141
+ /** The Bays a child relay is running for — for status and tests. */
142
+ bays() {
143
+ return [...this.running.keys()].sort();
144
+ }
145
+ sync() {
146
+ if (this.stopped)
147
+ return;
148
+ const bays = this.deps.loadBays();
149
+ const baseUrl = this.deps.baseUrl();
150
+ const wanted = new Map();
151
+ if (baseUrl && bays.length > 1)
152
+ for (const b of bays)
153
+ if (!b.isDefault)
154
+ wanted.set(b.bayId, b);
155
+ for (const [bayId, run] of this.running) {
156
+ const want = wanted.get(bayId);
157
+ if (!want || want.token !== run.token)
158
+ this.stopChild(bayId, want ? "login renewed" : "no longer a Bay login here");
159
+ }
160
+ for (const [bayId, bay] of wanted) {
161
+ if (this.running.has(bayId))
162
+ continue;
163
+ const exited = this.lastExit.get(bayId);
164
+ if (exited !== undefined && this.now() - exited < (this.deps.restartDelayMs ?? 10_000))
165
+ continue;
166
+ this.startChild(bay, baseUrl);
167
+ }
168
+ }
169
+ startChild(bay, baseUrl) {
170
+ const scope = bayRelayScope(bay.bayId, this.deps.baseConfigDir, this.deps.baseSocket);
171
+ let child;
172
+ try {
173
+ child = this.spawnChild(bayRelayEnv(bay, baseUrl, scope, process.pid));
174
+ }
175
+ catch (err) {
176
+ this.lastExit.set(bay.bayId, this.now());
177
+ this.log(`could not start the relay for Bay "${bay.bayName}": ${err instanceof Error ? err.message : String(err)}`);
178
+ return;
179
+ }
180
+ const run = { child, token: bay.token, stopping: false };
181
+ this.running.set(bay.bayId, run);
182
+ this.log(`relay for Bay "${bay.bayName}" started (pid ${child.pid ?? "?"})`);
183
+ child.on("exit", (code) => {
184
+ if (this.running.get(bay.bayId) === run)
185
+ this.running.delete(bay.bayId);
186
+ if (!run.stopping) {
187
+ this.lastExit.set(bay.bayId, this.now());
188
+ this.log(`relay for Bay "${bay.bayName}" exited (${code ?? "signal"}); it is restarted shortly`);
189
+ }
190
+ });
191
+ child.on("error", (err) => {
192
+ this.log(`relay for Bay "${bay.bayName}": ${err.message}`);
193
+ });
194
+ }
195
+ stopChild(bayId, why) {
196
+ const run = this.running.get(bayId);
197
+ if (!run)
198
+ return;
199
+ run.stopping = true;
200
+ this.running.delete(bayId);
201
+ this.log(`stopping the relay for Bay ${bayId} (${why})`);
202
+ try {
203
+ run.child.kill("SIGTERM");
204
+ }
205
+ catch (err) {
206
+ this.log(`could not stop the relay for Bay ${bayId}: ${err instanceof Error ? err.message : String(err)}`);
207
+ }
208
+ }
209
+ stop() {
210
+ this.stopped = true;
211
+ if (this.timer)
212
+ clearInterval(this.timer);
213
+ for (const bayId of [...this.running.keys()])
214
+ this.stopChild(bayId, "relay stopping");
215
+ }
216
+ }
217
+ exports.BayRelaySupervisor = BayRelaySupervisor;
218
+ /** Run this same CLI as `relay start --foreground`, in the Bay's scope. */
219
+ function defaultSpawn(env) {
220
+ return (0, node_child_process_1.spawn)(process.execPath, [process.argv[1] ?? "", "relay", "start", "--foreground"], {
221
+ env: { ...process.env, ...env },
222
+ stdio: ["ignore", "inherit", "inherit"],
223
+ });
224
+ }
@@ -37,6 +37,7 @@ exports.cmdRelayStart = cmdRelayStart;
37
37
  exports.ensureRelayInstalled = ensureRelayInstalled;
38
38
  exports.tryRelayStatus = tryRelayStatus;
39
39
  exports.registerAcpSession = registerAcpSession;
40
+ exports.useBayRelay = useBayRelay;
40
41
  exports.cmdRelayStatus = cmdRelayStatus;
41
42
  exports.cmdRelayStop = cmdRelayStop;
42
43
  exports.shouldFallBackToMailbox = shouldFallBackToMailbox;
@@ -61,6 +62,8 @@ const terminal_pane_1 = require("./terminal-pane");
61
62
  const owner_pid_1 = require("./owner-pid");
62
63
  const parent_watch_1 = require("./parent-watch");
63
64
  const daemon_1 = require("./daemon");
65
+ const bay_relays_1 = require("./bay-relays");
66
+ const config_1 = require("../config");
64
67
  const resume_1 = require("./resume");
65
68
  const mailbox_1 = require("./mailbox");
66
69
  const socket_1 = require("./socket");
@@ -119,13 +122,34 @@ function request(sock, frame, timeoutMs = 5_000) {
119
122
  */
120
123
  async function cmdRelayStart(opts = {}) {
121
124
  if (opts.foreground) {
122
- const daemon = new daemon_1.RelayDaemon();
125
+ // One relay connection per Bay login (MULTI_BAY_DEVICE_LOGIN.md): this daemon is the
126
+ // DEFAULT Bay's, exactly as before; every other Bay login gets its own daemon in a child
127
+ // process scoped to that Bay. A per-Bay child supervises nothing and stops with its parent.
128
+ const child = (0, bay_relays_1.isBayRelayChild)();
129
+ const supervisor = child
130
+ ? undefined
131
+ : new bay_relays_1.BayRelaySupervisor({
132
+ loadBays: config_1.loadBayLogins,
133
+ baseUrl: () => (0, config_1.loadDeviceCredentials)()?.baseUrl ?? null,
134
+ baseConfigDir: (0, config_1.configDir)(),
135
+ baseSocket: (0, socket_1.socketPath)(),
136
+ });
137
+ const daemon = new daemon_1.RelayDaemon({ onListening: () => supervisor?.start() });
123
138
  const shutdown = () => {
139
+ supervisor?.stop();
124
140
  void daemon.stop().then(() => process.exit(0));
125
141
  };
126
142
  process.on("SIGINT", shutdown);
127
143
  process.on("SIGTERM", shutdown);
128
- await daemon.start();
144
+ const parentPid = Number(process.env[bay_relays_1.BAY_RELAY_PARENT_ENV]);
145
+ if (child && Number.isInteger(parentPid) && parentPid > 1)
146
+ (0, parent_watch_2.watchParent)(parentPid, shutdown);
147
+ try {
148
+ await daemon.start();
149
+ }
150
+ finally {
151
+ supervisor?.stop();
152
+ }
129
153
  return;
130
154
  }
131
155
  if (await (0, socket_1.probeSocket)((0, socket_1.socketPath)())) {
@@ -229,6 +253,32 @@ async function registerAcpSession(frame) {
229
253
  throw err;
230
254
  }
231
255
  }
256
+ /**
257
+ * Point THIS process's relay client calls (attach, status, mailbox registration) at one Bay's
258
+ * relay — the per-Bay child the supervising relay runs (bay-relays.ts). Used by a session that
259
+ * joined a non-default Bay. Call once, before the first relay call; the default Bay needs none.
260
+ */
261
+ function useBayRelay(bayId) {
262
+ const scope = (0, bay_relays_1.bayRelayScope)(bayId, (0, config_1.configDir)(), (0, socket_1.socketPath)());
263
+ Object.assign(process.env, (0, bay_relays_1.bayClientEnv)(scope));
264
+ }
265
+ /** One line per non-default Bay login: is its relay answering? Empty for a one-Bay computer. */
266
+ async function bayRelayLines() {
267
+ const bays = (0, config_1.loadBayLogins)();
268
+ if (bays.length < 2)
269
+ return [];
270
+ const lines = [" Bay relays:"];
271
+ for (const bay of bays) {
272
+ if (bay.isDefault) {
273
+ lines.push(` ${bay.bayName} (default) — this relay`);
274
+ continue;
275
+ }
276
+ const scope = (0, bay_relays_1.bayRelayScope)(bay.bayId, (0, config_1.configDir)(), (0, socket_1.socketPath)());
277
+ const alive = await (0, socket_1.probeSocket)(scope.socket);
278
+ lines.push(` ${bay.bayName} — ${alive ? "running" : "NOT running (it starts within a minute of the relay; else restart the relay)"}`);
279
+ }
280
+ return lines;
281
+ }
232
282
  async function cmdRelayStatus() {
233
283
  let status;
234
284
  try {
@@ -243,6 +293,8 @@ async function cmdRelayStatus() {
243
293
  console.log(` polls: ${status.polls} delivered: ${status.delivered} cursor: ${status.cursor ?? "—"}`);
244
294
  if (status.lastError)
245
295
  console.log(` last error: ${status.lastError}`);
296
+ for (const line of await bayRelayLines())
297
+ console.log(line);
246
298
  // Computed before the session list so a line can report its own last failure.
247
299
  const byName = new Map(status.sessions.map((s) => [s.name, s]));
248
300
  /**
@@ -178,8 +178,10 @@ class RelayDaemon {
178
178
  */
179
179
  binaries = new Map();
180
180
  log;
181
+ onListening;
181
182
  constructor(opts = {}) {
182
183
  this.log = opts.log ?? ((line) => console.log(`[relay] ${line}`));
184
+ this.onListening = opts.onListening;
183
185
  // EVERY candidate root, because the daemon cannot know which one the agent's
184
186
  // sandbox turned out to permit — it may have fallen back past the one this
185
187
  // process would have chosen.
@@ -317,6 +319,7 @@ class RelayDaemon {
317
319
  });
318
320
  });
319
321
  writePidFile();
322
+ this.onListening?.();
320
323
  // After the socket is ours: a relay refused as a duplicate must not keep sessions alive.
321
324
  this.acpKeepAlive = (0, daemon_glue_1.startAcpUpkeep)(this.acpAlive.run);
322
325
  this.codexContext.start();
package/dist/runtimes.js CHANGED
@@ -192,6 +192,7 @@ ${ctx.invocation}
192
192
  the same for this session and differs for other sessions. If verification
193
193
  fails, ask the user for a name.
194
194
  - With neither a name nor \`--group\` nor \`--sessions\`: run \`list_sessions\` and stop.
195
+ - With \`--bay <Bay>\`: join that Bay, through its own connection (see "Which Bay").
195
196
 
196
197
  ## The one rule that outranks everything else
197
198
 
@@ -206,6 +207,8 @@ Answering in the wrong room is the worst failure this feature has.
206
207
 
207
208
  ${help_topics_1.ROOMS_TOPIC}
208
209
 
210
+ ${claude_onboarding_1.BAY_CHOICE}
211
+
209
212
  ${ctx.runtime === "claude" || ctx.runtime === "codex" ? (0, session_setup_1.sessionSetupSteps)(ctx.runtime) : ""}
210
213
 
211
214
  ## Steps
@@ -364,7 +367,7 @@ exports.RUNTIME_SPECS = {
364
367
  frontmatter: true,
365
368
  render: (ctx) => renderCommand(ctx, true),
366
369
  },
367
- invocation: '/baychat <name> ["<Group Title>"]',
370
+ invocation: '/baychat <name> ["<Group Title>"] [--bay <Bay>]',
368
371
  // Verified: `CLAUDE_CODE_SESSION_ID` is present in the environment of every
369
372
  // Bash tool call and equals the id of the transcript the session is writing,
370
373
  // which is exactly what `claude --resume` takes.
@@ -392,7 +395,7 @@ for a session that has genuinely ended, not the plan for one that is still open.
392
395
  frontmatter: true,
393
396
  render: (ctx) => renderCommand(ctx, true),
394
397
  },
395
- invocation: '$baychat <name> ["<Group Title>"]',
398
+ invocation: '$baychat <name> ["<Group Title>"] [--bay <Bay>]',
396
399
  // NO `sessionIdExpr`, and the reason is not the one this comment used to give.
397
400
  //
398
401
  // Codex DOES export `$CODEX_THREAD_ID` — an attach on 2026-08-31 printed
@@ -510,7 +513,7 @@ DELIVERY PENDING and waits for a human — re-arm attach after every wake, witho
510
513
  // this copy SHADOWS a Codex skill a client already has — which would tell dsh to attach as
511
514
  // `--runtime codex`. It must be written here, never shared with the Codex file.
512
515
  command: { dir: ".dsh/skills/baychat", file: "SKILL.md", frontmatter: true, render: (ctx) => renderCommand(ctx, true) },
513
- invocation: '/baychat <name> ["<Group Title>"]',
516
+ invocation: '/baychat <name> ["<Group Title>"] [--bay <Bay>]',
514
517
  relay: { runtime: "dsh", reArm: "background-every-wake", resumeNote: GENERIC_RESUME_NOTE },
515
518
  needsRestart: true,
516
519
  },
@@ -5,6 +5,7 @@ exports.cmdJoinSession = cmdJoinSession;
5
5
  const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
6
6
  const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
7
7
  const config_1 = require("./config");
8
+ const bays_1 = require("./bays");
8
9
  const session_name_1 = require("./session-name");
9
10
  const owner_pid_1 = require("./relay/owner-pid");
10
11
  const terminal_pane_1 = require("./relay/terminal-pane");
@@ -26,7 +27,8 @@ function parseJoinArgs(args) {
26
27
  if (argument.startsWith("--")) {
27
28
  if (argument !== "--group" &&
28
29
  argument !== "--runtime" &&
29
- argument !== "--session") {
30
+ argument !== "--session" &&
31
+ argument !== "--bay") {
30
32
  throw new Error(`Unknown join option: ${argument}`);
31
33
  }
32
34
  const value = args[++index];
@@ -45,7 +47,7 @@ function parseJoinArgs(args) {
45
47
  }
46
48
  }
47
49
  if (positionals.length > 2)
48
- throw new Error("Usage: baychat join [name] [group] [--runtime runtime]");
50
+ throw new Error("Usage: baychat join [name] [group] [--runtime runtime] [--bay Bay]");
49
51
  if (positionals[1] && options.group)
50
52
  throw new Error("Choose the group once, as a title or with --group.");
51
53
  if (options.session && positionals.length)
@@ -67,6 +69,11 @@ async function cmdJoinSession(args) {
67
69
  const device = (0, config_1.loadDeviceCredentials)();
68
70
  if (!device)
69
71
  throw new Error("Connect this computer once with baychat connect codex or baychat connect claude, then retry.");
72
+ // ONE LOGIN PER BAY: `--bay` picks which of this computer's logins the session joins with,
73
+ // and every call it makes uses that login — so it can only ever reach that Bay. With no
74
+ // `--bay`, the default Bay, exactly as before. An unknown name is refused, never guessed.
75
+ const bay = options.bay ? (0, bays_1.findBayLogin)((0, config_1.loadBayLogins)(), options.bay) : undefined;
76
+ const token = bay ? bay.token : device.token;
70
77
  const joining = Boolean(options.session || options.group || options.destination);
71
78
  const runtime = options.runtime ?? (0, owner_pid_1.detectRuntime)(profiles_1.RUNTIME_PROFILES);
72
79
  if (joining && !runtime)
@@ -88,7 +95,7 @@ async function cmdJoinSession(args) {
88
95
  try {
89
96
  await client.connect(new streamableHttp_js_1.StreamableHTTPClientTransport(new URL("/api/mcp", device.baseUrl), {
90
97
  requestInit: {
91
- headers: { Authorization: `Bearer ${device.token}` },
98
+ headers: { Authorization: `Bearer ${token}` },
92
99
  signal: AbortSignal.timeout(15_000),
93
100
  },
94
101
  }));
@@ -155,7 +162,11 @@ async function cmdJoinSession(args) {
155
162
  title?.trim().toLowerCase() !== options.group.trim().toLowerCase())) {
156
163
  throw new Error("The server did not confirm the requested group. Incoming delivery was not attached.");
157
164
  }
158
- console.log((0, message_format_1.cleanTerminalText)(`Joined as "${name}" in ${isGroup ? `group "${title ?? "Untitled"}"` : "private chat"} (${conversation.id}).`));
165
+ const joinedBay = structured?.bay && typeof structured.bay === "object" && "name" in structured.bay &&
166
+ typeof structured.bay.name === "string" && structured.bay.name
167
+ ? ` in Bay "${structured.bay.name}"`
168
+ : "";
169
+ console.log((0, message_format_1.cleanTerminalText)(`Joined as "${name}"${joinedBay} in ${isGroup ? `group "${title ?? "Untitled"}"` : "private chat"} (${conversation.id}).`));
159
170
  console.log((0, message_format_1.cleanTerminalText)(`Use session="${name}" and conversationId="${conversation.id}" on chat calls.`));
160
171
  // Keep the room's actual rules, not the remote client's transport menu:
161
172
  // this command is already responsible for registering incoming delivery.
@@ -181,9 +192,18 @@ async function cmdJoinSession(args) {
181
192
  // Installing a service does not prove it started. Confirm its socket answers
182
193
  // before claiming that an incoming message has somewhere to go.
183
194
  const startup = await (0, commands_1.ensureRelayInstalled)();
195
+ // A non-default Bay has its own relay, run by the main one (relay/bay-relays.ts); from here
196
+ // on this process talks to THAT one. It can take up to the supervisor's interval to appear
197
+ // after a fresh login, so it is given longer than the main relay's one second.
198
+ const otherBay = bay && !bay.isDefault;
199
+ if (otherBay)
200
+ (0, commands_1.useBayRelay)(bay.bayId);
184
201
  let status = await (0, commands_1.tryRelayStatus)();
185
- for (let attempt = 0; !status && attempt < 10; attempt++) {
186
- await new Promise((resolve) => setTimeout(resolve, 100));
202
+ const attempts = otherBay ? 70 : 10;
203
+ if (!status && otherBay)
204
+ console.log(`Waiting for the ${bay.bayName} relay…`);
205
+ for (let attempt = 0; !status && attempt < attempts; attempt++) {
206
+ await new Promise((resolve) => setTimeout(resolve, otherBay ? 500 : 100));
187
207
  status = await (0, commands_1.tryRelayStatus)();
188
208
  }
189
209
  if (!status) {
@@ -5,7 +5,7 @@ exports.renderSkillBootstrap = renderSkillBootstrap;
5
5
  function renderSkillBootstrap(runtime) {
6
6
  const invocation = runtime === "codex" ? "$baychat" : "/baychat";
7
7
  const tools = runtime === "claude"
8
- ? "allowed-tools:\n - Bash\n - Monitor\n - mcp__baychat\n"
8
+ ? "allowed-tools:\n - Bash\n - Monitor\n - mcp__baychat\n - mcp__baychat_channel\n"
9
9
  : "";
10
10
  return `---
11
11
  name: baychat
@@ -45,6 +45,7 @@ const terminal_pane_1 = require("./relay/terminal-pane");
45
45
  const profiles_1 = require("./relay/profiles");
46
46
  const runtime_binary_1 = require("./runtime-binary");
47
47
  const update_command_1 = require("./update-command");
48
+ const launch_1 = require("./channel/launch");
48
49
  /**
49
50
  * `baychat start [name]` — the whole product in one command.
50
51
  *
@@ -130,25 +131,32 @@ async function cmdStart(args) {
130
131
  console.error(`Could not start ${runtime}: ${binary.reason}`);
131
132
  return 1;
132
133
  }
134
+ // THE WAKE-UP CHANNEL (docs/features/CLAUDE_CODE_CHANNEL.md). Claude Code delivers a channel's
135
+ // events only to a session started with the flag, so this is where it is switched on — when
136
+ // `connect claude` registered the server and this Claude Code is new enough to take the flag
137
+ // (an unknown flag would stop it starting). Otherwise it starts exactly as before and the
138
+ // session listens through a Monitor.
139
+ const agentArgs = runtime === "claude" ? channelArgs(binary.path) : [];
140
+ const env = agentArgs.length ? { ...process.env, BAYCHAT_CHANNEL: "1" } : process.env;
133
141
  // ALREADY INSIDE ONE? Then just run the agent. Nesting multiplexers sends every prefix
134
142
  // key to the wrong one, and the person already has everything this command provides.
135
143
  const inside = (0, terminal_pane_1.detectPane)();
136
144
  const multiplexer = inside ? undefined : firstAvailable(["tmux", "screen"]);
137
145
  if (!multiplexer) {
138
- const result = (0, child_process_1.spawnSync)(binary.path, [], { stdio: "inherit" });
146
+ const result = (0, child_process_1.spawnSync)(binary.path, agentArgs, { stdio: "inherit", env });
139
147
  return result.status ?? 0;
140
148
  }
141
149
  const sessionName = name || "baychat";
142
150
  const argv = multiplexer === "tmux"
143
151
  ? // `-A` attaches to an existing session of that name instead of failing, so running
144
152
  // this twice returns you to your agent rather than erroring at you.
145
- ["new", "-A", "-s", sessionName, binary.path]
146
- : ["-S", sessionName, "-R", binary.path];
147
- const result = (0, child_process_1.spawnSync)(multiplexer, argv, { stdio: "inherit" });
153
+ ["new", "-A", "-s", sessionName, binary.path, ...agentArgs]
154
+ : ["-S", sessionName, "-R", binary.path, ...agentArgs];
155
+ const result = (0, child_process_1.spawnSync)(multiplexer, argv, { stdio: "inherit", env });
148
156
  if (result.error) {
149
157
  // Falling back rather than failing: the multiplexer is an enhancement, and refusing to
150
158
  // start somebody's agent because of one would be the tail wagging the dog.
151
- const direct = (0, child_process_1.spawnSync)(binary.path, [], { stdio: "inherit" });
159
+ const direct = (0, child_process_1.spawnSync)(binary.path, agentArgs, { stdio: "inherit", env });
152
160
  return direct.status ?? 0;
153
161
  }
154
162
  return result.status ?? 0;
@@ -233,3 +241,12 @@ function valueAfter(args, flagName) {
233
241
  const index = args.indexOf(flagName);
234
242
  return index === -1 ? undefined : args[index + 1];
235
243
  }
244
+ /** The channel flag for this Claude Code, or nothing when it is not registered or too old. */
245
+ function channelArgs(claudePath) {
246
+ if (!(0, launch_1.channelRegistered)())
247
+ return [];
248
+ const probe = (0, child_process_1.spawnSync)(claudePath, ["--version"], { encoding: "utf8", timeout: 10_000 });
249
+ if (probe.error || probe.status !== 0)
250
+ return [];
251
+ return (0, launch_1.claudeSupportsChannel)((0, launch_1.parseClaudeVersion)(String(probe.stdout ?? ""))) ? [...launch_1.CHANNEL_LAUNCH_ARGS] : [];
252
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.25.3",
3
+ "version": "0.27.0",
4
4
  "description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
5
5
  "bin": {
6
6
  "baychat": "dist/index.js"