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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -0
- package/dist/connect-link.js +85 -19
- package/dist/start-command.js +38 -26
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.35.
|
|
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
|
package/dist/connect-link.js
CHANGED
|
@@ -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
|
|
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),
|
|
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
|
-
|
|
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
|
|
167
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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.
|
package/dist/start-command.js
CHANGED
|
@@ -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
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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
|
|
165
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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(
|
|
187
|
-
return
|
|
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 "
|
|
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
|
|
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.
|
|
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"
|