baychat 0.25.2 → 0.26.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.
- package/README.md +19 -0
- package/dist/bay-mcp.js +177 -0
- package/dist/bays-command.js +60 -0
- package/dist/bays.js +145 -0
- package/dist/claude-onboarding.js +42 -9
- package/dist/client-config-writer.js +67 -19
- package/dist/commands.js +37 -4
- package/dist/config.js +31 -2
- package/dist/connect-claude.js +7 -0
- package/dist/device-login.js +39 -2
- package/dist/doctor-command.js +14 -0
- package/dist/doctor.js +38 -0
- package/dist/main.js +23 -2
- package/dist/protocol-content.js +1 -1
- package/dist/relay/autostart.js +77 -2
- package/dist/relay/bay-relays.js +224 -0
- package/dist/relay/commands.js +70 -3
- package/dist/relay/daemon.js +3 -0
- package/dist/runtimes.js +6 -3
- package/dist/session-command.js +26 -6
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/relay/commands.js
CHANGED
|
@@ -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");
|
|
@@ -101,6 +104,12 @@ function request(sock, frame, timeoutMs = 5_000) {
|
|
|
101
104
|
clearTimeout(timer);
|
|
102
105
|
reject(err);
|
|
103
106
|
});
|
|
107
|
+
// The relay went away without answering. Waiting out the timer would only delay
|
|
108
|
+
// the same answer; the caller decides whether gone was what it wanted.
|
|
109
|
+
sock.on("close", () => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
reject(new Error("relay closed the connection without answering"));
|
|
112
|
+
});
|
|
104
113
|
(0, socket_1.writeFrame)(sock, frame);
|
|
105
114
|
});
|
|
106
115
|
}
|
|
@@ -113,13 +122,34 @@ function request(sock, frame, timeoutMs = 5_000) {
|
|
|
113
122
|
*/
|
|
114
123
|
async function cmdRelayStart(opts = {}) {
|
|
115
124
|
if (opts.foreground) {
|
|
116
|
-
|
|
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() });
|
|
117
138
|
const shutdown = () => {
|
|
139
|
+
supervisor?.stop();
|
|
118
140
|
void daemon.stop().then(() => process.exit(0));
|
|
119
141
|
};
|
|
120
142
|
process.on("SIGINT", shutdown);
|
|
121
143
|
process.on("SIGTERM", shutdown);
|
|
122
|
-
|
|
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
|
+
}
|
|
123
153
|
return;
|
|
124
154
|
}
|
|
125
155
|
if (await (0, socket_1.probeSocket)((0, socket_1.socketPath)())) {
|
|
@@ -223,6 +253,32 @@ async function registerAcpSession(frame) {
|
|
|
223
253
|
throw err;
|
|
224
254
|
}
|
|
225
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
|
+
}
|
|
226
282
|
async function cmdRelayStatus() {
|
|
227
283
|
let status;
|
|
228
284
|
try {
|
|
@@ -237,6 +293,8 @@ async function cmdRelayStatus() {
|
|
|
237
293
|
console.log(` polls: ${status.polls} delivered: ${status.delivered} cursor: ${status.cursor ?? "—"}`);
|
|
238
294
|
if (status.lastError)
|
|
239
295
|
console.log(` last error: ${status.lastError}`);
|
|
296
|
+
for (const line of await bayRelayLines())
|
|
297
|
+
console.log(line);
|
|
240
298
|
// Computed before the session list so a line can report its own last failure.
|
|
241
299
|
const byName = new Map(status.sessions.map((s) => [s.name, s]));
|
|
242
300
|
/**
|
|
@@ -386,7 +444,16 @@ async function cmdRelayStop() {
|
|
|
386
444
|
if (await (0, socket_1.probeSocket)((0, socket_1.socketPath)())) {
|
|
387
445
|
// A windowless supervisor may stop without killing its child. Ask the
|
|
388
446
|
// relay itself to shut down, and wait so the next start cannot reuse it.
|
|
389
|
-
|
|
447
|
+
try {
|
|
448
|
+
await request(await connectOrFail(), { type: "stop" });
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
// launchd's bootout above already sent the relay SIGTERM; it can accept this
|
|
452
|
+
// connection and exit before answering. A relay that is gone is what was asked
|
|
453
|
+
// for — only a relay still listening is a failure to report.
|
|
454
|
+
if (await (0, socket_1.probeSocket)((0, socket_1.socketPath)()))
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
390
457
|
const deadline = Date.now() + 5_000;
|
|
391
458
|
while (await (0, socket_1.probeSocket)((0, socket_1.socketPath)())) {
|
|
392
459
|
if (Date.now() >= deadline)
|
package/dist/relay/daemon.js
CHANGED
|
@@ -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
|
},
|
package/dist/session-command.js
CHANGED
|
@@ -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 ${
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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"
|