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.
- package/README.md +26 -0
- package/dist/bay-mcp.js +177 -0
- package/dist/bays-command.js +60 -0
- package/dist/bays.js +145 -0
- package/dist/channel/detect.js +145 -0
- package/dist/channel/feed.js +281 -0
- package/dist/channel/launch.js +111 -0
- package/dist/channel/server.js +275 -0
- package/dist/claude-onboarding.js +61 -12
- 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 +40 -1
- package/dist/device-login.js +39 -2
- package/dist/doctor-command.js +14 -0
- package/dist/doctor.js +38 -0
- package/dist/main.js +31 -2
- package/dist/protocol-content.js +1 -1
- package/dist/relay/bay-relays.js +224 -0
- package/dist/relay/commands.js +54 -2
- package/dist/relay/daemon.js +3 -0
- package/dist/runtimes.js +6 -3
- package/dist/session-command.js +26 -6
- package/dist/skill-bootstrap.js +1 -1
- package/dist/start-command.js +22 -5
- 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");
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
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/dist/skill-bootstrap.js
CHANGED
|
@@ -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
|
package/dist/start-command.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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.
|
|
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"
|