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/dist/commands.js CHANGED
@@ -20,6 +20,7 @@ exports.cmdWatch = cmdWatch;
20
20
  exports.cmdLink = cmdLink;
21
21
  exports.quoteForCmd = quoteForCmd;
22
22
  exports.claudeMcpAddSpawn = claudeMcpAddSpawn;
23
+ exports.refreshConnectedClients = refreshConnectedClients;
23
24
  exports.registerWithClaude = registerWithClaude;
24
25
  exports.cmdLogin = cmdLogin;
25
26
  exports.deviceExpiryWarning = deviceExpiryWarning;
@@ -35,6 +36,7 @@ const api_1 = require("./api");
35
36
  const protocol_1 = require("./protocol");
36
37
  const connection_qr_1 = require("./connection-qr");
37
38
  const config_1 = require("./config");
39
+ const bay_mcp_1 = require("./bay-mcp");
38
40
  const context_1 = require("./context");
39
41
  const runtime_install_1 = require("./runtime-install");
40
42
  const credential_refresh_1 = require("./credential-refresh");
@@ -818,6 +820,9 @@ async function cmdLogin(opts = {}) {
818
820
  console.log(`Approve this device in BayChat, then run baychat login --finish on this same computer. Link expires ${request.expiresAt}.`);
819
821
  return true;
820
822
  }
823
+ // Read BEFORE the new login is saved: the per-Bay MCP entries the previous login wrote are
824
+ // removed when they are no longer wanted (MULTI_BAY_DEVICE_LOGIN.md).
825
+ const previousBays = (0, config_1.loadBayLogins)();
821
826
  if (opts.mode === "finish") {
822
827
  const result = await (0, device_login_1.finishDeviceLogin)(opts.base || process.env.BAYCHAT_API_URL);
823
828
  if (result.status === "pending") {
@@ -830,6 +835,8 @@ async function cmdLogin(opts = {}) {
830
835
  completeDeviceLogin(result.device, {
831
836
  ...opts,
832
837
  message: `✓ Logged in as ${result.device.user.name}`,
838
+ bays: result.bays,
839
+ previousBays,
833
840
  });
834
841
  console.log("Reconnect BayChat in your client to load the refreshed login, then retry the same session name.");
835
842
  return true;
@@ -844,6 +851,7 @@ async function cmdLogin(opts = {}) {
844
851
  }, {
845
852
  ...opts,
846
853
  message: `✓ Logged in as ${me.user.name} (${me.tenant.name})`,
854
+ previousBays,
847
855
  });
848
856
  return true;
849
857
  }
@@ -894,22 +902,47 @@ async function cmdLogin(opts = {}) {
894
902
  token: status.token,
895
903
  user: status.user,
896
904
  expiresAt: status.expiresAt,
897
- }, { ...opts, message: `✓ Logged in as ${status.user.name}` });
905
+ }, {
906
+ ...opts,
907
+ message: `✓ Logged in as ${status.user.name}`,
908
+ bays: (0, device_login_1.parseBayLogins)(status.bays, status.token),
909
+ previousBays,
910
+ });
898
911
  return true;
899
912
  }
900
913
  }
901
914
  console.log("Login request expired — run baychat login again.");
902
915
  return false;
903
916
  }
904
- /** Persist an approved login before refreshing the clients that use it. */
917
+ /**
918
+ * Persist an approved login before refreshing the clients that use it.
919
+ *
920
+ * `bays` is the per-Bay set a multi-Bay approval delivered (MULTI_BAY_DEVICE_LOGIN.md). The
921
+ * default Bay is `device` and is written to every client exactly as before; the OTHER Bays are
922
+ * added beside it as `baychat-<bay>` — only when there are two or more, so a one-Bay login
923
+ * changes nothing it did not change before.
924
+ */
905
925
  function completeDeviceLogin(device, options) {
906
- (0, config_1.saveDeviceCredentials)(device);
926
+ (0, config_1.saveDeviceCredentials)(device, options.bays);
907
927
  console.log(options.message);
928
+ const bays = options.bays ?? [];
929
+ if (bays.length > 1) {
930
+ const names = bays.map((b) => (b.isDefault ? `${b.bayName} (default)` : b.bayName)).join(", ");
931
+ console.log(` This computer can join: ${names}`);
932
+ }
908
933
  refreshConnectedClients(device.baseUrl, device.token);
909
934
  if (options.registerClaude !== false)
910
935
  registerWithClaude(device.baseUrl, device.token);
911
- if (options.hint !== false)
936
+ (0, bay_mcp_1.applyBayEntries)(device.baseUrl, options.previousBays ?? [], bays, {
937
+ claude: options.registerClaude !== false,
938
+ io: exports.realConfigIo,
939
+ env: (0, client_paths_1.currentPathEnv)(),
940
+ });
941
+ if (options.hint !== false) {
912
942
  console.log("\n Run /baychat <name> in any session.");
943
+ if (bays.length > 1)
944
+ console.log(" In another Bay: /baychat <name> --bay <Bay>. See them all: baychat bays.");
945
+ }
913
946
  }
