baychat 0.26.0 → 0.27.1

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 CHANGED
@@ -111,6 +111,13 @@ not reconnect. Claude Code needs a host that exposes native WebSocket Monitor
111
111
  the old shell/relay join is available only as an explicitly chosen fallback.
112
112
  Updating npm alone does not refresh a skill already loaded in a running session.
113
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 claude` (or `baychat start`, which uses tmux when installed)
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
+
114
121
  The Windows relay runs without a visible console, including after login and
115
122
  network reconnects. Incoming messages appear as `@Sender` in the coding task.
116
123
  The skill echoes BayChat chat replies locally; raw tool output stays local.
@@ -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
+ * Two kinds of evidence, either enough:
51
+ *
52
+ * 1. `BAYCHAT_CHANNEL=1` in this process's environment. `baychat claude` and `baychat start` set
53
+ * it on the Claude Code they launch WITH the flag, and Claude Code passes its environment to
54
+ * the servers it spawns (checked on 2.1.284). Set per launch, never exported, so a plain
55
+ * `claude` does not inherit it.
56
+ * 2. The flag on an ancestor's command line: `--dangerously-load-development-channels` or
57
+ * `--channels` naming `server:baychat_channel`, for people who type the flag themselves.
58
+ * NOT reliable on its own: Claude Code sets `process.title = "claude"` at start, and where it
59
+ * runs on Node (an npm install, e.g. under nvm on WSL) that overwrites /proc/<pid>/cmdline,
60
+ * so the flags vanish. Found 2026-10-02 on the owner's WSL: launched with the flag, `ps`
61
+ * showed only `claude`, and `listen` wrongly answered inactive. A wrong "inactive" is safe —
62
+ * the session falls back to a Monitor — but it is why (1) exists.
63
+ */
64
+ const node_child_process_1 = require("node:child_process");
65
+ const fs = __importStar(require("node:fs"));
66
+ /** This server's name in Claude Code's MCP config. An underscore, so no Bay slug can equal it. */
67
+ exports.CHANNEL_MCP_ENTRY = "baychat_channel";
68
+ /** What `--dangerously-load-development-channels` takes for a bare MCP server. */
69
+ exports.CHANNEL_FLAG_VALUE = `server:${exports.CHANNEL_MCP_ENTRY}`;
70
+ exports.DEV_CHANNELS_FLAG = "--dangerously-load-development-channels";
71
+ const CHANNEL_FLAGS = [exports.DEV_CHANNELS_FLAG, "--channels"];
72
+ /** How far up the process tree to look. */
73
+ const MAX_DEPTH = 4;
74
+ /**
75
+ * True when one process's argument list switches the channel on: a channels flag followed —
76
+ * as its value, `=value`, or one of the space-separated entries after it — by our server.
77
+ */
78
+ function argsEnableChannel(args) {
79
+ for (let i = 0; i < args.length; i++) {
80
+ const arg = args[i];
81
+ const flag = CHANNEL_FLAGS.find((f) => arg === f || arg.startsWith(`${f}=`));
82
+ if (!flag)
83
+ continue;
84
+ const values = [];
85
+ if (arg.startsWith(`${flag}=`))
86
+ values.push(...arg.slice(flag.length + 1).split(/[\s,]+/));
87
+ else {
88
+ for (let j = i + 1; j < args.length && !args[j].startsWith("-"); j++)
89
+ values.push(...args[j].split(/[\s,]+/));
90
+ }
91
+ if (values.includes(exports.CHANNEL_FLAG_VALUE))
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+ /** Linux: /proc, exact argv. Elsewhere: `ps`, whose argument text is split on spaces. */
97
+ function realProcessReader(platform = process.platform) {
98
+ return (pid) => {
99
+ if (platform === "linux") {
100
+ try {
101
+ const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
102
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
103
+ // Field 4 of stat, counted after the parenthesised command name (which may hold spaces).
104
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
105
+ return { ppid, args: cmdline.split("\0").filter(Boolean) };
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ if (platform === "win32")
112
+ return undefined;
113
+ try {
114
+ const out = (0, node_child_process_1.execFileSync)("ps", ["-o", "ppid=", "-o", "args=", "-p", String(pid)], {
115
+ encoding: "utf8",
116
+ stdio: ["ignore", "pipe", "ignore"],
117
+ timeout: 3_000,
118
+ }).trim();
119
+ const m = /^(\d+)\s+(.*)$/s.exec(out);
120
+ if (!m)
121
+ return undefined;
122
+ return { ppid: Number(m[1]), args: m[2].split(/\s+/) };
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ };
128
+ }
129
+ /** Look for the flag on this process's ancestors, then fall back to the env marker. */
130
+ function detectChannel(opts = {}) {
131
+ const env = opts.env ?? process.env;
132
+ if (env.BAYCHAT_CHANNEL === "1")
133
+ return { active: true, how: "env" };
134
+ const read = opts.read ?? realProcessReader();
135
+ let pid = opts.pid ?? process.ppid;
136
+ for (let depth = 0; depth < MAX_DEPTH && pid > 1; depth++) {
137
+ const info = read(pid);
138
+ if (!info)
139
+ break;
140
+ if (info.args && argsEnableChannel(info.args))
141
+ return { active: true, how: "process" };
142
+ pid = info.ppid;
143
+ }
144
+ return { active: false, how: "none" };
145
+ }
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChannelFeed = exports.BACKOFF_MS = exports.CLOSE_STOPPED = void 0;
4
+ /** Close code the server sends when a feed was stopped or replaced on purpose. */
5
+ exports.CLOSE_STOPPED = 4000;
6
+ /** Seconds between reconnect attempts: quick at first, then no more than once a minute. */
7
+ exports.BACKOFF_MS = [1_000, 2_000, 5_000, 10_000, 30_000, 60_000];
8
+ const READY_TIMEOUT_MS = 10_000;
9
+ const WATCHDOG_MS = 2 * 60_000;
10
+ /** A timer that fires this much later than asked means the computer slept: reconnect at once. */
11
+ const SLEEP_GAP_MS = 60_000;
12
+ class ChannelFeed {
13
+ deps;
14
+ state = "connecting";
15
+ cursor;
16
+ socket;
17
+ /** Bumped for each socket, so a late callback from an old one cannot touch the new one. */
18
+ generation = 0;
19
+ attempt = 0;
20
+ retryTimer;
21
+ watchdogTimer;
22
+ lastTick = 0;
23
+ now;
24
+ setTimer;
25
+ clearTimer;
26
+ constructor(deps) {
27
+ this.deps = deps;
28
+ this.now = deps.now ?? Date.now;
29
+ this.setTimer = deps.setTimer ?? ((fn, ms) => {
30
+ const t = setTimeout(fn, ms);
31
+ // The stdio transport keeps the process alive; a timer must not on its own.
32
+ t.unref?.();
33
+ return t;
34
+ });
35
+ this.clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
36
+ }
37
+ get currentState() {
38
+ return this.state;
39
+ }
40
+ get lastCursor() {
41
+ return this.cursor;
42
+ }
43
+ /**
44
+ * Open the feed and wait for its first `ready`.
45
+ *
46
+ * Resolves `{ ok: true }` once the server has seated the feed — the only moment "connected"
47
+ * is true — or with the reason it could not. A failed start leaves nothing running, so the
48
+ * session can fall back to a Monitor with nothing competing for its feed.
49
+ */
50
+ async start() {
51
+ const minted = await this.deps.mint(undefined);
52
+ if (!minted.ok) {
53
+ this.state = "stopped";
54
+ return { ok: false, detail: minted.detail };
55
+ }
56
+ const ready = await new Promise((resolve) => {
57
+ const timer = this.setTimer(() => {
58
+ this.teardown();
59
+ this.state = "stopped";
60
+ resolve({ ok: false, detail: "The BayChat feed did not answer within 10 seconds." });
61
+ }, this.deps.readyTimeoutMs ?? READY_TIMEOUT_MS);
62
+ this.open(minted.ticket, {
63
+ onReady: () => {
64
+ this.clearTimer(timer);
65
+ resolve({ ok: true });
66
+ },
67
+ onFailBeforeReady: (detail) => {
68
+ this.clearTimer(timer);
69
+ this.teardown();
70
+ this.state = "stopped";
71
+ resolve({ ok: false, detail });
72
+ },
73
+ });
74
+ });
75
+ if (ready.ok)
76
+ this.armWatchdog();
77
+ return ready;
78
+ }
79
+ /** Stop for good: no reconnect, no watchdog. Used on replace and on process exit. */
80
+ stop() {
81
+ this.state = "stopped";
82
+ this.teardown();
83
+ }
84
+ teardown() {
85
+ this.generation++;
86
+ if (this.retryTimer !== undefined)
87
+ this.clearTimer(this.retryTimer);
88
+ if (this.watchdogTimer !== undefined)
89
+ this.clearTimer(this.watchdogTimer);
90
+ this.retryTimer = undefined;
91
+ this.watchdogTimer = undefined;
92
+ const socket = this.socket;
93
+ this.socket = undefined;
94
+ try {
95
+ socket?.close(1000, "BayChat channel closed");
96
+ }
97
+ catch {
98
+ /* Already closed: nothing left to release. */
99
+ }
100
+ }
101
+ open(ticket, first) {
102
+ const generation = ++this.generation;
103
+ const current = () => generation === this.generation && this.state !== "stopped";
104
+ let ready = false;
105
+ this.state = "connecting";
106
+ try {
107
+ this.socket = this.deps.connect(ticket.url, ticket.protocols, {
108
+ onOpen: () => { },
109
+ onMessage: (data) => {
110
+ if (!current())
111
+ return;
112
+ const frame = parseFrame(data);
113
+ if (!frame)
114
+ return;
115
+ if (typeof frame.cursor === "string" && frame.cursor)
116
+ this.cursor = frame.cursor;
117
+ if (frame.type === "ready") {
118
+ ready = true;
119
+ this.state = "open";
120
+ this.attempt = 0;
121
+ first?.onReady();
122
+ return;
123
+ }
124
+ if (frame.type === "messages")
125
+ void this.pushMessages(frame);
126
+ else if (frame.type === "reset")
127
+ void this.pushReset();
128
+ },
129
+ onError: (detail) => this.log(`feed error: ${detail}`),
130
+ onClose: (code, reason) => {
131
+ if (!current())
132
+ return;
133
+ this.socket = undefined;
134
+ if (!ready && first) {
135
+ first.onFailBeforeReady(`The BayChat feed closed before it was ready (${code}${reason ? `: ${reason}` : ""}).`);
136
+ return;
137
+ }
138
+ if (code === exports.CLOSE_STOPPED) {
139
+ // Stopped or replaced on purpose — by stop_listening, end_session, or a newer
140
+ // listener for this session. Reconnecting would fight whichever replaced it.
141
+ this.log(`feed for ${this.deps.session} stopped by the server (${reason || code})`);
142
+ this.stop();
143
+ return;
144
+ }
145
+ this.scheduleReconnect(`closed ${code}${reason ? `: ${reason}` : ""}`);
146
+ },
147
+ });
148
+ }
149
+ catch (err) {
150
+ const detail = err instanceof Error ? err.message : String(err);
151
+ if (first)
152
+ first.onFailBeforeReady(detail);
153
+ else
154
+ this.scheduleReconnect(detail);
155
+ }
156
+ }
157
+ scheduleReconnect(why) {
158
+ if (this.state === "stopped")
159
+ return;
160
+ this.state = "retrying";
161
+ const delay = exports.BACKOFF_MS[Math.min(this.attempt, exports.BACKOFF_MS.length - 1)];
162
+ this.attempt++;
163
+ this.log(`feed for ${this.deps.session} ${why}; reconnecting in ${delay / 1000}s`);
164
+ if (this.retryTimer !== undefined)
165
+ this.clearTimer(this.retryTimer);
166
+ this.retryTimer = this.setTimer(() => {
167
+ this.retryTimer = undefined;
168
+ void this.reconnect();
169
+ }, delay);
170
+ }
171
+ async reconnect() {
172
+ if (this.state === "stopped")
173
+ return;
174
+ const minted = await this.deps.mint(this.cursor);
175
+ // `stop()` may have run while the mint was in flight.
176
+ if (this.stopped())
177
+ return;
178
+ if (minted.ok) {
179
+ this.open(minted.ticket);
180
+ return;
181
+ }
182
+ if (minted.fatal) {
183
+ this.stop();
184
+ await this.deps.push(`BayChat stopped listening for session "${this.deps.session}": ${minted.detail} ` +
185
+ `If the session expired, call join_session again with the same name and room, then call ` +
186
+ `the baychat_channel listen tool again. Until then this session does not hear the Bay.`, this.meta({ event: "stopped" }));
187
+ return;
188
+ }
189
+ this.scheduleReconnect(`could not reconnect (${minted.detail})`);
190
+ }
191
+ /**
192
+ * Every two minutes: ask the server whether it still sees this feed.
193
+ *
194
+ * A socket can die without a close — a laptop lid, a NAT that forgot us — and the server's
195
+ * pings do not reach this side's code to notice. The server's own answer is the check. A
196
+ * timer that fires far later than asked means the computer slept: reconnect at once rather
197
+ * than trust a socket from before the sleep.
198
+ */
199
+ armWatchdog() {
200
+ const every = this.deps.watchdogMs ?? WATCHDOG_MS;
201
+ this.lastTick = this.now();
202
+ this.watchdogTimer = this.setTimer(() => void this.watchdog(every), every);
203
+ }
204
+ async watchdog(every) {
205
+ this.watchdogTimer = undefined;
206
+ if (this.state === "stopped")
207
+ return;
208
+ const late = this.now() - this.lastTick - every > SLEEP_GAP_MS;
209
+ if (this.state === "open") {
210
+ const seen = late ? "disconnected" : await this.deps.status();
211
+ if (this.state === "open" && seen === "disconnected") {
212
+ this.log(`feed for ${this.deps.session} ${late ? "outlived a sleep" : "is not seen by the server"}; reopening`);
213
+ this.generation++;
214
+ const socket = this.socket;
215
+ this.socket = undefined;
216
+ try {
217
+ socket?.close(1000, "Reopening");
218
+ }
219
+ catch {
220
+ /* Already gone. */
221
+ }
222
+ this.attempt = 0;
223
+ this.scheduleReconnect("watchdog");
224
+ }
225
+ }
226
+ else if (late && this.state === "retrying" && this.retryTimer !== undefined) {
227
+ // Woken from sleep mid-backoff: try now instead of after the rest of a long wait.
228
+ this.clearTimer(this.retryTimer);
229
+ this.retryTimer = undefined;
230
+ this.attempt = 0;
231
+ void this.reconnect();
232
+ }
233
+ if (!this.stopped())
234
+ this.armWatchdog();
235
+ }
236
+ async pushMessages(frame) {
237
+ const items = (frame.messages ?? []).filter((m) => typeof m?.conversationId === "string" && typeof m?.messageId === "string");
238
+ if (!items.length)
239
+ return;
240
+ const conversations = [...new Set(items.map((m) => m.conversationId))];
241
+ const count = items.length;
242
+ await this.deps.push(`${count === 1 ? "A new BayChat message" : `${count} new BayChat messages`} for session "${this.deps.session}"` +
243
+ `${this.deps.bayName ? ` in the ${this.deps.bayName} Bay` : ""}. ` +
244
+ `Read ${conversations.length === 1 ? "conversation" : "conversations"} ${conversations.join(", ")} with get_messages ` +
245
+ `from your last watermark, obey shouldRespond, and answer in BayChat. This channel stays open: do not open a Monitor.`, this.meta({
246
+ event: "messages",
247
+ conversation_ids: conversations.join(","),
248
+ message_ids: items.map((m) => m.messageId).join(","),
249
+ }));
250
+ }
251
+ async pushReset() {
252
+ await this.deps.push(`BayChat reset the feed for session "${this.deps.session}". Catch up on your joined conversations with get_messages; ` +
253
+ `a reset never means there were no messages. The channel stays open.`, this.meta({ event: "reset" }));
254
+ }
255
+ meta(extra) {
256
+ return {
257
+ session: this.deps.session,
258
+ ...(this.deps.bayName ? { bay: this.deps.bayName } : {}),
259
+ ...extra,
260
+ };
261
+ }
262
+ /** Read through a method: an `await` in between can change it, which narrowing cannot see. */
263
+ stopped() {
264
+ return this.state === "stopped";
265
+ }
266
+ log(line) {
267
+ this.deps.log?.(line);
268
+ }
269
+ }
270
+ exports.ChannelFeed = ChannelFeed;
271
+ function parseFrame(data) {
272
+ try {
273
+ const parsed = JSON.parse(data);
274
+ if (parsed && typeof parsed === "object" && typeof parsed.type === "string")
275
+ return parsed;
276
+ }
277
+ catch {
278
+ /* Not JSON: the server never sends that, so it is not ours to act on. */
279
+ }
280
+ return undefined;
281
+ }
@@ -0,0 +1,159 @@
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.CHANNEL_ENV = exports.CHANNEL_LAUNCH_COMMAND = exports.CHANNEL_LAUNCH_ARGS = exports.CHANNEL_MIN_CLAUDE = void 0;
37
+ exports.parseClaudeVersion = parseClaudeVersion;
38
+ exports.claudeSupportsChannel = claudeSupportsChannel;
39
+ exports.channelLaunchFor = channelLaunchFor;
40
+ exports.cmdClaude = cmdClaude;
41
+ exports.claudeChannelAddArgs = claudeChannelAddArgs;
42
+ exports.registerChannelWithClaude = registerChannelWithClaude;
43
+ exports.claudeUserConfigPath = claudeUserConfigPath;
44
+ exports.channelRegistered = channelRegistered;
45
+ /**
46
+ * Getting the channel into Claude Code: registering the server, and starting sessions with it on.
47
+ *
48
+ * Two halves, both needed: Claude Code must know the server (`claude mcp add`, once per computer)
49
+ * AND each session must be started with the channel flag — being in the MCP config is not enough
50
+ * for a channel to deliver anything (docs: "a server also has to be named in --channels").
51
+ */
52
+ const node_child_process_1 = require("node:child_process");
53
+ const fs = __importStar(require("node:fs"));
54
+ const os = __importStar(require("node:os"));
55
+ const path = __importStar(require("node:path"));
56
+ const runtime_binary_1 = require("../runtime-binary");
57
+ const detect_1 = require("./detect");
58
+ /**
59
+ * The first Claude Code this is switched on for. Channels exist from earlier releases, but
60
+ * 2.1.234 is where Claude Code started sending a channel only what it registered for the
61
+ * session — the behaviour this design is checked against. On an older one `baychat start`
62
+ * launches without the flag (an unknown flag would stop Claude Code from starting at all), and
63
+ * the session listens through a Monitor exactly as before.
64
+ */
65
+ exports.CHANNEL_MIN_CLAUDE = [2, 1, 234];
66
+ /** `2.1.284 (Claude Code)` → [2, 1, 284]; undefined for anything else. */
67
+ function parseClaudeVersion(text) {
68
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
69
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : undefined;
70
+ }
71
+ function claudeSupportsChannel(version) {
72
+ if (!version)
73
+ return false;
74
+ for (let i = 0; i < 3; i++) {
75
+ if (version[i] !== exports.CHANNEL_MIN_CLAUDE[i])
76
+ return version[i] > exports.CHANNEL_MIN_CLAUDE[i];
77
+ }
78
+ return true;
79
+ }
80
+ /** The arguments `baychat start` adds to a Claude Code launch. */
81
+ exports.CHANNEL_LAUNCH_ARGS = [detect_1.DEV_CHANNELS_FLAG, detect_1.CHANNEL_FLAG_VALUE];
82
+ /** What to type to start Claude Code with the channel on. */
83
+ exports.CHANNEL_LAUNCH_COMMAND = "baychat claude";
84
+ /**
85
+ * The marker the channel trusts (detect.ts). Set on the ONE process launched, never exported:
86
+ * Claude Code can hide its own flags, so the launcher vouches for them instead.
87
+ */
88
+ exports.CHANNEL_ENV = { BAYCHAT_CHANNEL: "1" };
89
+ /**
90
+ * Decide for one Claude Code binary. Plain (and the session uses a Monitor) when the channel
91
+ * server is not registered or this Claude Code is too old for the flag — an unknown flag would
92
+ * stop it from starting at all.
93
+ */
94
+ function channelLaunchFor(claudePath, deps = {}) {
95
+ const registered = deps.registered ?? (() => channelRegistered());
96
+ if (!registered())
97
+ return { on: false, args: [], env: {}, why: "the BayChat channel is not set up here (run `baychat connect claude`)" };
98
+ const text = (deps.version ?? probeVersion)(claudePath);
99
+ if (!claudeSupportsChannel(text ? parseClaudeVersion(text) : undefined)) {
100
+ return { on: false, args: [], env: {}, why: `Claude Code ${exports.CHANNEL_MIN_CLAUDE.join(".")} or newer is needed for the channel (run \`claude update\`)` };
101
+ }
102
+ return { on: true, args: [...exports.CHANNEL_LAUNCH_ARGS], env: { ...exports.CHANNEL_ENV } };
103
+ }
104
+ function probeVersion(claudePath) {
105
+ const probe = (0, node_child_process_1.spawnSync)(claudePath, ["--version"], { encoding: "utf8", timeout: 10_000 });
106
+ return probe.error || probe.status !== 0 ? undefined : String(probe.stdout ?? "");
107
+ }
108
+ /**
109
+ * `baychat claude [args…]` — Claude Code with the BayChat channel switched on, in this terminal.
110
+ * No multiplexer (that is `baychat start`), nothing else changed: every argument is passed on.
111
+ */
112
+ function cmdClaude(args) {
113
+ const binary = (0, runtime_binary_1.resolveRuntimeBinary)("claude", (0, runtime_binary_1.currentBinaryEnv)());
114
+ if (!binary.ok) {
115
+ console.error(`Could not start Claude Code: ${binary.reason}`);
116
+ return 1;
117
+ }
118
+ const launch = channelLaunchFor(binary.path);
119
+ if (!launch.on)
120
+ console.error(`Starting Claude Code without the BayChat channel: ${launch.why}. Messages will use the Monitor.`);
121
+ const result = (0, node_child_process_1.spawnSync)(binary.path, [...launch.args, ...args], { stdio: "inherit", env: { ...process.env, ...launch.env } });
122
+ if (result.error) {
123
+ console.error(`Could not start Claude Code: ${result.error.message}`);
124
+ return 1;
125
+ }
126
+ return result.status ?? 0;
127
+ }
128
+ /** `claude mcp add` argv for the channel server: this CLI by absolute path, run as `channel`. */
129
+ function claudeChannelAddArgs(launch) {
130
+ return ["mcp", "add", "--scope", "user", detect_1.CHANNEL_MCP_ENTRY, "--", launch.node, launch.entry, "channel"];
131
+ }
132
+ /**
133
+ * Register the channel server with Claude Code. Re-adds on "already exists", so a CLI that moved
134
+ * (a new Node, a reinstall elsewhere) points Claude Code at where it is now.
135
+ */
136
+ function registerChannelWithClaude(launch, run) {
137
+ const args = claudeChannelAddArgs(launch);
138
+ let res = run(args);
139
+ if (res.status !== 0 && /already exists/i.test(res.stderr)) {
140
+ const removed = run(["mcp", "remove", "--scope", "user", detect_1.CHANNEL_MCP_ENTRY]);
141
+ if (removed.status === 0)
142
+ res = run(args);
143
+ }
144
+ return res.status === 0;
145
+ }
146
+ /** Claude Code's user config, where `--scope user` servers live. */
147
+ function claudeUserConfigPath(home = os.homedir()) {
148
+ return path.join(home, ".claude.json");
149
+ }
150
+ /** Is the channel server in Claude Code's user config? Read-only; unreadable counts as no. */
151
+ function channelRegistered(file = claudeUserConfigPath(), read = (p) => fs.readFileSync(p, "utf8")) {
152
+ try {
153
+ const parsed = JSON.parse(read(file));
154
+ return !!parsed?.mcpServers && Object.hasOwn(parsed.mcpServers, detect_1.CHANNEL_MCP_ENTRY);
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }