baychat 0.26.0 → 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 +7 -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 +19 -3
- package/dist/connect-claude.js +33 -1
- package/dist/main.js +8 -0
- package/dist/protocol-content.js +1 -1
- package/dist/skill-bootstrap.js +1 -1
- package/dist/start-command.js +22 -5
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChannelService = exports.nodeFeedConnect = exports.CHANNEL_INSTRUCTIONS = void 0;
|
|
4
|
+
exports.realLogin = realLogin;
|
|
5
|
+
exports.realCallTool = realCallTool;
|
|
6
|
+
exports.mintTicket = mintTicket;
|
|
7
|
+
exports.startChannelServer = startChannelServer;
|
|
8
|
+
/**
|
|
9
|
+
* `baychat channel` — the Claude Code channel server (docs/features/CLAUDE_CODE_CHANNEL.md).
|
|
10
|
+
*
|
|
11
|
+
* A local stdio MCP server that Claude Code starts beside the remote `baychat` connection. It
|
|
12
|
+
* declares `claude/channel`, so every `notifications/claude/channel` it sends is queued into the
|
|
13
|
+
* running session as a prompt — which is what wakes an idle session without a Monitor.
|
|
14
|
+
*
|
|
15
|
+
* It does ONE job: hold a session's incoming feed and say when something arrived. Reading,
|
|
16
|
+
* answering and everything else stay on the remote `baychat` tools, so the session's view of
|
|
17
|
+
* the Bay is the same whichever way it was woken.
|
|
18
|
+
*
|
|
19
|
+
* stdout is the JSON-RPC channel: diagnostics go to stderr only.
|
|
20
|
+
*/
|
|
21
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
22
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
23
|
+
const index_js_2 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
24
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
25
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
26
|
+
const bays_1 = require("../bays");
|
|
27
|
+
const config_1 = require("../config");
|
|
28
|
+
const ws_1 = require("../relay/ws");
|
|
29
|
+
const detect_1 = require("./detect");
|
|
30
|
+
const feed_1 = require("./feed");
|
|
31
|
+
/** Delivered to Claude as context when this server connects. */
|
|
32
|
+
exports.CHANNEL_INSTRUCTIONS = `This is BayChat's wake-up channel. After join_session, call this server's listen tool with the ` +
|
|
33
|
+
`confirmed session name (and bay, when the session joined a Bay other than the default). If it ` +
|
|
34
|
+
`answers active, BayChat messages arrive as <channel source="${detect_1.CHANNEL_MCP_ENTRY}" session="…" ` +
|
|
35
|
+
`event="messages" conversation_ids="…">: read those conversations with the remote BayChat ` +
|
|
36
|
+
`get_messages tool (the same Bay's tools the session joined with), obey shouldRespond and answer ` +
|
|
37
|
+
`in BayChat. Do NOT also open a Monitor or call listen_messages: that would replace this feed. ` +
|
|
38
|
+
`If listen answers inactive or failed, use listen_messages and a native Monitor as before. ` +
|
|
39
|
+
`event="reset" means catch up with get_messages; event="stopped" means the feed ended and says why.`;
|
|
40
|
+
const LISTEN_TOOL = {
|
|
41
|
+
name: "listen",
|
|
42
|
+
description: "Hear this BayChat session's incoming messages through the Claude Code channel instead of a Monitor. " +
|
|
43
|
+
"Call once, right after join_session, with the confirmed session name. Answers active (messages will " +
|
|
44
|
+
"arrive as <channel> events; open no Monitor), inactive (this Claude Code was not started with the " +
|
|
45
|
+
"channel; use listen_messages + Monitor) or failed (use listen_messages + Monitor).",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
session: { type: "string", description: "The session name join_session confirmed." },
|
|
50
|
+
bay: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "The Bay the session joined, when it is not this computer's default Bay (its name or baychat-<bay> entry).",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["session"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
/** The real login lookup: the default Bay, or a named one from `credentials.json`. */
|
|
59
|
+
function realLogin(bay) {
|
|
60
|
+
const device = (0, config_1.loadDeviceCredentials)();
|
|
61
|
+
if (!device)
|
|
62
|
+
throw new Error("This computer is not signed in to BayChat. Run `baychat login`.");
|
|
63
|
+
const bays = (0, config_1.loadBayLogins)();
|
|
64
|
+
if (bay === undefined || !bay.trim()) {
|
|
65
|
+
const def = bays.find((b) => b.isDefault);
|
|
66
|
+
return { baseUrl: device.baseUrl, token: device.token, expiresAt: device.expiresAt, ...(def ? { bayName: def.bayName } : {}) };
|
|
67
|
+
}
|
|
68
|
+
const found = (0, bays_1.findBayLogin)(bays, bay);
|
|
69
|
+
return { baseUrl: device.baseUrl, token: found.token, expiresAt: found.expiresAt, bayName: found.bayName };
|
|
70
|
+
}
|
|
71
|
+
/** One remote tool call over the BayChat HTTP MCP endpoint, then disconnect. */
|
|
72
|
+
async function realCallTool(login, name, args) {
|
|
73
|
+
const client = new index_js_2.Client({ name: "baychat-channel", version: "1" });
|
|
74
|
+
try {
|
|
75
|
+
await client.connect(new streamableHttp_js_1.StreamableHTTPClientTransport(new URL("/api/mcp", login.baseUrl), {
|
|
76
|
+
requestInit: {
|
|
77
|
+
headers: { Authorization: `Bearer ${login.token}` },
|
|
78
|
+
signal: AbortSignal.timeout(15_000),
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
return (await client.callTool({ name, arguments: args }));
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await client.close().catch(() => {
|
|
85
|
+
/* The call already finished or failed; a failed close has nothing left to release. */
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Node's built-in WebSocket with subprotocols (the ticket rides in them, never the URL). */
|
|
90
|
+
const nodeFeedConnect = (url, protocols, handlers) => {
|
|
91
|
+
const ctor = globalThis.WebSocket;
|
|
92
|
+
if (typeof ctor !== "function") {
|
|
93
|
+
throw new ws_1.WebSocketUnavailableError("this Node has no built-in WebSocket (Node 22 or newer is needed for the channel)");
|
|
94
|
+
}
|
|
95
|
+
const ws = new ctor(url, protocols);
|
|
96
|
+
ws.onopen = () => handlers.onOpen();
|
|
97
|
+
ws.onmessage = (ev) => handlers.onMessage(typeof ev?.data === "string" ? ev.data : String(ev?.data ?? ""));
|
|
98
|
+
ws.onerror = (ev) => handlers.onError(typeof ev?.message === "string" && ev.message ? ev.message : "socket error");
|
|
99
|
+
ws.onclose = (ev) => handlers.onClose(typeof ev?.code === "number" ? ev.code : 1006, ev?.reason ?? "");
|
|
100
|
+
return { send: (data) => ws.send(data), close: (code, reason) => ws.close(code, reason) };
|
|
101
|
+
};
|
|
102
|
+
exports.nodeFeedConnect = nodeFeedConnect;
|
|
103
|
+
function resultText(result) {
|
|
104
|
+
return (result.content ?? [])
|
|
105
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
106
|
+
.map((c) => c.text)
|
|
107
|
+
.join("\n")
|
|
108
|
+
.trim();
|
|
109
|
+
}
|
|
110
|
+
/** HTTP status of a refused MCP request, when the SDK error carries one. */
|
|
111
|
+
function httpStatus(err) {
|
|
112
|
+
const code = err?.code;
|
|
113
|
+
return typeof code === "number" ? code : undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Mint a listener ticket through `listen_messages`. Fatal only when retrying cannot help: the
|
|
117
|
+
* session is gone, or the login was refused or ran out. Everything else (offline, a restart,
|
|
118
|
+
* the per-minute limit) is retried by the feed.
|
|
119
|
+
*/
|
|
120
|
+
async function mintTicket(deps, bay, session, cursor) {
|
|
121
|
+
let login;
|
|
122
|
+
try {
|
|
123
|
+
login = deps.login(bay);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return { ok: false, fatal: true, detail: err instanceof Error ? err.message : String(err) };
|
|
127
|
+
}
|
|
128
|
+
const expires = Date.parse(login.expiresAt);
|
|
129
|
+
if (login.expiresAt && Number.isFinite(expires) && expires <= Date.now()) {
|
|
130
|
+
return { ok: false, fatal: true, detail: "This computer's BayChat login has expired. Run `baychat login`." };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const result = await deps.callTool(login, "listen_messages", { session, ...(cursor ? { cursor } : {}) });
|
|
134
|
+
const text = resultText(result);
|
|
135
|
+
if (result.isError) {
|
|
136
|
+
const fatal = /no live session|SESSION_NOT_FOUND|not a member|cannot delegate/i.test(text);
|
|
137
|
+
return { ok: false, fatal, detail: text || "BayChat refused to open the feed." };
|
|
138
|
+
}
|
|
139
|
+
const structured = (result.structuredContent ?? safeJson(text));
|
|
140
|
+
const ws = structured?.monitor?.ws;
|
|
141
|
+
if (typeof ws?.url !== "string" || !Array.isArray(ws.protocols) || !ws.protocols.every((p) => typeof p === "string")) {
|
|
142
|
+
return { ok: false, fatal: false, detail: "BayChat's answer had no feed address." };
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, ticket: { url: ws.url, protocols: ws.protocols } };
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const status = httpStatus(err);
|
|
148
|
+
if (status === 401 || status === 403) {
|
|
149
|
+
return { ok: false, fatal: true, detail: "BayChat refused this computer's login. Run `baychat login`." };
|
|
150
|
+
}
|
|
151
|
+
return { ok: false, fatal: false, detail: err instanceof Error ? err.message : String(err) };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function safeJson(text) {
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(text);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function toolResult(status, text, extra = {}) {
|
|
163
|
+
return { content: [{ type: "text", text }], structuredContent: { status, ...extra } };
|
|
164
|
+
}
|
|
165
|
+
/** The tool-call side of the server, separate from stdio so the tests drive it directly. */
|
|
166
|
+
class ChannelService {
|
|
167
|
+
deps;
|
|
168
|
+
push;
|
|
169
|
+
feeds = new Map();
|
|
170
|
+
constructor(deps, push) {
|
|
171
|
+
this.deps = deps;
|
|
172
|
+
this.push = push;
|
|
173
|
+
}
|
|
174
|
+
async listen(args) {
|
|
175
|
+
const session = typeof args.session === "string" ? args.session.trim() : "";
|
|
176
|
+
if (!session)
|
|
177
|
+
return { content: [{ type: "text", text: "Pass session: the name join_session confirmed." }], isError: true };
|
|
178
|
+
const bay = typeof args.bay === "string" && args.bay.trim() ? args.bay.trim() : undefined;
|
|
179
|
+
const evidence = this.deps.detect();
|
|
180
|
+
if (!evidence.active) {
|
|
181
|
+
return toolResult("inactive", `Inactive: this Claude Code session was not started with the BayChat channel, so its events would be ` +
|
|
182
|
+
`dropped. Use listen_messages and a native Monitor for session "${session}" as usual. To use the channel ` +
|
|
183
|
+
`next time, start Claude Code with \`baychat start\`, or \`claude ${detect_1.DEV_CHANNELS_FLAG} ${detect_1.CHANNEL_FLAG_VALUE}\`.`, { session });
|
|
184
|
+
}
|
|
185
|
+
let bayName;
|
|
186
|
+
try {
|
|
187
|
+
bayName = this.deps.login(bay).bayName;
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
return toolResult("failed", `Failed: ${err instanceof Error ? err.message : String(err)} Use listen_messages and a native Monitor instead.`, { session });
|
|
191
|
+
}
|
|
192
|
+
const key = `${bayName ?? ""}\u0000${session}`;
|
|
193
|
+
this.feeds.get(key)?.stop();
|
|
194
|
+
const feed = new feed_1.ChannelFeed({
|
|
195
|
+
session,
|
|
196
|
+
...(bayName ? { bayName } : {}),
|
|
197
|
+
mint: (cursor) => mintTicket(this.deps, bay, session, cursor),
|
|
198
|
+
connect: this.deps.connect,
|
|
199
|
+
status: async () => {
|
|
200
|
+
try {
|
|
201
|
+
const result = await this.deps.callTool(this.deps.login(bay), "get_delivery_status", { session });
|
|
202
|
+
if (result.isError)
|
|
203
|
+
return "unknown";
|
|
204
|
+
const s = result.structuredContent?.status ?? safeJsonStatus(resultText(result));
|
|
205
|
+
return s === "connected" || s === "disconnected" ? s : "unknown";
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return "unknown";
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
push: this.push,
|
|
212
|
+
log: this.deps.log,
|
|
213
|
+
...this.deps.feedOverrides,
|
|
214
|
+
});
|
|
215
|
+
this.feeds.set(key, feed);
|
|
216
|
+
const started = await feed.start();
|
|
217
|
+
if (!started.ok) {
|
|
218
|
+
this.feeds.delete(key);
|
|
219
|
+
return toolResult("failed", `Failed: ${started.detail} Use listen_messages and a native Monitor instead.`, { session });
|
|
220
|
+
}
|
|
221
|
+
return toolResult("active", `Active: session "${session}"${bayName ? ` (${bayName})` : ""} now hears the Bay through the channel. New messages ` +
|
|
222
|
+
`arrive here as <channel source="${detect_1.CHANNEL_MCP_ENTRY}"> events, with no deadline and no re-arming. Do NOT open a ` +
|
|
223
|
+
`Monitor or call listen_messages — either would replace this feed. Now read one get_messages batch (limit 10) ` +
|
|
224
|
+
`to catch up on anything from before listening.`, { session, ...(bayName ? { bay: bayName } : {}), how: evidence.how });
|
|
225
|
+
}
|
|
226
|
+
stopAll() {
|
|
227
|
+
for (const feed of this.feeds.values())
|
|
228
|
+
feed.stop();
|
|
229
|
+
this.feeds.clear();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.ChannelService = ChannelService;
|
|
233
|
+
function safeJsonStatus(text) {
|
|
234
|
+
const parsed = safeJson(text);
|
|
235
|
+
return parsed?.status;
|
|
236
|
+
}
|
|
237
|
+
/** Boot on stdio and block on the transport. */
|
|
238
|
+
async function startChannelServer(deps) {
|
|
239
|
+
const log = deps?.log ?? ((line) => console.error(`[baychat channel] ${line}`));
|
|
240
|
+
const mcp = new index_js_1.Server({ name: detect_1.CHANNEL_MCP_ENTRY, version: "1" }, {
|
|
241
|
+
capabilities: { experimental: { "claude/channel": {} }, tools: {} },
|
|
242
|
+
instructions: exports.CHANNEL_INSTRUCTIONS,
|
|
243
|
+
});
|
|
244
|
+
const service = new ChannelService({
|
|
245
|
+
login: deps?.login ?? realLogin,
|
|
246
|
+
detect: deps?.detect ?? (() => (0, detect_1.detectChannel)()),
|
|
247
|
+
callTool: deps?.callTool ?? realCallTool,
|
|
248
|
+
connect: deps?.connect ?? exports.nodeFeedConnect,
|
|
249
|
+
log,
|
|
250
|
+
}, async (content, meta) => {
|
|
251
|
+
try {
|
|
252
|
+
await mcp.notification({ method: "notifications/claude/channel", params: { content, meta } });
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
log(`could not hand an event to Claude Code: ${err instanceof Error ? err.message : String(err)}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
mcp.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: [LISTEN_TOOL] }));
|
|
259
|
+
mcp.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
260
|
+
if (req.params.name === LISTEN_TOOL.name)
|
|
261
|
+
return service.listen((req.params.arguments ?? {}));
|
|
262
|
+
return { content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }], isError: true };
|
|
263
|
+
});
|
|
264
|
+
// Claude Code closes our stdin when the session ends. An open feed socket would otherwise
|
|
265
|
+
// keep this process alive with nobody left to tell, holding the session "connected".
|
|
266
|
+
const shutdown = () => {
|
|
267
|
+
service.stopAll();
|
|
268
|
+
process.exit(0);
|
|
269
|
+
};
|
|
270
|
+
mcp.onclose = shutdown;
|
|
271
|
+
process.stdin.once("end", shutdown);
|
|
272
|
+
process.stdin.once("close", shutdown);
|
|
273
|
+
await mcp.connect(new stdio_js_1.StdioServerTransport());
|
|
274
|
+
log("ready on stdio");
|
|
275
|
+
}
|
|
@@ -4,7 +4,8 @@ exports.BAY_CHOICE = exports.CLAUDE_REACHABILITY = exports.CLAUDE_JOIN_STEPS = v
|
|
|
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,7 +24,14 @@ 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
36
|
\`ws: {url, protocols}\` and \`timeout_ms\`. If your Monitor offers a
|
|
29
37
|
\`persistent\` option, also set \`persistent: true\`. Keep its ticket private.
|
|
@@ -51,7 +59,15 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
|
|
|
51
59
|
/** Native socket lifecycle, including the intentional-stop boundary. */
|
|
52
60
|
exports.CLAUDE_REACHABILITY = `## Staying reachable — native remote messages
|
|
53
61
|
|
|
54
|
-
|
|
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,
|
|
55
71
|
read the indicated conversations with \`get_messages\`, deduplicate message IDs,
|
|
56
72
|
obey \`shouldRespond\`, acknowledge slow work with 👀, and answer in BayChat.
|
|
57
73
|
Use \`contact_agent\` for directed replies to peers. A delivery event is not a
|
package/dist/connect-claude.js
CHANGED
|
@@ -6,6 +6,9 @@ const config_1 = require("./config");
|
|
|
6
6
|
const bay_mcp_1 = require("./bay-mcp");
|
|
7
7
|
const client_paths_1 = require("./client-paths");
|
|
8
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");
|
|
9
12
|
/** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
|
|
10
13
|
async function cmdConnectClaude(opts = {}) {
|
|
11
14
|
try {
|
|
@@ -43,7 +46,7 @@ async function cmdConnectClaude(opts = {}) {
|
|
|
43
46
|
console.log(" " + line);
|
|
44
47
|
if (!registered)
|
|
45
48
|
return 1;
|
|
46
|
-
|
|
49
|
+
reportChannel(registerChannel());
|
|
47
50
|
console.log(' Restart Claude Code once, then use /baychat <name> "<group>".');
|
|
48
51
|
return 0;
|
|
49
52
|
}
|
|
@@ -53,3 +56,32 @@ async function cmdConnectClaude(opts = {}) {
|
|
|
53
56
|
return 1;
|
|
54
57
|
}
|
|
55
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/main.js
CHANGED
|
@@ -15,6 +15,7 @@ const connect_1 = require("./connect");
|
|
|
15
15
|
const connect_plan_1 = require("./connect-plan");
|
|
16
16
|
const runtimes_1 = require("./runtimes");
|
|
17
17
|
const mcp_1 = require("./mcp");
|
|
18
|
+
const server_1 = require("./channel/server");
|
|
18
19
|
const mcp_config_1 = require("./mcp-config");
|
|
19
20
|
const commands_2 = require("./relay/commands");
|
|
20
21
|
const bays_1 = require("./bays");
|
|
@@ -472,6 +473,13 @@ async function main() {
|
|
|
472
473
|
(0, mcp_config_1.cmdMcpConfig)(args.includes("--client") ? ((0, args_1.flag)(args, "--client") ?? "") : undefined);
|
|
473
474
|
return 0;
|
|
474
475
|
}
|
|
476
|
+
case "channel": {
|
|
477
|
+
// The Claude Code channel server (docs/features/CLAUDE_CODE_CHANNEL.md). Claude Code starts
|
|
478
|
+
// it; like `mcp`, it blocks on stdio and exits when Claude Code closes its stdin.
|
|
479
|
+
await (0, server_1.startChannelServer)();
|
|
480
|
+
await new Promise(() => { });
|
|
481
|
+
return 0; // unreachable
|
|
482
|
+
}
|
|
475
483
|
case "mcp": {
|
|
476
484
|
// Boot the stdio MCP server, then block forever: the transport keeps the
|
|
477
485
|
// process alive on stdin, and falling through to process.exit() would kill
|