baychat 0.16.0 → 0.17.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/dist/connect.js +48 -1
- package/dist/relay/commands.js +8 -8
- package/dist/runtime-binary.js +39 -2
- package/dist/runtimes.js +52 -3
- package/package.json +2 -2
package/dist/connect.js
CHANGED
|
@@ -32,8 +32,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
32
32
|
};
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
34
|
exports.renderConnectMenu = renderConnectMenu;
|
|
35
|
+
exports.isClaudeConnectTarget = isClaudeConnectTarget;
|
|
35
36
|
exports.parseConnectClient = parseConnectClient;
|
|
36
37
|
exports.writeClientConfig = writeClientConfig;
|
|
38
|
+
exports.cmdConnectClaude = cmdConnectClaude;
|
|
37
39
|
exports.cmdConnect = cmdConnect;
|
|
38
40
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -61,14 +63,32 @@ function renderConnectMenu() {
|
|
|
61
63
|
"Connect this laptop to BayChat, once, and configure your client:",
|
|
62
64
|
"",
|
|
63
65
|
...rows,
|
|
66
|
+
" npx baychat connect claude → Claude Code (refreshes its skill)",
|
|
64
67
|
"",
|
|
65
68
|
"You approve a QR on your phone. After that, in any coding session:",
|
|
66
69
|
"",
|
|
67
70
|
" /baychat <name> join BayChat as a session called <name>",
|
|
68
71
|
"",
|
|
69
|
-
"Claude Code
|
|
72
|
+
"First-time Claude Code setup is `npx baychat login` — it registers the MCP",
|
|
73
|
+
"server AND writes the skill. After that, `connect claude` refreshes the skill",
|
|
74
|
+
"on its own: no QR, no re-pairing. Run it after every CLI upgrade.",
|
|
70
75
|
].join("\n");
|
|
71
76
|
}
|
|
77
|
+
/** Spellings of Claude Code accepted by `connect`. */
|
|
78
|
+
const CLAUDE_CONNECT_ALIASES = new Set(["claude", "claude-code", "claudecode"]);
|
|
79
|
+
/**
|
|
80
|
+
* Is this `connect` argument Claude Code?
|
|
81
|
+
*
|
|
82
|
+
* Claude Code is deliberately NOT in `MCP_CLIENTS`: that list is clients whose
|
|
83
|
+
* MCP config is a file we write, and Claude Code's is registered by
|
|
84
|
+
* `claude mcp add` instead. But refusing the word entirely made the runtime this
|
|
85
|
+
* product is mostly used from the only one you could not `connect` — and sent a
|
|
86
|
+
* user whose credential was perfectly valid to `login`, which opens a pairing QR,
|
|
87
|
+
* when all they needed was the skill file refreshed after a CLI upgrade.
|
|
88
|
+
*/
|
|
89
|
+
function isClaudeConnectTarget(value) {
|
|
90
|
+
return CLAUDE_CONNECT_ALIASES.has(value.trim().toLowerCase());
|
|
91
|
+
}
|
|
72
92
|
/**
|
|
73
93
|
* Narrow a client argument.
|
|
74
94
|
*
|
|
@@ -159,11 +179,38 @@ const realIo = {
|
|
|
159
179
|
*
|
|
160
180
|
* @returns 0 on success, 2 when the laptop login expired without approval.
|
|
161
181
|
*/
|
|
182
|
+
/**
|
|
183
|
+
* `baychat connect claude` — refresh Claude Code's skill.
|
|
184
|
+
*
|
|
185
|
+
* Deliberately does NOT touch the credential or the MCP registration. Both are
|
|
186
|
+
* `login`'s job, and re-doing them would put a device-pairing QR in front of
|
|
187
|
+
* somebody whose login is fine. Writing the skill needs no auth at all: it is
|
|
188
|
+
* generated locally from this CLI's own version, which is exactly why it goes
|
|
189
|
+
* stale on upgrade and exactly why this command has to exist.
|
|
190
|
+
*/
|
|
191
|
+
function cmdConnectClaude() {
|
|
192
|
+
try {
|
|
193
|
+
for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
|
|
194
|
+
console.log(` ${line}`);
|
|
195
|
+
console.log("");
|
|
196
|
+
console.log(" Restart Claude Code once so it picks the skill up.");
|
|
197
|
+
console.log(" Not logged in yet? `npx baychat login` — that registers the MCP server too.");
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
console.log(`Could not install the Claude Code skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
162
205
|
async function cmdConnect(clientArg, opts = {}) {
|
|
163
206
|
if (clientArg === undefined) {
|
|
164
207
|
console.log(renderConnectMenu());
|
|
165
208
|
return 0;
|
|
166
209
|
}
|
|
210
|
+
// Claude Code first: it is not an MCP-config client, so it must not reach
|
|
211
|
+
// `parseConnectClient`, which would reject it.
|
|
212
|
+
if (isClaudeConnectTarget(clientArg))
|
|
213
|
+
return cmdConnectClaude();
|
|
167
214
|
const client = parseConnectClient(clientArg);
|
|
168
215
|
const base = (opts.base || process.env.BAYCHAT_API_URL || config_1.DEFAULT_API_URL).replace(/\/$/, "");
|
|
169
216
|
const steps = (0, connect_plan_1.planConnect)({
|
package/dist/relay/commands.js
CHANGED
|
@@ -746,7 +746,7 @@ async function cmdRelayAttach(opts) {
|
|
|
746
746
|
* bare name, which is exactly the behaviour that shipped before this existed — a
|
|
747
747
|
* machine where PATH is fine keeps working, and one where it is not now works too.
|
|
748
748
|
*/
|
|
749
|
-
function resolveRuntimeBin(runtime) {
|
|
749
|
+
function resolveRuntimeBin(runtime, env = process.env, execPath = process.execPath) {
|
|
750
750
|
// Delegates to the prover rather than walking PATH itself. The hand-rolled
|
|
751
751
|
// walk this replaces had two Windows faults, and both produced a recorded
|
|
752
752
|
// path the daemon could never spawn:
|
|
@@ -763,7 +763,7 @@ function resolveRuntimeBin(runtime) {
|
|
|
763
763
|
// `resolveRuntimeBinary` tries the platform's real extension order and PROVES
|
|
764
764
|
// each candidate by running it, which is the same question this function was
|
|
765
765
|
// always asking — just answered correctly.
|
|
766
|
-
const decided = decideRuntimeBin(runtime);
|
|
766
|
+
const decided = decideRuntimeBin(runtime, env, execPath);
|
|
767
767
|
return decided.ok ? decided.bin : undefined;
|
|
768
768
|
}
|
|
769
769
|
/**
|
|
@@ -775,16 +775,16 @@ function resolveRuntimeBin(runtime) {
|
|
|
775
775
|
* then re-probed could still register `runtimeBin: undefined` — the exact state
|
|
776
776
|
* the guard exists to prevent, reached by way of the guard.
|
|
777
777
|
*/
|
|
778
|
-
function decideRuntimeBin(runtime) {
|
|
779
|
-
const override =
|
|
780
|
-
const resolved = (0, runtime_binary_1.resolveRuntimeBinary)(runtime, (0, runtime_binary_1.currentBinaryEnv)(override));
|
|
778
|
+
function decideRuntimeBin(runtime, env = process.env, execPath = process.execPath) {
|
|
779
|
+
const override = env[`BAYCHAT_${runtime.toUpperCase()}_BIN`];
|
|
780
|
+
const resolved = (0, runtime_binary_1.resolveRuntimeBinary)(runtime, (0, runtime_binary_1.currentBinaryEnv)(override, env, execPath));
|
|
781
781
|
if (!resolved.ok) {
|
|
782
782
|
// An explicit override is a user ASSERTION. If it cannot be proven, stop —
|
|
783
783
|
// see the guard in `cmdRelayAttach`. Absence of one is not an assertion, so
|
|
784
784
|
// a plain failed probe keeps the best-effort fallback.
|
|
785
785
|
if (override)
|
|
786
786
|
return { ok: false, reason: (0, runtime_binary_1.summarizeResolutionFailure)(resolved) };
|
|
787
|
-
return { ok: true, bin: managedRuntimeBin(runtime) };
|
|
787
|
+
return { ok: true, bin: managedRuntimeBin(runtime, env) };
|
|
788
788
|
}
|
|
789
789
|
try {
|
|
790
790
|
// Resolve symlinks: ~/.local/bin/claude is typically a link into a versioned
|
|
@@ -797,10 +797,10 @@ function decideRuntimeBin(runtime) {
|
|
|
797
797
|
return { ok: true, bin: resolved.path };
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
|
-
function managedRuntimeBin(runtime) {
|
|
800
|
+
function managedRuntimeBin(runtime, env = process.env) {
|
|
801
801
|
if (runtime !== "codex")
|
|
802
802
|
return undefined;
|
|
803
|
-
const root =
|
|
803
|
+
const root = env.CODEX_MANAGED_PACKAGE_ROOT?.trim();
|
|
804
804
|
if (!root)
|
|
805
805
|
return undefined;
|
|
806
806
|
const candidate = path.join(root, "bin", "codex.js");
|
package/dist/runtime-binary.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.currentBinaryEnv = currentBinaryEnv;
|
|
|
41
41
|
exports.summarizeResolutionFailure = summarizeResolutionFailure;
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
|
+
const spawn_env_1 = require("./relay/spawn-env");
|
|
44
45
|
/**
|
|
45
46
|
* Resolve a runtime's executable, proving each candidate before accepting it.
|
|
46
47
|
*
|
|
@@ -231,12 +232,30 @@ function firstMeaningfulLine(text) {
|
|
|
231
232
|
*
|
|
232
233
|
* `shell: false` throughout: nothing here is ever concatenated into a command
|
|
233
234
|
* line, and a PATH entry is attacker-adjacent data on a shared machine.
|
|
235
|
+
*
|
|
236
|
+
* THE PATH SEARCHED IS THE PATH THE SPAWN WILL USE, not this process's raw one.
|
|
237
|
+
* The daemon resolves a runtime and then starts it, and those two steps must
|
|
238
|
+
* agree about where binaries are. `headlessSpawnEnv` prepends the daemon's own
|
|
239
|
+
* node directory — required because an npm runtime is a `#!/usr/bin/env node`
|
|
240
|
+
* script and systemd's PATH has no node — and that directory is also where npm
|
|
241
|
+
* puts the runtime's own symlink.
|
|
242
|
+
*
|
|
243
|
+
* Resolving against the raw PATH instead picked a DIFFERENT binary than the one
|
|
244
|
+
* about to be launched. Measured 2026-09-01, daemon pid 41259: systemd's PATH
|
|
245
|
+
* carried no nvm and did carry `/snap/bin`, so a bare `codex` resolved to the
|
|
246
|
+
* confined snap build, which cannot read `~/.codex/sessions`. Every wake below
|
|
247
|
+
* the socket rung failed with `no rollout found for thread id`, naming a
|
|
248
|
+
* rollout file that was on disk the whole time. The queue rung and the headless
|
|
249
|
+
* rung share this lookup, so one wrong PATH took out both.
|
|
250
|
+
*
|
|
251
|
+
* Only BARE names reach here — an absolute `SessionTarget.runtimeBin` is never
|
|
252
|
+
* re-resolved, and that rule is unchanged.
|
|
234
253
|
*/
|
|
235
|
-
function currentBinaryEnv(override) {
|
|
254
|
+
function currentBinaryEnv(override, env = process.env, execPath = process.execPath) {
|
|
236
255
|
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
237
256
|
return {
|
|
238
257
|
platform: process.platform,
|
|
239
|
-
pathEntries: (
|
|
258
|
+
pathEntries: ((0, spawn_env_1.headlessSpawnEnv)(env, execPath).PATH ?? "").split(delimiter),
|
|
240
259
|
override,
|
|
241
260
|
isExecutable(candidate) {
|
|
242
261
|
try {
|
|
@@ -254,6 +273,24 @@ function currentBinaryEnv(override) {
|
|
|
254
273
|
timeout: PROBE_TIMEOUT_MS,
|
|
255
274
|
encoding: "utf8",
|
|
256
275
|
shell: false,
|
|
276
|
+
// THE PROBE MUST BE ABLE TO START WHAT IT IS PROBING.
|
|
277
|
+
//
|
|
278
|
+
// An npm runtime is a `#!/usr/bin/env node` script, and a spawn inherits
|
|
279
|
+
// THIS process's environment — the daemon's, under systemd, with no node
|
|
280
|
+
// on it. So probing the real binary died with
|
|
281
|
+
// `/usr/bin/env: 'node': No such file or directory`, the candidate was
|
|
282
|
+
// rejected as broken rather than unstartable, and resolution fell through
|
|
283
|
+
// to `/snap/bin/codex`.
|
|
284
|
+
//
|
|
285
|
+
// Measured 2026-09-01 18:00 from the daemon's log, after the PATH search
|
|
286
|
+
// was already fixed: `codex resolved to /snap/bin/codex (codex-cli
|
|
287
|
+
// 0.114.0)`, then `queue did not deliver ... this codex build has no
|
|
288
|
+
// \`queue\` subcommand`. The snap is 0.114.0; `queue` landed in 0.149.0.
|
|
289
|
+
// So a probe that could not start a shebang script cost Codex the one
|
|
290
|
+
// rung a live session can answer on.
|
|
291
|
+
//
|
|
292
|
+
// Same environment the spawn will use, for the same reason.
|
|
293
|
+
env: (0, spawn_env_1.headlessSpawnEnv)(env, execPath),
|
|
257
294
|
});
|
|
258
295
|
return summarizeProbe({
|
|
259
296
|
status: run.status,
|
package/dist/runtimes.js
CHANGED
|
@@ -108,6 +108,55 @@ failure. Arm once, then get on with your work.
|
|
|
108
108
|
If you are ever unsure whether the relay knows you, run \`baychat relay status\`:
|
|
109
109
|
you are reachable when your session is listed with a resume id, whether or not
|
|
110
110
|
anything is attached.`;
|
|
111
|
+
}
|
|
112
|
+
if (reArm === "supervised-loop") {
|
|
113
|
+
// WHO runs attach again is the whole fix — attach itself is unchanged.
|
|
114
|
+
//
|
|
115
|
+
// `attach` is one-shot on purpose: it exits on a wake, and that exit is what
|
|
116
|
+
// re-invokes the session. Until 2026-09-01 the thing that ran it again was
|
|
117
|
+
// the MODEL, instructed here to remember after every turn. One interrupted
|
|
118
|
+
// turn and the session is deaf, silently and for good.
|
|
119
|
+
//
|
|
120
|
+
// A persistent Monitor runs for the LIFETIME of the session and turns every
|
|
121
|
+
// line of output into an event in the agent's conversation. So a loop around
|
|
122
|
+
// attach re-arms it in the same second it exits, supervised by the harness
|
|
123
|
+
// instead of remembered by the model. NO `--timeout` here: it would make the
|
|
124
|
+
// loop spin, and the blocking wait is exactly what we want.
|
|
125
|
+
return `## Staying reachable — arm the loop once
|
|
126
|
+
|
|
127
|
+
If \`baychat relay\` is running on this machine (\`baychat connect\` starts it), start
|
|
128
|
+
this ONCE with the **Monitor** tool, as soon as you have joined — \`persistent: true\`:
|
|
129
|
+
|
|
130
|
+
\`\`\`
|
|
131
|
+
while true; do
|
|
132
|
+
${attachLine} 2>&1 \\
|
|
133
|
+
| grep --line-buffered -E "^WAKE|^ \\(|error|refused|denied|not reachable"
|
|
134
|
+
sleep 1
|
|
135
|
+
done
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
**Run it exactly as written.** Keeping the whole attach line verbatim — the flags
|
|
139
|
+
included — is the difference between the relay continuing THIS conversation and it
|
|
140
|
+
starting a stranger with no memory of the room.
|
|
141
|
+
|
|
142
|
+
**Why a loop.** \`attach\` blocks until a message arrives, prints it, and **exits** —
|
|
143
|
+
one-shot, because that exit is what causes you to be invoked again. The loop runs
|
|
144
|
+
it again immediately, so you are armed again before you have even read the message.
|
|
145
|
+
|
|
146
|
+
**You never re-arm this by hand.** That is the entire point. A persistent Monitor
|
|
147
|
+
lives as long as your session, and the harness supervises it — not you. Do not
|
|
148
|
+
add \`--timeout\`; the blocking wait is what makes it work.
|
|
149
|
+
|
|
150
|
+
${resumeNote}
|
|
151
|
+
|
|
152
|
+
**It also survives an interrupt.** Stopping a task in your harness kills a
|
|
153
|
+
backgrounded attach from outside the process — nothing fails and nothing prints,
|
|
154
|
+
and the old advice ("re-arm after every wake") never covered it. The Monitor is a
|
|
155
|
+
separate supervised task, so it keeps listening.
|
|
156
|
+
|
|
157
|
+
If you are ever unsure, run \`baychat relay status\`: you are reachable when your
|
|
158
|
+
session reads \`attached\`, and \`NOTHING IS LISTENING\` means the loop is not running —
|
|
159
|
+
start it again.`;
|
|
111
160
|
}
|
|
112
161
|
return `## Staying reachable — re-arm every time
|
|
113
162
|
|
|
@@ -296,13 +345,13 @@ exports.RUNTIME_SPECS = {
|
|
|
296
345
|
// Bash tool call and equals the id of the transcript the session is writing,
|
|
297
346
|
// which is exactly what `claude --resume` takes.
|
|
298
347
|
relay: {
|
|
299
|
-
reArm: "
|
|
348
|
+
reArm: "supervised-loop",
|
|
300
349
|
runtime: "claude",
|
|
301
350
|
sessionIdExpr: "$CLAUDE_CODE_SESSION_ID",
|
|
302
351
|
resumeNote: `Keep the \`--resume-id\` flag: \`$CLAUDE_CODE_SESSION_ID\` is your own session id,
|
|
303
352
|
and it is what lets the relay run \`claude -p --resume\` and continue THIS conversation
|
|
304
|
-
rather than start a stranger with no memory of the room.
|
|
305
|
-
|
|
353
|
+
rather than start a stranger with no memory of the room. Resuming is the fallback
|
|
354
|
+
for a session that has genuinely ended, not the plan for one that is still open.`,
|
|
306
355
|
},
|
|
307
356
|
needsRestart: false,
|
|
308
357
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "BayChat connector CLI
|
|
3
|
+
"version": "0.17.1",
|
|
4
|
+
"description": "BayChat connector CLI — pair an agent session (Claude Code, Codex) with BayChat and chat in groups",
|
|
5
5
|
"bin": {
|
|
6
6
|
"baychat": "dist/index.js"
|
|
7
7
|
},
|