baychat 0.35.0 → 0.35.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.35.0",
3
+ "version": "0.35.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/README.md CHANGED
@@ -161,6 +161,11 @@ it too). On Node 20 `connect claude` says so, and `baychat doctor` fails the `ch
161
161
  catalog of the same name) gets it replaced; `connect` reports the channel added only when
162
162
  the installed plugin actually declares it.
163
163
 
164
+ **Which agent `baychat start` launches.** With one coding agent installed, that one. With more,
165
+ it asks every time, lists every installed agent and marks the last one chosen; Enter picks it.
166
+ `--runtime claude` (or `codex`, …) skips the question. With no terminal (the relay, a script) it
167
+ never asks: it starts the last choice, or the best-supported installed agent.
168
+
164
169
  To keep the system Node at 20 for other projects, run only `baychat` on Node 22 (Linux/WSL with
165
170
  systemd; details in [`CLAUDE_CODE_CHANNEL.md`](../../docs/features/CLAUDE_CODE_CHANNEL.md)):
166
171
  unpack Node 22 into `~/.local/node22`, then put a wrapper `~/.local/bin/baychat` ahead of npm's
@@ -149,46 +149,112 @@ async function cmdConnectLink(raw, deps, opts = {}) {
149
149
  }
150
150
  // The group comes only from the server's record of the link, never from anything typed here.
151
151
  const g = redeemed.group ?? null;
152
- const group = g ? ("id" in g ? g.id : g.newTitle) : undefined;
152
+ const target = g ? ("id" in g ? { id: g.id } : { newTitle: g.newTitle }) : undefined;
153
153
  const where = g ? ("id" in g ? (g.title ?? "the group") : g.newTitle) : null;
154
+ let landed;
154
155
  try {
155
- await deps.joinAndGreet(base, device.token, redeemed.agentName, helloLine(redeemed.agentName, runtime, computer), group);
156
+ landed = await deps.joinAndGreet(base, device.token, redeemed.agentName, helloLine(redeemed.agentName, runtime, computer), target);
156
157
  }
157
158
  catch (e) {
158
159
  deps.log(`Joined nothing yet: ${describeRedeemError(e)}`);
159
160
  deps.log("The computer is connected. After a restart the agent can join with /baychat <name>.");
160
161
  return 2;
161
162
  }
162
- deps.log(`✓ "${redeemed.agentName}" is in your Bay${where ? ` (group "${where}")` : ""} and said hello.`);
163
+ if (where && !landed.inGroup) {
164
+ deps.log(`✓ "${redeemed.agentName}" is in your Bay and said hello. It is in your private chat: the group "${where}" did not work (${landed.missed ?? "refused"}).`);
165
+ }
166
+ else {
167
+ deps.log(`✓ "${redeemed.agentName}" is in your Bay${where ? ` (group "${where}")` : ""} and said hello.`);
168
+ }
163
169
  deps.log(` Tell the person: restart ${runtime === "claude" ? "Claude Code" : "Codex"} (or start a session from the BayChat app) so it can answer there.`);
164
170
  return 0;
165
171
  }
166
- /** The real join + hello: the remote MCP's own `join_session` and `send_message`, as the device. */
167
- async function joinAndGreetOverMcp(base, deviceToken, name, hello, group) {
172
+ /** The conversation a join or create answer names: its text says "conversation <id>." either way. */
173
+ function conversationOf(answer) {
174
+ const s = answer.structured;
175
+ return ((typeof s.conversationId === "string" && s.conversationId) ||
176
+ /conversation (\S+?)\./.exec(answer.text)?.[1] ||
177
+ (typeof s.directConversationId === "string" && s.directConversationId) ||
178
+ undefined);
179
+ }
180
+ /**
181
+ * Join the link's group; answers its conversation. `join_session` only finds a group that exists,
182
+ * so a "New group" is made first — by this device, in its Bay, with this session in it — and then
183
+ * joined by its id like any other (B27: a link for "My agents" used to fail "No group named").
184
+ */
185
+ async function joinGroup(call, name, target) {
186
+ let group;
187
+ if ("id" in target) {
188
+ group = target.id;
189
+ }
190
+ else {
191
+ try {
192
+ const made = await call("create_group", { session: name, title: target.newTitle });
193
+ const id = conversationOf(made);
194
+ if (!id)
195
+ throw new Error("BayChat made the group but named no conversation.");
196
+ group = id;
197
+ }
198
+ catch (e) {
199
+ // A second link for the same new group finds it already made (the title is taken): join that
200
+ // one by its title. Anything else is the reason the person reads.
201
+ try {
202
+ return await joinAs(call, name, target.newTitle);
203
+ }
204
+ catch {
205
+ throw e;
206
+ }
207
+ }
208
+ }
209
+ return joinAs(call, name, group);
210
+ }
211
+ async function joinAs(call, name, group) {
212
+ const joined = await call("join_session", { session: name, group });
213
+ const id = conversationOf(joined);
214
+ if (!id)
215
+ throw new Error("BayChat joined the group but named no conversation.");
216
+ return id;
217
+ }
218
+ /** The real join + hello: the remote MCP's own tools (`create_group`, `join_session`, `send_message`), as the device. */
219
+ async function joinAndGreetOverMcp(base, deviceToken, name, hello, target) {
168
220
  const client = new index_js_1.Client({ name: "baychat-connect", version: "1" });
169
221
  await client.connect(new streamableHttp_js_1.StreamableHTTPClientTransport(new URL("/api/mcp", base), {
170
222
  requestInit: { headers: { Authorization: `Bearer ${deviceToken}` }, signal: AbortSignal.timeout(20_000) },
171
223
  }));
224
+ const call = async (tool, args) => {
225
+ const res = await client.callTool({ name: tool, arguments: args });
226
+ const text = res.content.map((c) => c.text ?? "").join("\n");
227
+ if (res.isError)
228
+ throw new Error(text || `BayChat refused ${tool}.`);
229
+ return { text, structured: (res.structuredContent ?? {}) };
230
+ };
172
231
  try {
173
- const joined = await client.callTool({ name: "join_session", arguments: { session: name, ...(group ? { group } : {}) } });
174
- const text = joined.content.map((c) => c.text ?? "").join("\n");
175
- if (joined.isError)
176
- throw new Error(text || "BayChat refused the join.");
177
- const structured = (joined.structuredContent ?? {});
178
- // A group join names the group's conversation in its text; a private join also returns it
179
- // as `directConversationId`. The group wins when there is one.
180
- const conversationId = /conversation (\S+?)\./.exec(text)?.[1] ||
181
- (typeof structured.directConversationId === "string" && structured.directConversationId) ||
182
- undefined;
183
- if (!conversationId)
184
- throw new Error("BayChat joined the session but named no chat to greet in.");
232
+ const joinPrivately = async (missed) => {
233
+ const id = conversationOf(await call("join_session", { session: name }));
234
+ if (!id)
235
+ throw new Error("BayChat joined the session but named no chat to greet in.");
236
+ return { conversationId: id, inGroup: false, ...(missed ? { missed } : {}) };
237
+ };
238
+ let landed;
239
+ if (!target) {
240
+ landed = await joinPrivately();
241
+ }
242
+ else {
243
+ try {
244
+ landed = { conversationId: await joinGroup(call, name, target), inGroup: true };
245
+ }
246
+ catch (e) {
247
+ // The person still gets their agent, in their private chat, with the reason on one line.
248
+ landed = await joinPrivately((e instanceof Error ? e.message : String(e)).split("\n")[0].trim());
249
+ }
250
+ }
185
251
  const sent = await client.callTool({
186
252
  name: "send_message",
187
- arguments: { session: name, conversationId, content: hello },
253
+ arguments: { session: name, conversationId: landed.conversationId, content: hello },
188
254
  });
189
255
  if (sent.isError)
190
256
  throw new Error("Joined, but the hello was refused.");
191
- return conversationId;
257
+ return landed;
192
258
  }
193
259
  finally {
194
260
  // Closing a finished MCP session can only fail on a dropped connection; the work is done.
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.START_RUNTIMES = void 0;
37
37
  exports.startRefusal = startRefusal;
38
38
  exports.cmdStart = cmdStart;
39
+ exports.pickRuntime = pickRuntime;
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
41
42
  const config_1 = require("./config");
@@ -155,36 +156,47 @@ async function startShouldUpdate() {
155
156
  return relay?.autoUpdate?.mode !== "auto";
156
157
  }
157
158
  /**
158
- * Which agent to start.
159
+ * Which agent to start (B30, 27 Oct 2026; Karmen: "default is codex, I want options").
159
160
  *
160
- * Asks only when it genuinely cannot tell — one installed runtime needs no question — and
161
- * remembers the answer, because being asked the same thing every morning is "something
162
- * else" by another name.
161
+ * One installed agent needs no question. With more than one, a terminal is asked EVERY time,
162
+ * with every installed agent listed, the last one chosen marked, and Enter picking it. It used to
163
+ * ask once and remember for good, so nobody could tell how to get the other agent back. With no
164
+ * terminal (the relay, a script) nothing asks, because nobody is there to press Enter: the last
165
+ * choice is used, or the best-supported installed agent.
163
166
  */
164
- async function chooseRuntime() {
165
- const remembered = readPreference();
166
- if (remembered)
167
- return remembered;
168
- const env = (0, runtime_binary_1.currentBinaryEnv)();
169
- const installed = [...new Set(exports.START_RUNTIMES)].filter((id) => (0, runtime_binary_1.resolveRuntimeBinary)(id, env).ok);
167
+ async function pickRuntime(opts) {
168
+ const { installed } = opts;
170
169
  if (installed.length === 0) {
171
- console.error("No coding agent found on this machine. Install one (Claude Code or Codex) and run this again.");
172
- return undefined;
173
- }
174
- if (installed.length === 1) {
175
- writePreference(installed[0]);
176
- return installed[0];
170
+ return { error: "No coding agent found on this machine. Install one (Claude Code or Codex) and run this again." };
177
171
  }
178
- console.log("Which agent should BayChat start?");
179
- installed.forEach((id, index) => console.log(` ${index + 1}. ${id}`));
180
- const answer = await (0, multiplexer_launch_1.askLine)(`Choose 1-${installed.length}: `);
181
- const chosen = installed[Number(answer.trim()) - 1];
182
- if (!chosen) {
183
- console.error("Not one of the options.");
172
+ if (installed.length === 1)
173
+ return { runtime: installed[0] };
174
+ const fallback = opts.last && installed.includes(opts.last) ? opts.last : installed[0];
175
+ if (!opts.interactive)
176
+ return { runtime: fallback };
177
+ opts.log("Which agent should BayChat start?");
178
+ installed.forEach((id, index) => opts.log(` ${index + 1}. ${id}${id === fallback ? " (last)" : ""}`));
179
+ const answer = (await opts.ask(`Choose 1-${installed.length} [Enter = ${fallback}]: `)).trim();
180
+ if (!answer)
181
+ return { runtime: fallback };
182
+ const chosen = installed[Number(answer) - 1] ?? installed.find((id) => id === answer.toLowerCase());
183
+ return chosen ? { runtime: chosen } : { error: "Not one of the options." };
184
+ }
185
+ async function chooseRuntime() {
186
+ const env = (0, runtime_binary_1.currentBinaryEnv)();
187
+ const picked = await pickRuntime({
188
+ installed: [...new Set(exports.START_RUNTIMES)].filter((id) => (0, runtime_binary_1.resolveRuntimeBinary)(id, env).ok),
189
+ last: readPreference(),
190
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
191
+ ask: multiplexer_launch_1.askLine,
192
+ log: (line) => console.log(line),
193
+ });
194
+ if ("error" in picked) {
195
+ console.error(picked.error);
184
196
  return undefined;
185
197
  }
186
- writePreference(chosen);
187
- return chosen;
198
+ writePreference(picked.runtime);
199
+ return picked.runtime;
188
200
  }
189
201
  function preferencePath() {
190
202
  return path.join((0, config_1.configDir)(), PREFERENCE_FILE);
@@ -195,7 +207,7 @@ function readPreference() {
195
207
  return typeof raw.runtime === "string" ? raw.runtime : undefined;
196
208
  }
197
209
  catch {
198
- // Absent or unreadable is simply "not chosen yet" — never an error worth showing.
210
+ // Absent or unreadable is simply "no last choice" — never an error worth showing.
199
211
  return undefined;
200
212
  }
201
213
  }
@@ -205,7 +217,7 @@ function writePreference(runtime) {
205
217
  fs.writeFileSync(preferencePath(), JSON.stringify({ runtime }, null, 2));
206
218
  }
207
219
  catch {
208
- // Not remembering costs one question next time. Never worth failing a start for.
220
+ // Not remembering only moves the Enter default. Never worth failing a start for.
209
221
  }
210
222
  }
211
223
  function valueAfter(args, flagName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
5
5
  "bin": {
6
6
  "baychat": "dist/index.js"