914
947
  /**
915
948
  * The renewal nudge for a device credential, or null when none is due. Device
package/dist/config.js CHANGED
@@ -38,6 +38,7 @@ exports.configDir = configDir;
38
38
  exports.saveCredentials = saveCredentials;
39
39
  exports.loadCredentials = loadCredentials;
40
40
  exports.saveDeviceCredentials = saveDeviceCredentials;
41
+ exports.loadBayLogins = loadBayLogins;
41
42
  exports.loadDeviceCredentials = loadDeviceCredentials;
42
43
  exports.loadCursor = loadCursor;
43
44
  exports.saveCursor = saveCursor;
@@ -141,8 +142,36 @@ function loadCredentials() {
141
142
  return null;
142
143
  return { baseUrl: String(file.baseUrl ?? exports.DEFAULT_API_URL), token: file.token, agent };
143
144
  }
144
- function saveDeviceCredentials(device) {
145
- writeMerged({ device });
145
+ /**
146
+ * Save a device login. `bays` is the per-Bay set a multi-Bay approval delivered; a login that
147
+ * delivered none (an older server, `--token`) CLEARS any previous set, so `device` and `bays`
148
+ * can never describe two different logins. (`undefined` drops the key: JSON.stringify omits it.)
149
+ */
150
+ function saveDeviceCredentials(device, bays) {
151
+ writeMerged({ device, bays: bays && bays.length > 0 ? bays : undefined });
152
+ }
153
+ /**
154
+ * Every Bay login on this computer, or `[]` when the login predates multi-Bay approval, when
155
+ * the device token comes from `BAYCHAT_DEVICE_TOKEN`, or when the stored set is unusable.
156
+ * `[]` means exactly today's behaviour: one login, `device`.
157
+ */
158
+ function loadBayLogins() {
159
+ if (process.env.BAYCHAT_DEVICE_TOKEN)
160
+ return [];
161
+ const raw = loadFile().bays;
162
+ if (!Array.isArray(raw))
163
+ return [];
164
+ const bays = raw.filter((b) => !!b &&
165
+ typeof b === "object" &&
166
+ typeof b.bayId === "string" &&
167
+ typeof b.bayName === "string" &&
168
+ typeof b.token === "string" &&
169
+ b.token.length > 0 &&
170
+ typeof b.expiresAt === "string");
171
+ // A set with no single default is not one this CLI wrote; trust `device` instead.
172
+ if (bays.length === 0 || bays.filter((b) => b.isDefault === true).length !== 1)
173
+ return [];
174
+ return bays.map((b) => ({ ...b, isDefault: b.isDefault === true }));
146
175
  }
