baychat 0.31.0 → 0.32.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/.claude-plugin/plugin.json +1 -1
- package/dist/bays.js +23 -1
- package/dist/channel/feed.js +72 -1
- package/dist/channel/server.js +11 -5
- package/dist/claude-onboarding.js +3 -1
- package/dist/protocol-content.js +1 -1
- package/dist/relay/bay-relays.js +2 -1
- package/dist/relay/commands.js +2 -1
- package/dist/relay/daemon.js +61 -1
- package/dist/relay/launcher/approval-settings.js +63 -6
- package/dist/relay/launcher/argv.js +19 -5
- package/dist/relay/launcher/handle.js +85 -22
- package/dist/relay/launcher/loop.js +8 -3
- package/dist/relay/launcher/multiplexer.js +32 -1
- package/dist/relay/launcher/relay-identity.js +41 -0
- package/dist/relay/log-line.js +11 -0
- package/dist/relay/registry.js +7 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
4
4
|
"displayName": "BayChat",
|
|
5
5
|
"description": "Wakes this Claude Code session the moment a BayChat message arrives: no Monitor, no 30-minute deadline. Uses the baychat CLI already on this computer (npm install -g baychat, then baychat login).",
|
|
6
6
|
"author": {
|
package/dist/bays.js
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
//
|
|
15
15
|
// This file is the PURE part — names, lookups, the default switch. No I/O.
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.DEFAULT_MCP_ENTRY = void 0;
|
|
17
|
+
exports.LAUNCH_MCP_ENTRY = exports.DEFAULT_MCP_ENTRY = void 0;
|
|
18
|
+
exports.channelBay = channelBay;
|
|
18
19
|
exports.baySlug = baySlug;
|
|
19
20
|
exports.bayEntryName = bayEntryName;
|
|
20
21
|
exports.extraBayEntries = extraBayEntries;
|
|
@@ -24,6 +25,27 @@ exports.withDefaultBay = withDefaultBay;
|
|
|
24
25
|
exports.renderBayList = renderBayList;
|
|
25
26
|
/** The MCP server name every client has always had: the default Bay. */
|
|
26
27
|
exports.DEFAULT_MCP_ENTRY = "baychat";
|
|
28
|
+
/**
|
|
29
|
+
* The MCP server name of a session launched by a relay on a setup of its own (another config
|
|
30
|
+
* folder or server: relay/launcher/relay-identity.ts). It is a CONNECTION name, never a Bay:
|
|
31
|
+
* that session has exactly one login, the relay's, so wherever it is passed as a Bay it means
|
|
32
|
+
* the default login (`channelBay`).
|
|
33
|
+
*/
|
|
34
|
+
exports.LAUNCH_MCP_ENTRY = "baychat-relay";
|
|
35
|
+
/**
|
|
36
|
+
* The Bay a channel call names, as the login lookup should read it: undefined = the default
|
|
37
|
+
* login. A value that is the launch connection's name (`baychat-relay`), or any value on a
|
|
38
|
+
* computer with one login (no saved Bay set, or a set of one), is the default — there is no
|
|
39
|
+
* other Bay it could mean, and failing there left a launched session without its channel
|
|
40
|
+
* (Mac QA 2026-10-16: `listen {"bay":"baychat-relay"}` → failed). With several Bays an unknown
|
|
41
|
+
* name still fails in `findBayLogin`: the wrong Bay is the wrong room.
|
|
42
|
+
*/
|
|
43
|
+
function channelBay(bay, bays) {
|
|
44
|
+
const q = bay?.trim();
|
|
45
|
+
if (!q || q === exports.LAUNCH_MCP_ENTRY || bays.length <= 1)
|
|
46
|
+
return undefined;
|
|
47
|
+
return q;
|
|
48
|
+
}
|
|
27
49
|
/** How a Bay name becomes part of an MCP server name: `[a-z0-9-]`, no edge dashes, short. */
|
|
28
50
|
function baySlug(name) {
|
|
29
51
|
const slug = name
|
package/dist/channel/feed.js
CHANGED
|
@@ -9,6 +9,9 @@ const READY_TIMEOUT_MS = 10_000;
|
|
|
9
9
|
const WATCHDOG_MS = 2 * 60_000;
|
|
10
10
|
/** A timer that fires this much later than asked means the computer slept: reconnect at once. */
|
|
11
11
|
const SLEEP_GAP_MS = 60_000;
|
|
12
|
+
/** The card's words, as one short line: they are the session's own, but this is paid output. */
|
|
13
|
+
const APPROVAL_TEXT_MAX = 160;
|
|
14
|
+
const REQUEST_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
12
15
|
class ChannelFeed {
|
|
13
16
|
deps;
|
|
14
17
|
state = "connecting";
|
|
@@ -248,7 +251,26 @@ class ChannelFeed {
|
|
|
248
251
|
this.armWatchdog();
|
|
249
252
|
}
|
|
250
253
|
async pushMessages(frame) {
|
|
251
|
-
const
|
|
254
|
+
const entries = (frame.messages ?? []).filter((m) => typeof m?.conversationId === "string" && typeof m?.messageId === "string");
|
|
255
|
+
// An answered decision is said as itself; anything that does not read as one stays an
|
|
256
|
+
// ordinary wake, so a malformed entry can never make the session miss a message.
|
|
257
|
+
const approvals = [];
|
|
258
|
+
const clears = [];
|
|
259
|
+
const items = [];
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
const approval = answeredApproval(entry);
|
|
262
|
+
const cleared = approval ? undefined : clearedChat(entry);
|
|
263
|
+
if (approval)
|
|
264
|
+
approvals.push(approval);
|
|
265
|
+
else if (cleared)
|
|
266
|
+
clears.push(cleared);
|
|
267
|
+
else
|
|
268
|
+
items.push(entry);
|
|
269
|
+
}
|
|
270
|
+
for (const approval of approvals)
|
|
271
|
+
await this.pushApproval(approval);
|
|
272
|
+
for (const cleared of clears)
|
|
273
|
+
await this.pushCleared(cleared);
|
|
252
274
|
if (!items.length)
|
|
253
275
|
return;
|
|
254
276
|
const conversations = [...new Set(items.map((m) => m.conversationId))];
|
|
@@ -262,6 +284,23 @@ class ChannelFeed {
|
|
|
262
284
|
message_ids: items.map((m) => m.messageId).join(","),
|
|
263
285
|
}));
|
|
264
286
|
}
|
|
287
|
+
async pushApproval(a) {
|
|
288
|
+
await this.deps.push(`Your decision "${a.question}" (${a.requestId}) in conversation ${a.conversationId} was answered: "${a.answer}". ` +
|
|
289
|
+
`Call await_approval with this requestId to confirm, then continue.`, this.meta({
|
|
290
|
+
event: "approval",
|
|
291
|
+
conversation_ids: a.conversationId,
|
|
292
|
+
message_ids: a.messageId,
|
|
293
|
+
request_id: a.requestId,
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
async pushCleared(c) {
|
|
297
|
+
await this.deps.push(`${c.by} cleared BayChat conversation ${c.conversationId} for everyone: every earlier message and file in it was deleted. ` +
|
|
298
|
+
`Treat that chat as a fresh start — do not refer to or rely on anything said there before. No reply is needed.`, this.meta({
|
|
299
|
+
event: "cleared",
|
|
300
|
+
conversation_ids: c.conversationId,
|
|
301
|
+
message_ids: c.messageId,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
265
304
|
async pushReset() {
|
|
266
305
|
await this.deps.push(`BayChat reset the feed for session "${this.deps.session}". Catch up on your joined conversations with get_messages; ` +
|
|
267
306
|
`a reset never means there were no messages. The channel stays open.`, this.meta({ event: "reset" }));
|
|
@@ -282,6 +321,38 @@ class ChannelFeed {
|
|
|
282
321
|
}
|
|
283
322
|
}
|
|
284
323
|
exports.ChannelFeed = ChannelFeed;
|
|
324
|
+
/** An entry's `approval`, when it is a well-formed answered decision; else undefined. */
|
|
325
|
+
function answeredApproval(entry) {
|
|
326
|
+
const a = entry.approval;
|
|
327
|
+
if (!a || typeof a !== "object" || a.status !== "answered")
|
|
328
|
+
return undefined;
|
|
329
|
+
if (typeof a.requestId !== "string" || !REQUEST_ID.test(a.requestId))
|
|
330
|
+
return undefined;
|
|
331
|
+
if (typeof a.question !== "string" || typeof a.answer !== "string")
|
|
332
|
+
return undefined;
|
|
333
|
+
const question = oneLine(a.question);
|
|
334
|
+
const answer = oneLine(a.answer);
|
|
335
|
+
if (!answer)
|
|
336
|
+
return undefined;
|
|
337
|
+
return { conversationId: entry.conversationId, messageId: entry.messageId, requestId: a.requestId, question, answer };
|
|
338
|
+
}
|
|
339
|
+
/** An entry's `cleared`, when it is a well-formed "cleared for everyone" notice; else undefined. */
|
|
340
|
+
function clearedChat(entry) {
|
|
341
|
+
const c = entry.cleared;
|
|
342
|
+
if (!c || typeof c !== "object" || c.status !== "cleared")
|
|
343
|
+
return undefined;
|
|
344
|
+
const by = typeof c.by === "string" ? oneLine(c.by) : "";
|
|
345
|
+
return { conversationId: entry.conversationId, messageId: entry.messageId, by: by || "Someone" };
|
|
346
|
+
}
|
|
347
|
+
/** One line of plain text, no control characters or quotes that would blur where it ends. */
|
|
348
|
+
function oneLine(text) {
|
|
349
|
+
const line = text
|
|
350
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, " ")
|
|
351
|
+
.replace(/"/g, "'")
|
|
352
|
+
.replace(/\s+/g, " ")
|
|
353
|
+
.trim();
|
|
354
|
+
return line.length > APPROVAL_TEXT_MAX ? `${line.slice(0, APPROVAL_TEXT_MAX - 1).trimEnd()}…` : line;
|
|
355
|
+
}
|
|
285
356
|
function parseFrame(data) {
|
|
286
357
|
try {
|
|
287
358
|
const parsed = JSON.parse(data);
|
package/dist/channel/server.js
CHANGED
|
@@ -83,6 +83,7 @@ exports.CHANNEL_INSTRUCTIONS = `This is BayChat's wake-up channel. After join_se
|
|
|
83
83
|
`If listen answers inactive or failed, use listen_messages and a native Monitor as before. ` +
|
|
84
84
|
`event="reset" means catch up with get_messages; event="stopped" means the feed ended and says why; ` +
|
|
85
85
|
`event="handoff" is BayChat's own request for a handoff note before a fresh start: do what it says. ` +
|
|
86
|
+
`event="approval" means the owner answered your request_approval card: call await_approval with its request_id, then continue. ` +
|
|
86
87
|
`Tool-permission prompts are also offered to the session's owner as a BayChat card; answer in the terminal or ` +
|
|
87
88
|
`on the phone, whichever is first. Answer in BayChat only; in the terminal write at most one short line, ` +
|
|
88
89
|
`never the answer again (both are paid output).`;
|
|
@@ -114,22 +115,27 @@ function realCommandLink(log) {
|
|
|
114
115
|
}
|
|
115
116
|
/** The relay that serves a Bay's sessions: the default relay, or that Bay's own (relay/bay-relays.ts). */
|
|
116
117
|
function relaySocketFor(bay) {
|
|
117
|
-
|
|
118
|
+
const bays = (0, config_1.loadBayLogins)();
|
|
119
|
+
const named = (0, bays_1.channelBay)(bay, bays);
|
|
120
|
+
if (named === undefined)
|
|
118
121
|
return (0, socket_1.socketPath)();
|
|
119
|
-
const login = (0, bays_1.findBayLogin)(
|
|
122
|
+
const login = (0, bays_1.findBayLogin)(bays, named);
|
|
120
123
|
return login.isDefault ? (0, socket_1.socketPath)() : (0, bay_relays_1.bayRelayScope)(login.bayId, (0, config_1.configDir)(), (0, socket_1.socketPath)()).socket;
|
|
121
124
|
}
|
|
122
|
-
/** The real login lookup: the default Bay, or a named one from `credentials.json
|
|
125
|
+
/** The real login lookup: the default Bay, or a named one from `credentials.json` (`channelBay`). */
|
|
123
126
|
function realLogin(bay) {
|
|
124
127
|
const device = (0, config_1.loadDeviceCredentials)();
|
|
125
128
|
if (!device)
|
|
126
129
|
throw new Error("This computer is not signed in to BayChat. Run `baychat login`.");
|
|
127
130
|
const bays = (0, config_1.loadBayLogins)();
|
|
128
|
-
|
|
131
|
+
// `baychat-relay` (a launched session's connection), or any name on a one-login computer, is
|
|
132
|
+
// the default login (bays.ts `channelBay`).
|
|
133
|
+
const named = (0, bays_1.channelBay)(bay, bays);
|
|
134
|
+
if (named === undefined) {
|
|
129
135
|
const def = bays.find((b) => b.isDefault);
|
|
130
136
|
return { baseUrl: device.baseUrl, token: device.token, expiresAt: device.expiresAt, ...(def ? { bayName: def.bayName } : {}) };
|
|
131
137
|
}
|
|
132
|
-
const found = (0, bays_1.findBayLogin)(bays,
|
|
138
|
+
const found = (0, bays_1.findBayLogin)(bays, named);
|
|
133
139
|
return { baseUrl: device.baseUrl, token: found.token, expiresAt: found.expiresAt, bayName: found.bayName };
|
|
134
140
|
}
|
|
135
141
|
/** One remote tool call over the BayChat HTTP MCP endpoint, then disconnect. */
|
|
@@ -73,7 +73,9 @@ Read those conversations with \`get_messages\` and answer as below. There is no
|
|
|
73
73
|
deadline and nothing to re-arm; the channel reconnects by itself. \`event="reset"\`
|
|
74
74
|
means catch up with \`get_messages\`; \`event="stopped"\` says why the feed ended
|
|
75
75
|
(for an expired session: \`join_session\` again with the same name and room, then
|
|
76
|
-
\`listen\` again)
|
|
76
|
+
\`listen\` again); \`event="approval"\` means the owner answered your
|
|
77
|
+
\`request_approval\` card: call \`await_approval\` with its \`request_id\`, then
|
|
78
|
+
continue. Never open a Monitor beside an active channel.
|
|
77
79
|
|
|
78
80
|
**With a Monitor:** keep the native Monitor (WebSocket or command) open between messages. On a \`messages\` event,
|
|
79
81
|
read the indicated conversations with \`get_messages\`, deduplicate message IDs,
|