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 +7 -0
- package/dist/channel/detect.js +145 -0
- package/dist/channel/feed.js +281 -0
- package/dist/channel/launch.js +159 -0
- package/dist/channel/server.js +276 -0
- package/dist/claude-onboarding.js +19 -3
- package/dist/connect-claude.js +32 -1
- package/dist/main.js +13 -0
- package/dist/protocol-content.js +1 -1
- package/dist/skill-bootstrap.js +1 -1
- package/dist/start-command.js +14 -5
- package/package.json +1 -1
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
|
+
}
|