147
176
  function loadDeviceCredentials() {
148
177
  // Same env-override shape as BAYCHAT_TOKEN: a headless/CI box can supply a
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cmdConnectClaude = cmdConnectClaude;
4
4
  const commands_1 = require("./commands");
5
5
  const config_1 = require("./config");
6
+ const bay_mcp_1 = require("./bay-mcp");
7
+ const client_paths_1 = require("./client-paths");
6
8
  const runtime_install_1 = require("./runtime-install");
7
9
  /** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
8
10
  async function cmdConnectClaude(opts = {}) {
@@ -32,6 +34,11 @@ async function cmdConnectClaude(opts = {}) {
32
34
  if (!device)
33
35
  throw new Error("No device login was saved. Retry baychat connect claude.");
34
36
  const registered = (0, commands_1.registerWithClaude)(device.baseUrl, device.token);
37
+ // The other Bays this login covers, as `baychat-<bay>` (none for a one-Bay login). Passing
38
+ // the current set as "previous" too removes only entries that no longer belong — e.g. the
39
+ // Bay that is now the default.
40
+ const bays = (0, config_1.loadBayLogins)();
41
+ (0, bay_mcp_1.applyBayEntries)(device.baseUrl, bays, bays, { claude: true, io: commands_1.realConfigIo, env: (0, client_paths_1.currentPathEnv)() });
35
42
  for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
36
43
  console.log(" " + line);
37
44
  if (!registered)
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseBayLogins = parseBayLogins;
36
37
  exports.startDeviceLogin = startDeviceLogin;
37
38
  exports.finishDeviceLogin = finishDeviceLogin;
38
39
  const fs = __importStar(require("node:fs"));
@@ -42,12 +43,46 @@ const node_crypto_1 = require("node:crypto");
42
43
  const zod_1 = require("zod");
43
44
  const api_1 = require("./api");
44
45
  const config_1 = require("./config");
46
+ const bayLoginSchema = zod_1.z.object({
47
+ bayId: zod_1.z.string().min(1),
48
+ bayName: zod_1.z.string(),
49
+ token: zod_1.z.string().min(1),
50
+ expiresAt: zod_1.z.string().datetime(),
51
+ isDefault: zod_1.z.boolean(),
52
+ });
45
53
  const deviceSchema = zod_1.z.object({
46
54
  baseUrl: zod_1.z.string().url(),
47
55
  token: zod_1.z.string().min(1),
48
56
  user: zod_1.z.object({ id: zod_1.z.string().min(1), name: zod_1.z.string() }),
49
57
  expiresAt: zod_1.z.string().datetime(),
58
+ /** Every Bay's login (MULTI_BAY_DEVICE_LOGIN.md); absent from an older server. */
59
+ bays: zod_1.z.array(bayLoginSchema).optional(),
50
60
  });
61
+ /**
62
+ * The per-Bay logins in an approval, or `undefined` to behave exactly as before (one login).
63
+ *
64
+ * The set is used only when it is coherent with the top-level token every older reader uses:
65
+ * exactly one default, and the default's token IS the top-level token. Anything else is
66
+ * ignored rather than half-trusted — `device` alone still works, and a mismatched set would
67
+ * route sessions to a Bay the approval did not make the default.
68
+ */
69
+ function parseBayLogins(raw, topLevelToken) {
70
+ const parsed = zod_1.z.array(bayLoginSchema.passthrough()).safeParse(raw);
71
+ if (!parsed.success || parsed.data.length === 0)
72
+ return undefined;
73
+ const defaults = parsed.data.filter((b) => b.isDefault);
74
+ if (defaults.length !== 1 || defaults[0].token !== topLevelToken)
75
+ return undefined;
76
+ if (new Set(parsed.data.map((b) => b.bayId)).size !== parsed.data.length)
77
+ return undefined;
78
+ return parsed.data.map(({ bayId, bayName, token, expiresAt, isDefault }) => ({
79
+ bayId,
80
+ bayName,
81
+ token,
82
+ expiresAt,
83
+ isDefault,
84
+ }));
85
+ }
51
86
  const pendingSchema = zod_1.z.object({
52
87
  baseUrl: zod_1.z.string().url(),
53
88
  id: zod_1.z.string().min(1),
@@ -105,6 +140,7 @@ async function finishDeviceLogin(baseUrl) {
105
140
  throw new Error("BayChat returned an invalid login status. Run baychat login --start again.");
106
141
  const parsed = deviceSchema.safeParse({
107
142
  ...status,
143
+ bays: parseBayLogins(status.bays, status.token),
108
144
  baseUrl: pending.baseUrl,
109
145
  });
110
146
  if (!parsed.success)
@@ -114,9 +150,10 @@ async function finishDeviceLogin(baseUrl) {
114
150
  // existing credentials fails; the next finish must not consume it again.
115
151
  writePending({ ...pending, approved: device });
116
152
  }
117
- (0, config_1.saveDeviceCredentials)(device);
153
+ const { bays, ...single } = device;
154
+ (0, config_1.saveDeviceCredentials)(single, bays);
118
155
  fs.unlinkSync(pendingPath());
119
- return { status: "approved", device };
156
+ return { status: "approved", device: single, ...(bays ? { bays } : {}) };
120
157
  });
121
158
  }
