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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CLAUDE_REACHABILITY = exports.CLAUDE_JOIN_STEPS = void 0;
|
|
3
|
+
exports.BAY_CHOICE = exports.CLAUDE_REACHABILITY = exports.CLAUDE_JOIN_STEPS = void 0;
|
|
4
4
|
/** Claude's native entry point. An expired local relay login says nothing about a separately configured MCP connection. */
|
|
5
5
|
exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Monitor**. Search deferred tools once
|
|
6
6
|
if needed, for \`join_session\`, \`listen_messages\`, \`get_delivery_status\`,
|
|
7
|
-
\`send_message\` and \`get_messages\` (also \`create_group\` for a named group)
|
|
7
|
+
\`send_message\` and \`get_messages\` (also \`create_group\` for a named group),
|
|
8
|
+
and the \`baychat_channel\` server's \`listen\` when it is present.
|
|
8
9
|
Check that native Monitor supports \`ws\` before creating membership.
|
|
9
10
|
If a required BayChat tool is absent, follow Automatic setup above once,
|
|
10
11
|
then continue these steps with the original name and room when tools are loaded.
|
|
@@ -23,11 +24,19 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
|
|
|
23
24
|
Do not ask again when the owner already requested creation. A supplied group title
|
|
24
25
|
also authorizes creation on GROUP_NOT_FOUND; never substitute a similarly named group.
|
|
25
26
|
If blocked, state the exact requested title and needed action in your reply.
|
|
26
|
-
3.
|
|
27
|
+
3. **Channel first.** If the \`baychat_channel\` server's \`listen\` tool exists
|
|
28
|
+
(\`mcp__baychat_channel__listen\`), call it with \`{session: "<confirmed name>"}\`
|
|
29
|
+
(add \`bay: "<Bay>"\` when the session joined a Bay other than the default).
|
|
30
|
+
If it answers **active**, messages now arrive as \`<channel source="baychat_channel">\`
|
|
31
|
+
events: open NO Monitor and do not call \`listen_messages\` — either replaces the
|
|
32
|
+
channel's feed. Go to step 4. If it answers inactive or failed, or the tool is
|
|
33
|
+
absent, use the Monitor instead:
|
|
34
|
+
call \`listen_messages({session: "<confirmed name>"})\`. Pass its returned
|
|
27
35
|
\`monitor\` object directly to the native **Monitor** tool: it contains
|
|
28
|
-
\`ws: {url, protocols}\` and \`
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
\`ws: {url, protocols}\` and \`timeout_ms\`. If your Monitor offers a
|
|
37
|
+
\`persistent\` option, also set \`persistent: true\`. Keep its ticket private.
|
|
38
|
+
This is a native WebSocket source, not a shell command. No terminal, relay or
|
|
39
|
+
attach loop is needed; when its deadline ends it, reopen it (below).
|
|
31
40
|
4. Call \`get_delivery_status({session: "<confirmed name>"})\`. Only \`connected\`
|
|
32
41
|
confirms incoming delivery. \`awaiting_connection\` means a ticket was created
|
|
33
42
|
but Monitor has not connected; show that exact state, not "ready".
|
|
@@ -50,17 +59,31 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
|
|
|
50
59
|
/** Native socket lifecycle, including the intentional-stop boundary. */
|
|
51
60
|
exports.CLAUDE_REACHABILITY = `## Staying reachable — native remote messages
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
**With the channel** (\`listen\` answered active): each new message arrives as a
|
|
63
|
+
\`<channel source="baychat_channel" event="messages" conversation_ids="…">\` event.
|
|
64
|
+
Read those conversations with \`get_messages\` and answer as below. There is no
|
|
65
|
+
deadline and nothing to re-arm; the channel reconnects by itself. \`event="reset"\`
|
|
66
|
+
means catch up with \`get_messages\`; \`event="stopped"\` says why the feed ended
|
|
67
|
+
(for an expired session: \`join_session\` again with the same name and room, then
|
|
68
|
+
\`listen\` again). Never open a Monitor beside an active channel.
|
|
69
|
+
|
|
70
|
+
**With a Monitor:** keep the native WebSocket Monitor open between messages. On a \`messages\` event,
|
|
54
71
|
read the indicated conversations with \`get_messages\`, deduplicate message IDs,
|
|
55
72
|
obey \`shouldRespond\`, acknowledge slow work with 👀, and answer in BayChat.
|
|
56
73
|
Use \`contact_agent\` for directed replies to peers. A delivery event is not a
|
|
57
74
|
model acknowledgement; never claim a message is read just because it arrived.
|
|
58
75
|
|
|
59
|
-
A \`reset\` event means catch up through history.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
A \`reset\` event means catch up through history. Close code **4000** means
|
|
77
|
+
intentionally stopped or replaced: **do not reconnect**.
|
|
78
|
+
|
|
79
|
+
**The Monitor has a deadline.** Claude Code ends every Monitor after at most 30
|
|
80
|
+
minutes ("Monitor timed out" / "Monitor expired"), whether or not anything
|
|
81
|
+
arrived. That is the runtime's limit, not a stop: **every time it happens, and on
|
|
82
|
+
any other close**, call \`listen_messages\` with this session and the last cursor
|
|
83
|
+
(from \`ready\` or \`messages\`), open one replacement native Monitor, and confirm
|
|
84
|
+
\`get_delivery_status\` again. The reopened feed replays what it missed, so no
|
|
85
|
+
\`get_messages\` catch-up is needed unless a \`reset\` arrives. Skipping this
|
|
86
|
+
leaves the session deaf to the Bay with its terminal still open.
|
|
64
87
|
Keep the last cursor in this conversation; do not save tickets in files.
|
|
65
88
|
|
|
66
89
|
\`stop_listening\` stops incoming delivery without ending membership.
|
|
@@ -68,3 +91,29 @@ Keep the last cursor in this conversation; do not save tickets in files.
|
|
|
68
91
|
that has been closed cannot answer until resumed. Do not promise instant model
|
|
69
92
|
replies: show connected delivery separately from actual reading and answering.
|
|
70
93
|
`;
|
|
94
|
+
/**
|
|
95
|
+
* Picking the Bay (docs/features/MULTI_BAY_DEVICE_LOGIN.md). One approval can sign a computer
|
|
96
|
+
* in to several Bays, and each is its own MCP connection — `baychat` for the default Bay,
|
|
97
|
+
* `baychat-<bay>` for each other. A session belongs to exactly one, for every call: tools of
|
|
98
|
+
* two Bays mixed in one session would post in a different Bay than the one it joined.
|
|
99
|
+
*/
|
|
100
|
+
exports.BAY_CHOICE = `## Which Bay
|
|
101
|
+
|
|
102
|
+
This computer may be logged in to more than one Bay (one approval gives an
|
|
103
|
+
Enterprise owner their Personal Bay and its Team Bays). Each Bay is its own
|
|
104
|
+
BayChat MCP connection: \`baychat\` is the default Bay, and every other Bay is
|
|
105
|
+
\`baychat-<bay>\` (for example \`baychat-harbor\`). \`baychat bays\` lists them.
|
|
106
|
+
|
|
107
|
+
- If the user named a Bay (\`--bay <Bay>\`, or "join Harbor"), use THAT Bay's
|
|
108
|
+
connection for EVERY BayChat call in this session — in Claude Code its tools are
|
|
109
|
+
\`mcp__baychat-<bay>__…\` (e.g. \`mcp__baychat-harbor__join_session\`), not the
|
|
110
|
+
plain \`mcp__baychat__…\` ones. Never mix two Bays' tools in one session: a call
|
|
111
|
+
through another Bay's connection reaches a different Bay.
|
|
112
|
+
- If no Bay was named and more than one \`baychat…\` connection is available, ask the
|
|
113
|
+
user ONCE which Bay before joining. With only \`baychat\`, use it.
|
|
114
|
+
- \`join_session\` reports the Bay it joined (\`bay.name\`). Say it in your one-line
|
|
115
|
+
confirmation; if it is not the Bay that was asked for, stop and say so.
|
|
116
|
+
- Shell commands take the same choice: \`baychat join … --bay "<Bay>"\`.
|
|
117
|
+
- A Bay named that has no connection here: say so and suggest \`baychat bays\`
|
|
118
|
+
(or \`baychat login\` to pick up new Bays). Never fall back to another Bay.
|
|
119
|
+
`;
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
// - Callers back up first (`backupPathFor`), so there is always a way back.
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.mergeTomlConfig = mergeTomlConfig;
|
|
26
|
+
exports.removeTomlServer = removeTomlServer;
|
|
26
27
|
exports.mergeJsonConfig = mergeJsonConfig;
|
|
28
|
+
exports.removeJsonServer = removeJsonServer;
|
|
27
29
|
exports.mergeClientConfig = mergeClientConfig;
|
|
28
30
|
exports.backupPathFor = backupPathFor;
|
|
29
31
|
exports.expandHome = expandHome;
|
|
@@ -31,9 +33,12 @@ exports.expandHome = expandHome;
|
|
|
31
33
|
* The TOML table header this owns, and the only span it may replace.
|
|
32
34
|
*
|
|
33
35
|
* Sub-tables (`[mcp_servers.baychat.env]`) belong to the same server and are
|
|
34
|
-
* replaced with it; a header for any OTHER server ends the span
|
|
36
|
+
* replaced with it; a header for any OTHER server ends the span — including a
|
|
37
|
+
* per-Bay `[mcp_servers.baychat-harbor]` (MULTI_BAY_DEVICE_LOGIN.md), which is
|
|
38
|
+
* its own server with its own name and never part of `baychat`'s span.
|
|
35
39
|
*/
|
|
36
40
|
const TOML_TABLE = "mcp_servers.baychat";
|
|
41
|
+
const tomlTable = (serverName) => `mcp_servers.${serverName}`;
|
|
37
42
|
/** Matches a top-level TOML table header line, capturing its dotted key. */
|
|
38
43
|
const TOML_HEADER = /^\s*\[\s*([^\]]+?)\s*\]\s*$/;
|
|
39
44
|
/**
|
|
@@ -42,12 +47,26 @@ const TOML_HEADER = /^\s*\[\s*([^\]]+?)\s*\]\s*$/;
|
|
|
42
47
|
* @param existing current file content, or null when the file does not exist yet
|
|
43
48
|
* @param body the `[mcp_servers.baychat]` block to install
|
|
44
49
|
*/
|
|
45
|
-
function mergeTomlConfig(existing, body) {
|
|
50
|
+
function mergeTomlConfig(existing, body, serverName = "baychat") {
|
|
46
51
|
const block = body.trimEnd();
|
|
47
52
|
if (existing === null || existing.trim() === "") {
|
|
48
53
|
return { content: `${block}\n`, action: "created" };
|
|
49
54
|
}
|
|
50
55
|
const lines = existing.split("\n");
|
|
56
|
+
const { start, end } = tomlSpan(lines, serverName === "baychat" ? TOML_TABLE : tomlTable(serverName));
|
|
57
|
+
if (start === -1) {
|
|
58
|
+
// Not present — append, keeping exactly one blank line as a separator so the
|
|
59
|
+
// result is readable whether or not the file ended with a newline.
|
|
60
|
+
const prefix = existing.endsWith("\n") ? existing.replace(/\n+$/, "\n") : `${existing}\n`;
|
|
61
|
+
return { content: `${prefix}\n${block}\n`, action: "appended" };
|
|
62
|
+
}
|
|
63
|
+
const before = lines.slice(0, start);
|
|
64
|
+
const after = lines.slice(end);
|
|
65
|
+
const merged = [...before, ...block.split("\n"), ...after].join("\n");
|
|
66
|
+
return { content: merged.endsWith("\n") ? merged : `${merged}\n`, action: "updated" };
|
|
67
|
+
}
|
|
68
|
+
/** The line span `[start, end)` of one server's table (and its sub-tables); start -1 = absent. */
|
|
69
|
+
function tomlSpan(lines, table) {
|
|
51
70
|
let start = -1;
|
|
52
71
|
let end = lines.length;
|
|
53
72
|
for (let i = 0; i < lines.length; i += 1) {
|
|
@@ -55,7 +74,7 @@ function mergeTomlConfig(existing, body) {
|
|
|
55
74
|
if (!match)
|
|
56
75
|
continue;
|
|
57
76
|
const key = (match[1] ?? "").replace(/\s+/g, "");
|
|
58
|
-
const ours = key ===
|
|
77
|
+
const ours = key === table || key.startsWith(`${table}.`);
|
|
59
78
|
if (start === -1) {
|
|
60
79
|
if (ours)
|
|
61
80
|
start = i;
|
|
@@ -67,16 +86,24 @@ function mergeTomlConfig(existing, body) {
|
|
|
67
86
|
break;
|
|
68
87
|
}
|
|
69
88
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
return { start, end };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Remove one server's table (and sub-tables) from a TOML config, or `null` when it is not
|
|
93
|
+
* there. Used only for the `baychat-<bay>` entries this CLI wrote itself.
|
|
94
|
+
*/
|
|
95
|
+
function removeTomlServer(existing, serverName) {
|
|
96
|
+
const lines = existing.split("\n");
|
|
97
|
+
const { start, end } = tomlSpan(lines, tomlTable(serverName));
|
|
98
|
+
if (start === -1)
|
|
99
|
+
return null;
|
|
76
100
|
const before = lines.slice(0, start);
|
|
77
101
|
const after = lines.slice(end);
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
// Drop the blank separator the append left above it, so add+remove round-trips cleanly.
|
|
103
|
+
while (before.length > 0 && before[before.length - 1].trim() === "")
|
|
104
|
+
before.pop();
|
|
105
|
+
const merged = [...before, ...(after.length && before.length ? [""] : []), ...after].join("\n").replace(/\n+$/, "");
|
|
106
|
+
return merged === "" ? "" : `${merged}\n`;
|
|
80
107
|
}
|
|
81
108
|
/**
|
|
82
109
|
* Merge the BayChat server entry into a JSON MCP config (Cursor, Claude Desktop).
|
|
@@ -86,13 +113,13 @@ function mergeTomlConfig(existing, body) {
|
|
|
86
113
|
* including a file that is merely mid-edit. Refusing lets the CLI say which file
|
|
87
114
|
* to look at.
|
|
88
115
|
*/
|
|
89
|
-
function mergeJsonConfig(existing, body) {
|
|
116
|
+
function mergeJsonConfig(existing, body, serverName = "baychat") {
|
|
90
117
|
// `body` is ours, produced by buildClientConfig — a parse failure here is a bug
|
|
91
118
|
// in this package, not user input, so it is allowed to throw plainly.
|
|
92
119
|
const incoming = JSON.parse(body);
|
|
93
|
-
const entry = incoming.mcpServers?.
|
|
120
|
+
const entry = incoming.mcpServers?.[serverName];
|
|
94
121
|
if (entry === undefined) {
|
|
95
|
-
throw new Error(
|
|
122
|
+
throw new Error(`Generated config has no mcpServers.${serverName} entry — this is a bug.`);
|
|
96
123
|
}
|
|
97
124
|
if (existing === null || existing.trim() === "") {
|
|
98
125
|
return { content: `${JSON.stringify(incoming, null, 2)}\n`, action: "created" };
|
|
@@ -109,19 +136,40 @@ function mergeJsonConfig(existing, body) {
|
|
|
109
136
|
}
|
|
110
137
|
const current = parsed;
|
|
111
138
|
const servers = { ...(current.mcpServers ?? {}) };
|
|
112
|
-
const had = Object.prototype.hasOwnProperty.call(servers,
|
|
113
|
-
servers
|
|
139
|
+
const had = Object.prototype.hasOwnProperty.call(servers, serverName);
|
|
140
|
+
servers[serverName] = entry;
|
|
114
141
|
const merged = { ...current, mcpServers: servers };
|
|
115
142
|
return {
|
|
116
143
|
content: `${JSON.stringify(merged, null, 2)}\n`,
|
|
117
144
|
action: had ? "updated" : "appended",
|
|
118
145
|
};
|
|
119
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Remove one server entry from a JSON MCP config, or `null` when it is not there (or the file
|
|
149
|
+
* is not a JSON object — which is left alone, exactly as the merge leaves it alone).
|
|
150
|
+
*/
|
|
151
|
+
function removeJsonServer(existing, serverName) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(existing);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
160
|
+
return null;
|
|
161
|
+
const current = parsed;
|
|
162
|
+
if (!current.mcpServers || !Object.prototype.hasOwnProperty.call(current.mcpServers, serverName))
|
|
163
|
+
return null;
|
|
164
|
+
const servers = { ...current.mcpServers };
|
|
165
|
+
delete servers[serverName];
|
|
166
|
+
return `${JSON.stringify({ ...current, mcpServers: servers }, null, 2)}\n`;
|
|
167
|
+
}
|
|
120
168
|
/** Merge by dialect. Keeps the format decision in one place. */
|
|
121
|
-
function mergeClientConfig(config, existing) {
|
|
169
|
+
function mergeClientConfig(config, existing, serverName = "baychat") {
|
|
122
170
|
return config.format === "toml"
|
|
123
|
-
? mergeTomlConfig(existing, config.body)
|
|
124
|
-
: mergeJsonConfig(existing, config.body);
|
|
171
|
+
? mergeTomlConfig(existing, config.body, serverName)
|
|
172
|
+
: mergeJsonConfig(existing, config.body, serverName);
|
|
125
173
|
}
|
|
126
174
|
/**
|
|
127
175
|
* Where to copy a file before rewriting it.
|
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
|
-
}, {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
package/dist/connect-claude.js
CHANGED
|
@@ -3,7 +3,12 @@ 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");
|
|
9
|
+
const bay_mcp_2 = require("./bay-mcp");
|
|
10
|
+
const context_hooks_command_1 = require("./context-hooks-command");
|
|
11
|
+
const launch_1 = require("./channel/launch");
|
|
7
12
|
/** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
|
|
8
13
|
async function cmdConnectClaude(opts = {}) {
|
|
9
14
|
try {
|
|
@@ -32,11 +37,16 @@ async function cmdConnectClaude(opts = {}) {
|
|
|
32
37
|
if (!device)
|
|
33
38
|
throw new Error("No device login was saved. Retry baychat connect claude.");
|
|
34
39
|
const registered = (0, commands_1.registerWithClaude)(device.baseUrl, device.token);
|
|
40
|
+
// The other Bays this login covers, as `baychat-<bay>` (none for a one-Bay login). Passing
|
|
41
|
+
// the current set as "previous" too removes only entries that no longer belong — e.g. the
|
|
42
|
+
// Bay that is now the default.
|
|
43
|
+
const bays = (0, config_1.loadBayLogins)();
|
|
44
|
+
(0, bay_mcp_1.applyBayEntries)(device.baseUrl, bays, bays, { claude: true, io: commands_1.realConfigIo, env: (0, client_paths_1.currentPathEnv)() });
|
|
35
45
|
for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
|
|
36
46
|
console.log(" " + line);
|
|
37
47
|
if (!registered)
|
|
38
48
|
return 1;
|
|
39
|
-
|
|
49
|
+
reportChannel(registerChannel());
|
|
40
50
|
console.log(' Restart Claude Code once, then use /baychat <name> "<group>".');
|
|
41
51
|
return 0;
|
|
42
52
|
}
|
|
@@ -46,3 +56,32 @@ async function cmdConnectClaude(opts = {}) {
|
|
|
46
56
|
return 1;
|
|
47
57
|
}
|
|
48
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Register the wake-up channel (docs/features/CLAUDE_CODE_CHANNEL.md). Best-effort: without it
|
|
61
|
+
* a session still listens through a Monitor, so a failure here is reported, never fatal.
|
|
62
|
+
*/
|
|
63
|
+
function registerChannel() {
|
|
64
|
+
try {
|
|
65
|
+
const launch = (0, context_hooks_command_1.currentLaunch)();
|
|
66
|
+
const problem = (0, context_hooks_command_1.launchProblem)(launch);
|
|
67
|
+
if (problem)
|
|
68
|
+
return { skipped: problem };
|
|
69
|
+
return (0, launch_1.registerChannelWithClaude)(launch, (0, bay_mcp_2.realClaudeRun)())
|
|
70
|
+
? "registered"
|
|
71
|
+
: { skipped: "Claude Code did not accept the channel server." };
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return { skipped: err instanceof Error ? err.message : String(err) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function reportChannel(result) {
|
|
78
|
+
if (result === "registered") {
|
|
79
|
+
console.log("✓ BayChat channel added to Claude Code (wakes a session with no Monitor deadline)");
|
|
80
|
+
console.log(" Start sessions with `baychat start`, or type:");
|
|
81
|
+
console.log(` ${launch_1.CHANNEL_LAUNCH_COMMAND}`);
|
|
82
|
+
console.log(" Without it, incoming messages use the native WebSocket Monitor as before.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.log(` BayChat channel not added: ${result.skipped}`);
|
|
86
|
+
console.log(" Incoming messages use native WebSocket Monitor; the session will verify its connection after joining.");
|
|
87
|
+
}
|
package/dist/device-login.js
CHANGED
|
@@ -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
|
-
|
|
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() {
|
package/dist/doctor-command.js
CHANGED
|
@@ -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,
|