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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.31.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
@@ -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 items = (frame.messages ?? []).filter((m) => typeof m?.conversationId === "string" && typeof m?.messageId === "string");
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);
@@ -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
- if (bay === undefined || !bay.trim())
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)((0, config_1.loadBayLogins)(), bay);
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
- if (bay === undefined || !bay.trim()) {
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, bay);
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). Never open a Monitor beside an active channel.
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,