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
package/README.md
CHANGED
|
@@ -50,6 +50,24 @@ setup runs inside the session; device approval or a client reload may still need
|
|
|
50
50
|
you. For a first installation without the skill, `baychat connect codex` or
|
|
51
51
|
`baychat connect claude` installs it once. Updating npm does not deploy remote MCP.
|
|
52
52
|
|
|
53
|
+
### Several Bays on one computer
|
|
54
|
+
|
|
55
|
+
If you own an Enterprise Bay with Team Bays, one approval of `baychat login` signs this
|
|
56
|
+
computer in to **every** one of them: your Personal Bay and each Team Bay you are in.
|
|
57
|
+
(Free and Pro, and people invited into a Team Bay, get the one Bay they approved in — as
|
|
58
|
+
before.) Each Bay is its own connection, and a session uses exactly one:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
/baychat Scout "Coding" # the default Bay (your Personal Bay unless you chose another)
|
|
62
|
+
/baychat Scout "Coding" --bay Harbor # the Team Bay "Harbor" — its own MCP entry, baychat-harbor
|
|
63
|
+
baychat bays # list this computer's Bay logins
|
|
64
|
+
baychat bays default Harbor # make Harbor the default instead
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Claude Code and every connected client get `baychat` (the default Bay) and one
|
|
68
|
+
`baychat-<bay>` entry per other Bay. With one Bay nothing changes: only `baychat`.
|
|
69
|
+
The relay runs one connection per Bay, so the same session name in two Bays never collides.
|
|
70
|
+
|
|
53
71
|
## What the package and MCP each do
|
|
54
72
|
|
|
55
73
|
The npm package contains the CLI, installed runtime commands, local relay,
|
|
@@ -93,6 +111,13 @@ not reconnect. Claude Code needs a host that exposes native WebSocket Monitor
|
|
|
93
111
|
the old shell/relay join is available only as an explicitly chosen fallback.
|
|
94
112
|
Updating npm alone does not refresh a skill already loaded in a running session.
|
|
95
113
|
|
|
114
|
+
**No Monitor deadline with the channel.** `baychat connect claude` also registers
|
|
115
|
+
`baychat_channel`, a local Claude Code channel. Start Claude Code with
|
|
116
|
+
`baychat start` (or `claude --dangerously-load-development-channels server:baychat_channel`)
|
|
117
|
+
and the skill calls its `listen` tool instead of opening a Monitor: the channel holds the
|
|
118
|
+
feed, reconnects by itself, and wakes the session only when a message arrives. Started
|
|
119
|
+
without the flag, `listen` answers `inactive` and the Monitor path above is used.
|
|
120
|
+
|
|
96
121
|
The Windows relay runs without a visible console, including after login and
|
|
97
122
|
network reconnects. Incoming messages appear as `@Sender` in the coding task.
|
|
98
123
|
The skill echoes BayChat chat replies locally; raw tool output stays local.
|
|
@@ -273,6 +298,7 @@ one where remembering to re-arm falls to the agent.
|
|
|
273
298
|
| Command | Description |
|
|
274
299
|
|---------|-------------|
|
|
275
300
|
| `baychat login [--start \| --finish \| --token <PAT>] [--base <url>]` | Log this computer in. Agents use `--start` to show an approval link immediately and `--finish` once approved; interactive login displays a QR and waits |
|
|
301
|
+
| `baychat bays [default <name>]` | List this computer's Bay logins (one approval signs an Enterprise owner in to the Personal Bay and its Team Bays), or choose the default Bay — the one a session joins when it names none |
|
|
276
302
|
| `baychat onboard [<conv>]` | **Run first.** Print the agent protocol + your live identity, conversations, and (a) room's context |
|
|
277
303
|
| `baychat pair <code> [--base <url>]` | Redeem a pairing code and store credentials |
|
|
278
304
|
| `baychat hermes init [--home <dir>] [--enable]` | Install the BayChat platform plugin into a Hermes agent (`~/.hermes`) and put your paired token in the `.env` its adapter reads. `--enable` also runs the two `hermes` commands that switch it on |
|
package/dist/bay-mcp.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Writing the per-Bay MCP entries (docs/features/MULTI_BAY_DEVICE_LOGIN.md).
|
|
3
|
+
//
|
|
4
|
+
// The default Bay stays the `baychat` server every client already has — written exactly as
|
|
5
|
+
// before by the code that always wrote it. THIS file adds the OTHER Bays as `baychat-<bay>`
|
|
6
|
+
// beside it, and removes ones it wrote earlier that are no longer wanted (a Bay that went
|
|
7
|
+
// away, or the Bay that just became the default).
|
|
8
|
+
//
|
|
9
|
+
// Two invariants:
|
|
10
|
+
// 1. A one-Bay computer is untouched. `extraBayEntries` is empty below two Bays, and with
|
|
11
|
+
// nothing stale this writes nothing and spawns nothing.
|
|
12
|
+
// 2. REFRESH IS NOT CONNECT (credential-refresh.ts): a client config file is only edited
|
|
13
|
+
// when it already names BayChat. Writing into a client the person never connected would
|
|
14
|
+
// configure software they did not ask us to touch.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.renameServerInConfig = renameServerInConfig;
|
|
17
|
+
exports.syncBayClientConfigs = syncBayClientConfigs;
|
|
18
|
+
exports.claudeBayAddArgs = claudeBayAddArgs;
|
|
19
|
+
exports.realClaudeRun = realClaudeRun;
|
|
20
|
+
exports.syncBayEntriesWithClaude = syncBayEntriesWithClaude;
|
|
21
|
+
exports.applyBayEntries = applyBayEntries;
|
|
22
|
+
const node_child_process_1 = require("node:child_process");
|
|
23
|
+
const client_config_writer_1 = require("./client-config-writer");
|
|
24
|
+
const client_paths_1 = require("./client-paths");
|
|
25
|
+
const mcp_dialects_1 = require("./mcp-dialects");
|
|
26
|
+
const bays_1 = require("./bays");
|
|
27
|
+
/**
|
|
28
|
+
* The same client config, under another server name. `buildClientConfig` (shared verbatim
|
|
29
|
+
* with the mobile app, so not edited here) always names the server `baychat`; a per-Bay entry
|
|
30
|
+
* is that config with the name swapped and nothing else changed.
|
|
31
|
+
*/
|
|
32
|
+
function renameServerInConfig(config, name) {
|
|
33
|
+
if (config.format === "toml") {
|
|
34
|
+
const body = config.body.replace(new RegExp(`^(\\s*\\[\\s*mcp_servers\\.)${bays_1.DEFAULT_MCP_ENTRY}(?=\\s*[\\].])`, "gm"), `$1${name}`);
|
|
35
|
+
return { ...config, body };
|
|
36
|
+
}
|
|
37
|
+
const parsed = JSON.parse(config.body);
|
|
38
|
+
const entry = parsed.mcpServers[bays_1.DEFAULT_MCP_ENTRY];
|
|
39
|
+
return { ...config, body: JSON.stringify({ ...parsed, mcpServers: { [name]: entry } }, null, 2) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Bring every ALREADY-CONNECTED client config file onto the wanted per-Bay entries.
|
|
43
|
+
*
|
|
44
|
+
* @returns the paths actually changed, for the caller to report.
|
|
45
|
+
*/
|
|
46
|
+
function syncBayClientConfigs(url, extras, stale, io, env) {
|
|
47
|
+
if (extras.length === 0 && stale.length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const changed = [];
|
|
50
|
+
for (const client of mcp_dialects_1.MCP_CLIENTS) {
|
|
51
|
+
for (const path of (0, client_paths_1.inspectableConfigPaths)(client, env)) {
|
|
52
|
+
const existing = io.readFile(path);
|
|
53
|
+
if (existing === null || !existing.includes(bays_1.DEFAULT_MCP_ENTRY))
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const next = syncOneFile(client, existing, url, extras, stale);
|
|
57
|
+
if (next === existing)
|
|
58
|
+
continue;
|
|
59
|
+
io.copyFile(path, (0, client_config_writer_1.backupPathFor)(path));
|
|
60
|
+
io.writeFile(path, next);
|
|
61
|
+
changed.push(path);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// One unreadable config must not stop the others; `doctor` names it. Same rule as the
|
|
65
|
+
// default entry's refresh in credential-refresh.ts.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return changed;
|
|
70
|
+
}
|
|
71
|
+
function syncOneFile(client, existing, url, extras, stale) {
|
|
72
|
+
let content = existing;
|
|
73
|
+
for (const name of stale) {
|
|
74
|
+
const removed = client === "codex" ? (0, client_config_writer_1.removeTomlServer)(content, name) : (0, client_config_writer_1.removeJsonServer)(content, name);
|
|
75
|
+
if (removed !== null)
|
|
76
|
+
content = removed;
|
|
77
|
+
}
|
|
78
|
+
for (const extra of extras) {
|
|
79
|
+
const config = renameServerInConfig((0, mcp_dialects_1.buildClientConfig)(client, { url, token: extra.bay.token }), extra.name);
|
|
80
|
+
content = (0, client_config_writer_1.mergeClientConfig)(config, content, extra.name).content;
|
|
81
|
+
}
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
84
|
+
/** `claude mcp …` argv for one named server. The token rides argv exactly as for `baychat`. */
|
|
85
|
+
function claudeBayAddArgs(name, baseUrl, token) {
|
|
86
|
+
return [
|
|
87
|
+
"mcp",
|
|
88
|
+
"add",
|
|
89
|
+
"--transport",
|
|
90
|
+
"http",
|
|
91
|
+
"--scope",
|
|
92
|
+
"user",
|
|
93
|
+
name,
|
|
94
|
+
`${baseUrl}/api/mcp`,
|
|
95
|
+
"--header",
|
|
96
|
+
`Authorization: Bearer ${token}`,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
/** Characters cmd.exe quoting cannot hold (commands.ts `WINDOWS_UNSAFE`). */
|
|
100
|
+
const WINDOWS_UNSAFE = /["%\u0000-\u001f\u007f]/;
|
|
101
|
+
/** Run `claude <args>` the way `registerWithClaude` does, or null when it cannot run safely. */
|
|
102
|
+
function realClaudeRun(platform = process.platform) {
|
|
103
|
+
return (args) => {
|
|
104
|
+
const shell = platform === "win32";
|
|
105
|
+
if (shell && args.some((a) => WINDOWS_UNSAFE.test(a))) {
|
|
106
|
+
return { status: null, stderr: "", error: new Error("unsafe argument for cmd.exe") };
|
|
107
|
+
}
|
|
108
|
+
const res = (0, node_child_process_1.spawnSync)("claude", shell ? args.map((a) => `"${a}"`) : args, {
|
|
109
|
+
encoding: "utf8",
|
|
110
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
111
|
+
timeout: 15_000,
|
|
112
|
+
killSignal: "SIGKILL",
|
|
113
|
+
shell,
|
|
114
|
+
});
|
|
115
|
+
return { status: res.status, stderr: String(res.stderr ?? ""), ...(res.error ? { error: res.error } : {}) };
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Register each extra Bay with Claude Code as `baychat-<bay>`, and remove stale ones.
|
|
120
|
+
*
|
|
121
|
+
* A renewal re-adds: Claude refuses `add` for an existing name, so "already exists" is
|
|
122
|
+
* answered by remove + add — the same rule `registerWithClaude` follows for `baychat`.
|
|
123
|
+
*
|
|
124
|
+
* @returns names added and names that could not be — never throws (the login is saved).
|
|
125
|
+
*/
|
|
126
|
+
function syncBayEntriesWithClaude(baseUrl, extras, stale, run = realClaudeRun()) {
|
|
127
|
+
const out = { added: [], removed: [], failed: [] };
|
|
128
|
+
for (const name of stale) {
|
|
129
|
+
const res = run(["mcp", "remove", name]);
|
|
130
|
+
if (res.status === 0)
|
|
131
|
+
out.removed.push(name);
|
|
132
|
+
// Absent is the common case and not a failure: there was nothing to remove.
|
|
133
|
+
}
|
|
134
|
+
for (const extra of extras) {
|
|
135
|
+
const args = claudeBayAddArgs(extra.name, baseUrl, extra.bay.token);
|
|
136
|
+
let res = run(args);
|
|
137
|
+
if (res.status !== 0 && /already exists/i.test(res.stderr)) {
|
|
138
|
+
const removed = run(["mcp", "remove", extra.name]);
|
|
139
|
+
if (removed.status === 0)
|
|
140
|
+
res = run(args);
|
|
141
|
+
}
|
|
142
|
+
if (res.status === 0)
|
|
143
|
+
out.added.push(extra.name);
|
|
144
|
+
else
|
|
145
|
+
out.failed.push(extra.name);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* After a login or a default switch: write the per-Bay entries everywhere this computer is
|
|
151
|
+
* already connected, and say what changed. Best-effort and always reported — the login itself
|
|
152
|
+
* is already saved, and a person told their login failed would redo it into the same place.
|
|
153
|
+
*/
|
|
154
|
+
function applyBayEntries(baseUrl, previous, current, opts) {
|
|
155
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
156
|
+
const extras = (0, bays_1.extraBayEntries)(current);
|
|
157
|
+
const stale = (0, bays_1.staleBayEntryNames)(previous, current);
|
|
158
|
+
if (extras.length === 0 && stale.length === 0)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
for (const path of syncBayClientConfigs(`${baseUrl}/api/mcp`, extras, stale, opts.io, opts.env)) {
|
|
162
|
+
log(`✓ Updated ${path} with your other Bays`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
log(` Could not update client configs for your other Bays: ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
}
|
|
168
|
+
if (!opts.claude)
|
|
169
|
+
return;
|
|
170
|
+
const result = syncBayEntriesWithClaude(baseUrl, extras, stale, opts.run);
|
|
171
|
+
for (const name of result.added)
|
|
172
|
+
log(`✓ BayChat ${name} added to Claude Code`);
|
|
173
|
+
for (const name of result.failed) {
|
|
174
|
+
log(` Could not add ${name} to Claude Code — run \`baychat login\` again, or add it by hand:`);
|
|
175
|
+
log(` claude mcp add --transport http --scope user ${name} ${baseUrl}/api/mcp --header "Authorization: Bearer <token>"`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// `baychat bays` — the Bay logins on this computer, and which one is the default
|
|
3
|
+
// (docs/features/MULTI_BAY_DEVICE_LOGIN.md).
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.BAYS_USAGE = void 0;
|
|
6
|
+
exports.cmdBays = cmdBays;
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const bays_1 = require("./bays");
|
|
9
|
+
const bay_mcp_1 = require("./bay-mcp");
|
|
10
|
+
const commands_1 = require("./commands");
|
|
11
|
+
const client_paths_1 = require("./client-paths");
|
|
12
|
+
exports.BAYS_USAGE = `Usage: baychat bays List the Bay logins on this computer
|
|
13
|
+
baychat bays default <name> Make <name> the default Bay (a session that names
|
|
14
|
+
no Bay joins it; older clients use it)`;
|
|
15
|
+
/**
|
|
16
|
+
* `baychat bays [default <name>]`.
|
|
17
|
+
*
|
|
18
|
+
* Switching the default is LOCAL: no server call, no new credential. It moves which stored
|
|
19
|
+
* token `device` holds, rewrites the `baychat` MCP entry to that Bay and the `baychat-<bay>`
|
|
20
|
+
* entries to the others. Every token still opens exactly its own Bay.
|
|
21
|
+
*/
|
|
22
|
+
function cmdBays(args) {
|
|
23
|
+
const [sub, ...rest] = args;
|
|
24
|
+
if (sub === undefined) {
|
|
25
|
+
for (const line of (0, bays_1.renderBayList)((0, config_1.loadBayLogins)(), (0, config_1.loadDeviceCredentials)()))
|
|
26
|
+
console.log(line);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (sub !== "default")
|
|
30
|
+
throw new Error(exports.BAYS_USAGE);
|
|
31
|
+
const name = rest.join(" ").trim();
|
|
32
|
+
if (!name)
|
|
33
|
+
throw new Error(exports.BAYS_USAGE);
|
|
34
|
+
const device = (0, config_1.loadDeviceCredentials)();
|
|
35
|
+
const bays = (0, config_1.loadBayLogins)();
|
|
36
|
+
if (!device)
|
|
37
|
+
throw new Error("This computer is not logged in to BayChat — run `baychat login`.");
|
|
38
|
+
if (bays.length < 2) {
|
|
39
|
+
throw new Error("This computer has one Bay login, so it is already the default. An Enterprise owner with Team Bays gets every Bay at `baychat login`.");
|
|
40
|
+
}
|
|
41
|
+
const target = (0, bays_1.findBayLogin)(bays, name);
|
|
42
|
+
if (target.isDefault) {
|
|
43
|
+
console.log(`${target.bayName} is already this computer's default Bay.`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
const next = (0, bays_1.withDefaultBay)(bays, device, target);
|
|
47
|
+
(0, config_1.saveDeviceCredentials)(next.device, next.bays);
|
|
48
|
+
console.log(`✓ ${target.bayName} is now this computer's default Bay.`);
|
|
49
|
+
// The default is the `baychat` entry: every connected client gets the new token there, and
|
|
50
|
+
// the Bay that WAS the default moves to its own `baychat-<bay>` entry.
|
|
51
|
+
(0, commands_1.refreshConnectedClients)(next.device.baseUrl, next.device.token);
|
|
52
|
+
(0, commands_1.registerWithClaude)(next.device.baseUrl, next.device.token);
|
|
53
|
+
(0, bay_mcp_1.applyBayEntries)(next.device.baseUrl, bays, next.bays, {
|
|
54
|
+
claude: true,
|
|
55
|
+
io: commands_1.realConfigIo,
|
|
56
|
+
env: (0, client_paths_1.currentPathEnv)(),
|
|
57
|
+
});
|
|
58
|
+
console.log(" Restart your agent clients (and `baychat relay stop && baychat relay start` if the relay runs) to pick it up.");
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
package/dist/bays.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// One computer, one login per Bay (docs/features/MULTI_BAY_DEVICE_LOGIN.md) — the CLI's half.
|
|
3
|
+
//
|
|
4
|
+
// One approval on the phone can sign this computer in to several Bays. The server mints one
|
|
5
|
+
// ORDINARY single-Bay credential per Bay; nothing about a token changed. What changes here is
|
|
6
|
+
// only how this computer keeps them and offers them:
|
|
7
|
+
//
|
|
8
|
+
// - credentials.json keeps `device` = the DEFAULT Bay (every older reader is unchanged) and
|
|
9
|
+
// the whole set under `bays` (config.ts).
|
|
10
|
+
// - Each MCP client gets the default Bay as the `baychat` server it always had, and every
|
|
11
|
+
// OTHER Bay as `baychat-<bay>` — written ONLY when there is more than one Bay, so a one-Bay
|
|
12
|
+
// user's client config is byte-identical to before.
|
|
13
|
+
// - A session picks its Bay at start: `--bay <name>`, or the default.
|
|
14
|
+
//
|
|
15
|
+
// This file is the PURE part — names, lookups, the default switch. No I/O.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.DEFAULT_MCP_ENTRY = void 0;
|
|
18
|
+
exports.baySlug = baySlug;
|
|
19
|
+
exports.bayEntryName = bayEntryName;
|
|
20
|
+
exports.extraBayEntries = extraBayEntries;
|
|
21
|
+
exports.staleBayEntryNames = staleBayEntryNames;
|
|
22
|
+
exports.findBayLogin = findBayLogin;
|
|
23
|
+
exports.withDefaultBay = withDefaultBay;
|
|
24
|
+
exports.renderBayList = renderBayList;
|
|
25
|
+
/** The MCP server name every client has always had: the default Bay. */
|
|
26
|
+
exports.DEFAULT_MCP_ENTRY = "baychat";
|
|
27
|
+
/** How a Bay name becomes part of an MCP server name: `[a-z0-9-]`, no edge dashes, short. */
|
|
28
|
+
function baySlug(name) {
|
|
29
|
+
const slug = name
|
|
30
|
+
.normalize("NFKD")
|
|
31
|
+
.replace(/[̀-ͯ]/g, "")
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
34
|
+
.replace(/^-+|-+$/g, "")
|
|
35
|
+
.slice(0, 32)
|
|
36
|
+
.replace(/-+$/g, "");
|
|
37
|
+
return slug || "bay";
|
|
38
|
+
}
|
|
39
|
+
/** A Bay id, reduced to what an MCP server name may carry. */
|
|
40
|
+
function idSuffix(bayId) {
|
|
41
|
+
return bayId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(-6) || "x";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The MCP server name for one Bay among the computer's Bays: `baychat-<slug>`.
|
|
45
|
+
*
|
|
46
|
+
* Stable for a given set of Bay names; when two of this person's Bays slug to the same text
|
|
47
|
+
* (two Bays called "Work"), BOTH get a short id suffix, so neither name depends on which one
|
|
48
|
+
* happened to be listed first. Claude Code exposes the tools as `mcp__baychat-<slug>__…`.
|
|
49
|
+
*/
|
|
50
|
+
function bayEntryName(bay, all) {
|
|
51
|
+
const slug = baySlug(bay.bayName);
|
|
52
|
+
const clash = all.some((other) => other.bayId !== bay.bayId && baySlug(other.bayName) === slug);
|
|
53
|
+
return `${exports.DEFAULT_MCP_ENTRY}-${clash ? `${slug}-${idSuffix(bay.bayId)}` : slug}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `baychat-<bay>` entries for the NON-default Bays — and none at all with fewer than two
|
|
57
|
+
* Bays, which is what keeps a one-Bay computer's client configs exactly as they were.
|
|
58
|
+
*/
|
|
59
|
+
function extraBayEntries(bays) {
|
|
60
|
+
if (bays.length < 2)
|
|
61
|
+
return [];
|
|
62
|
+
return bays.filter((b) => !b.isDefault).map((bay) => ({ name: bayEntryName(bay, bays), bay }));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Entry names THIS CLI wrote for the previous set of Bays and must now remove: the previous
|
|
66
|
+
* extras minus the ones still wanted (a Bay that went away, or the Bay that just became the
|
|
67
|
+
* default and is `baychat` now). Only ever names this CLI itself derived and wrote — never a
|
|
68
|
+
* server it did not write, never the default `baychat` entry — and none at all for a computer
|
|
69
|
+
* that had one Bay before, so a one-Bay login never even asks a client to remove anything.
|
|
70
|
+
*/
|
|
71
|
+
function staleBayEntryNames(previous, current) {
|
|
72
|
+
const wanted = new Set(extraBayEntries(current).map((e) => e.name));
|
|
73
|
+
return extraBayEntries(previous)
|
|
74
|
+
.map((e) => e.name)
|
|
75
|
+
.filter((n) => !wanted.has(n) && n !== exports.DEFAULT_MCP_ENTRY)
|
|
76
|
+
.sort();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Find a Bay login by what a person would type: its name (any case), its MCP entry name or
|
|
80
|
+
* slug, or its id. Ambiguity is an error, never a guess — the wrong Bay is the wrong room.
|
|
81
|
+
*
|
|
82
|
+
* @throws naming the Bays that exist, when nothing (or more than one) matches.
|
|
83
|
+
*/
|
|
84
|
+
function findBayLogin(bays, query) {
|
|
85
|
+
const q = query.trim();
|
|
86
|
+
const lower = q.toLowerCase();
|
|
87
|
+
const tiers = [
|
|
88
|
+
(b) => b.bayId === q,
|
|
89
|
+
(b) => b.bayName.trim().toLowerCase() === lower,
|
|
90
|
+
(b) => bayEntryName(b, bays) === lower || bayEntryName(b, bays) === `${exports.DEFAULT_MCP_ENTRY}-${lower}`,
|
|
91
|
+
(b) => baySlug(b.bayName) === baySlug(q),
|
|
92
|
+
];
|
|
93
|
+
for (const match of tiers) {
|
|
94
|
+
const found = bays.filter(match);
|
|
95
|
+
if (found.length === 1)
|
|
96
|
+
return found[0];
|
|
97
|
+
if (found.length > 1) {
|
|
98
|
+
throw new Error(`"${q}" matches more than one Bay (${found.map((b) => `"${b.bayName}"`).join(", ")}) — use the entry name from \`baychat bays\`.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const known = bays.map((b) => `"${b.bayName}"`).join(", ");
|
|
102
|
+
throw new Error(bays.length
|
|
103
|
+
? `No Bay login named "${q}" on this computer. Bays here: ${known}. Run \`baychat bays\`.`
|
|
104
|
+
: `This computer has one BayChat login and no other Bays — run \`baychat login\` to pick up more.`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Make `target` the default Bay: the set with `isDefault` moved, and the `device` record now
|
|
108
|
+
* holding the target's token. Pure — the caller saves both and rewrites the client configs.
|
|
109
|
+
*/
|
|
110
|
+
function withDefaultBay(bays, device, target) {
|
|
111
|
+
return {
|
|
112
|
+
bays: bays.map((b) => ({ ...b, isDefault: b.bayId === target.bayId })),
|
|
113
|
+
device: { ...device, token: target.token, expiresAt: target.expiresAt },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** `baychat bays` — one line per Bay login, the default marked. Pure, for testing. */
|
|
117
|
+
function renderBayList(bays, device, now = Date.now()) {
|
|
118
|
+
if (bays.length === 0) {
|
|
119
|
+
if (!device)
|
|
120
|
+
return ["This computer is not logged in to BayChat — run `baychat login`."];
|
|
121
|
+
return [
|
|
122
|
+
`One login on this computer (as ${device.user.name}), in the Bay it was approved in.`,
|
|
123
|
+
"An Enterprise owner with Team Bays gets every Bay here — run `baychat login` again after updating.",
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
const lines = ["Bay logins on this computer:"];
|
|
127
|
+
const width = Math.max(...bays.map((b) => b.bayName.length));
|
|
128
|
+
for (const bay of bays) {
|
|
129
|
+
const entry = bay.isDefault || bays.length < 2 ? exports.DEFAULT_MCP_ENTRY : bayEntryName(bay, bays);
|
|
130
|
+
lines.push(` ${bay.isDefault ? "*" : " "} ${bay.bayName.padEnd(width)} ${expiryText(bay.expiresAt, now)} MCP: ${entry}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("* = default Bay (a session that names no Bay joins it).");
|
|
133
|
+
if (bays.length > 1)
|
|
134
|
+
lines.push("Change it: baychat bays default <name>. Join another: /baychat <name> --bay <Bay>.");
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
function expiryText(expiresAt, now) {
|
|
138
|
+
const ms = Date.parse(expiresAt) - now;
|
|
139
|
+
if (Number.isNaN(ms))
|
|
140
|
+
return "expiry unknown";
|
|
141
|
+
if (ms <= 0)
|
|
142
|
+
return "EXPIRED — run baychat login";
|
|
143
|
+
const days = Math.floor(ms / 86_400_000);
|
|
144
|
+
return days >= 1 ? `expires in ${days} day(s)` : `expires in ${Math.max(1, Math.round(ms / 3_600_000))}h`;
|
|
145
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEV_CHANNELS_FLAG = exports.CHANNEL_FLAG_VALUE = exports.CHANNEL_MCP_ENTRY = void 0;
|
|
37
|
+
exports.argsEnableChannel = argsEnableChannel;
|
|
38
|
+
exports.realProcessReader = realProcessReader;
|
|
39
|
+
exports.detectChannel = detectChannel;
|
|
40
|
+
/**
|
|
41
|
+
* Was this Claude Code session started with the BayChat channel switched on?
|
|
42
|
+
*
|
|
43
|
+
* Claude Code never tells a channel server whether it registered it: without the launch flag
|
|
44
|
+
* the server still connects and its tools still work, but every event it pushes is dropped in
|
|
45
|
+
* silence (checked in the 2.1.284 source: the gate only logs). A session told "you are
|
|
46
|
+
* listening" when nothing can arrive is deaf with a green light — worse than the Monitor it
|
|
47
|
+
* replaced. So `listen` answers active only on evidence, and otherwise sends the session back
|
|
48
|
+
* to `listen_messages` + Monitor.
|
|
49
|
+
*
|
|
50
|
+
* The evidence is the command line Claude Code itself was started with: an ancestor process
|
|
51
|
+
* whose arguments carry `--dangerously-load-development-channels` or `--channels` naming
|
|
52
|
+
* `server:baychat_channel`. Claude Code spawns this server directly, so it is the parent; a few
|
|
53
|
+
* more levels are walked for launchers that wrap it. Where process arguments cannot be read
|
|
54
|
+
* (Windows), `BAYCHAT_CHANNEL=1` — set by `baychat start` on the process it launches with the
|
|
55
|
+
* flag — is accepted instead.
|
|
56
|
+
*/
|
|
57
|
+
const node_child_process_1 = require("node:child_process");
|
|
58
|
+
const fs = __importStar(require("node:fs"));
|
|
59
|
+
/** This server's name in Claude Code's MCP config. An underscore, so no Bay slug can equal it. */
|
|
60
|
+
exports.CHANNEL_MCP_ENTRY = "baychat_channel";
|
|
61
|
+
/** What `--dangerously-load-development-channels` takes for a bare MCP server. */
|
|
62
|
+
exports.CHANNEL_FLAG_VALUE = `server:${exports.CHANNEL_MCP_ENTRY}`;
|
|
63
|
+
exports.DEV_CHANNELS_FLAG = "--dangerously-load-development-channels";
|
|
64
|
+
const CHANNEL_FLAGS = [exports.DEV_CHANNELS_FLAG, "--channels"];
|
|
65
|
+
/** How far up the process tree to look. */
|
|
66
|
+
const MAX_DEPTH = 4;
|
|
67
|
+
/**
|
|
68
|
+
* True when one process's argument list switches the channel on: a channels flag followed —
|
|
69
|
+
* as its value, `=value`, or one of the space-separated entries after it — by our server.
|
|
70
|
+
*/
|
|
71
|
+
function argsEnableChannel(args) {
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
const arg = args[i];
|
|
74
|
+
const flag = CHANNEL_FLAGS.find((f) => arg === f || arg.startsWith(`${f}=`));
|
|
75
|
+
if (!flag)
|
|
76
|
+
continue;
|
|
77
|
+
const values = [];
|
|
78
|
+
if (arg.startsWith(`${flag}=`))
|
|
79
|
+
values.push(...arg.slice(flag.length + 1).split(/[\s,]+/));
|
|
80
|
+
else {
|
|
81
|
+
for (let j = i + 1; j < args.length && !args[j].startsWith("-"); j++)
|
|
82
|
+
values.push(...args[j].split(/[\s,]+/));
|
|
83
|
+
}
|
|
84
|
+
if (values.includes(exports.CHANNEL_FLAG_VALUE))
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/** Linux: /proc, exact argv. Elsewhere: `ps`, whose argument text is split on spaces. */
|
|
90
|
+
function realProcessReader(platform = process.platform) {
|
|
91
|
+
return (pid) => {
|
|
92
|
+
if (platform === "linux") {
|
|
93
|
+
try {
|
|
94
|
+
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
95
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
96
|
+
// Field 4 of stat, counted after the parenthesised command name (which may hold spaces).
|
|
97
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
98
|
+
return { ppid, args: cmdline.split("\0").filter(Boolean) };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (platform === "win32")
|
|
105
|
+
return undefined;
|
|
106
|
+
try {
|
|
107
|
+
const out = (0, node_child_process_1.execFileSync)("ps", ["-o", "ppid=", "-o", "args=", "-p", String(pid)], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
110
|
+
timeout: 3_000,
|
|
111
|
+
}).trim();
|
|
112
|
+
const m = /^(\d+)\s+(.*)$/s.exec(out);
|
|
113
|
+
if (!m)
|
|
114
|
+
return undefined;
|
|
115
|
+
return { ppid: Number(m[1]), args: m[2].split(/\s+/) };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Look for the flag on this process's ancestors, then fall back to the env marker. */
|
|
123
|
+
function detectChannel(opts = {}) {
|
|
124
|
+
const read = opts.read ?? realProcessReader();
|
|
125
|
+
const env = opts.env ?? process.env;
|
|
126
|
+
let pid = opts.pid ?? process.ppid;
|
|
127
|
+
let readable = false;
|
|
128
|
+
for (let depth = 0; depth < MAX_DEPTH && pid > 1; depth++) {
|
|
129
|
+
const info = read(pid);
|
|
130
|
+
if (!info)
|
|
131
|
+
break;
|
|
132
|
+
if (info.args) {
|
|
133
|
+
readable = true;
|
|
134
|
+
if (argsEnableChannel(info.args))
|
|
135
|
+
return { active: true, how: "process" };
|
|
136
|
+
}
|
|
137
|
+
pid = info.ppid;
|
|
138
|
+
}
|
|
139
|
+
// Only where nothing could be read: a readable tree without the flag is a real "no", and an
|
|
140
|
+
// inherited marker must not overrule it (a plain `claude` started inside a `baychat start`
|
|
141
|
+
// terminal would carry the marker without the flag).
|
|
142
|
+
if (!readable && env.BAYCHAT_CHANNEL === "1")
|
|
143
|
+
return { active: true, how: "env" };
|
|
144
|
+
return { active: false, how: "none" };
|
|
145
|
+
}
|