122
159
  function pendingPath() {
@@ -48,6 +48,8 @@ const os = __importStar(require("os"));
48
48
  const config_1 = require("./config");
49
49
  const doctor_1 = require("./doctor");
50
50
  const commands_1 = require("./relay/commands");
51
+ const bay_relays_1 = require("./relay/bay-relays");
52
+ const socket_1 = require("./relay/socket");
51
53
  const registry_1 = require("./relay/registry");
52
54
  const credential_refresh_1 = require("./credential-refresh");
53
55
  const client_paths_1 = require("./client-paths");
@@ -75,7 +77,19 @@ async function currentDoctorEnv() {
75
77
  // reached for in the first place.
76
78
  const registry = new registry_1.SessionRegistry();
77
79
  registry.load();
80
+ // One line per Bay login; a non-default Bay's relay is its own process (relay/bay-relays.ts).
81
+ const bays = (0, config_1.loadBayLogins)();
82
+ const bayRelayAlive = {};
83
+ if (bays.length > 1) {
84
+ for (const bay of bays) {
85
+ if (bay.isDefault)
86
+ continue;
87
+ bayRelayAlive[bay.bayId] = await (0, socket_1.probeSocket)((0, bay_relays_1.bayRelayScope)(bay.bayId, (0, config_1.configDir)(), (0, socket_1.socketPath)()).socket);
88
+ }
89
+ }
78
90
  return {
91
+ bays,
92
+ bayRelayAlive,
79
93
  home: os.homedir(),
80
94
  platform: process.platform,
81
95
  now: Date.now(),
package/dist/doctor.js CHANGED
@@ -60,6 +60,7 @@ exports.mailboxCheck = mailboxCheck;
60
60
  exports.acpChecks = acpChecks;
61
61
  exports.exitCodeFor = exitCodeFor;
62
62
  exports.renderReport = renderReport;
63
+ exports.bayChecks = bayChecks;
63
64
  const fs = __importStar(require("fs"));
64
65
  const os = __importStar(require("os"));
65
66
  const path = __importStar(require("path"));
@@ -106,6 +107,7 @@ const EXPIRY_WARNING_MS = 3 * 86_400_000;
106
107
  function buildReport(env) {
107
108
  return {
108
109
  credentials: credentialsCheck(env),
110
+ bays: bayChecks(env),
109
111
  relay: relayCheck(env),
110
112
  mailbox: mailboxCheck(),
111
113
  staleCredentials: staleCredentialCheck(env),
@@ -269,6 +271,8 @@ function exitCodeFor(report) {
269
271
  function renderReport(report) {
270
272
  const lines = [];
271
273
  lines.push(...renderSummaryCheck(report.credentials));
274
+ for (const check of report.bays)
275
+ lines.push(...renderSummaryCheck(check));
272
276
  lines.push(...renderSummaryCheck(report.staleCredentials));
273
277
  lines.push(...renderSummaryCheck(report.relay));
274
278
  lines.push(...renderSummaryCheck(report.mailbox));
@@ -286,6 +290,39 @@ function renderReport(report) {
286
290
  return lines;
287
291
  }
288
292
  // ─── Individual checks ──────────────────────────────────────────────────────
293
+ /**
294
+ * One line per Bay login. An expired Bay login fails exactly as an expired device login does
295
+ * (`baychat login` renews every Bay at once); a non-default Bay whose relay does not answer
296
+ * warns, because only that Bay's terminal sessions lose their wake-ups.
297
+ */
298
+ function bayChecks(env) {
299
+ const bays = env.bays ?? [];
300
+ if (bays.length < 2)
301
+ return [];
302
+ return bays.map((bay) => {
303
+ const name = "bay";
304
+ const label = `"${bay.bayName}"${bay.isDefault ? " (default)" : ""}`;
305
+ const expiresAt = Date.parse(bay.expiresAt);
306
+ if (Number.isNaN(expiresAt) || expiresAt <= env.now) {
307
+ return { name, status: "fail", detail: `${label} — login expired`, remedy: "baychat login" };
308
+ }
309
+ const left = remaining(expiresAt - env.now);
310
+ const alive = bay.isDefault ? undefined : env.bayRelayAlive?.[bay.bayId];
311
+ const relay = alive === undefined ? "" : alive ? ", relay running" : ", relay NOT running";
312
+ if (alive === false) {
313
+ return {
314
+ name,
315
+ status: "warn",
316
+ detail: `${label} — ${left} left${relay}`,
317
+ remedy: "baychat relay stop && baychat relay start",
318
+ };
319
+ }
320
+ const warn = expiresAt - env.now <= EXPIRY_WARNING_MS;
321
+ return warn
322
+ ? { name, status: "warn", detail: `${label} — ${left} left${relay}`, remedy: "baychat login" }
323
+ : { name, status: "pass", detail: `${label} — ${left} left${relay}` };
324
+ });
325
+ }
289
326
  function credentialsCheck(env) {
290
327
  if (!env.device) {
291
328
  return {
@@ -599,6 +636,7 @@ function withRemedy(line, check) {
599
636
  function allChecks(report) {
600
637
  return [
601
638
  report.credentials,
639
+ ...report.bays,
602
640
  report.staleCredentials,
603
641
  report.relay,
604
642
  report.mailbox,
package/dist/main.js CHANGED
@@ -9,6 +9,7 @@ const start_command_1 = require("./start-command");
9
9
  const update_command_1 = require("./update-command");
10
10
  const session_command_1 = require("./session-command");
11
11
  const doctor_command_1 = require("./doctor-command");
12
+ const bays_command_1 = require("./bays-command");
12
13
  const hermes_1 = require("./hermes");
13
14
  const connect_1 = require("./connect");
14
15
  const connect_plan_1 = require("./connect-plan");
@@ -16,6 +17,8 @@ const runtimes_1 = require("./runtimes");
16
17
  const mcp_1 = require("./mcp");
17
18
  const mcp_config_1 = require("./mcp-config");
18
19
  const commands_2 = require("./relay/commands");
20
+ const bays_1 = require("./bays");
21
+ const config_1 = require("./config");
19
22
  const help_topics_1 = require("./help-topics");
20
23
  const args_1 = require("./args");
21
24
  const owner_pid_1 = require("./relay/owner-pid");
@@ -44,7 +47,7 @@ Usage:
44
47
  the package, every connected client's config, and
45
48
  the relay daemon (which otherwise keeps running
46
49
  old code). \`baychat\` does this for you once a day
47
- baychat join [name] [group] [--sessions | --private | --group <title>] [--runtime <runtime>]
50
+ baychat join [name] [group] [--sessions | --private | --group <title>] [--runtime <runtime>] [--bay <Bay>]
48
51
  baychat join --session <name> --group <title> --runtime codex
49
52
  baychat skill --runtime codex|claude Read the current packaged skill workflow
50
53
  Join and connect incoming messages in this terminal
@@ -69,6 +72,10 @@ Usage:
69
72
  Log this laptop in to BayChat (QR) and add the
70
73
  BayChat MCP server to Claude Code. Creates a
71
74
  device credential only — no agent, no room
75
+ baychat bays [default <name>] The Bays this computer is logged in to. One phone
76
+ approval signs an Enterprise owner's computer in to
77
+ the Personal Bay AND its Team Bays; \`default\` picks
78
+ the one a session joins when it names no Bay
72
79
  baychat pair <code> [--base <url>] Redeem a pairing code from the BayChat app
73
80
  baychat hermes init [--home <dir>] [--enable]
74
81
  Install the BayChat platform plugin into a Hermes
@@ -267,6 +274,12 @@ async function main() {
267
274
  });
268
275
  return loggedIn ? 0 : 2; // 2 = not approved yet, or expired
269
276
  }
277
+ case "bays":
278
+ if (args.some((arg) => arg === "--help" || arg === "-h")) {
279
+ console.log(bays_command_1.BAYS_USAGE);
280
+ return 0;
281
+ }
282
+ return (0, bays_command_1.cmdBays)(args);
270
283
  case "pair": {
271
284
  if (!args[0])
272
285
  throw new Error("Usage: baychat pair <code>");
@@ -395,7 +408,15 @@ async function main() {
395
408
  "--resume-id",
396
409
  "--timeout",
397
410
  "--owner-pid",
398
- ], "baychat relay attach --session <name> [--runtime claude|codex|hermes] [--resume-id <id>] [--timeout <sec>]");
411
+ "--bay",
412
+ ], "baychat relay attach --session <name> [--runtime claude|codex|hermes] [--resume-id <id>] [--timeout <sec>] [--bay <Bay>]");
413
+ // A session in a non-default Bay attaches to THAT Bay's relay (MULTI_BAY_DEVICE_LOGIN.md).
414
+ const bayName = (0, args_1.flag)(rest, "--bay");
415
+ if (bayName) {
416
+ const bay = (0, bays_1.findBayLogin)((0, config_1.loadBayLogins)(), bayName);
417
+ if (!bay.isDefault)
418
+ (0, commands_2.useBayRelay)(bay.bayId);
419
+ }
399
420
  const timeoutSec = numberFlag(rest, "--timeout");
400
421
  return await (0, commands_2.cmdRelayAttach)({
401
422
  session,