baychat 0.14.0 → 0.16.0
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/README.md +25 -0
- package/dist/args.js +15 -0
- package/dist/doctor.js +21 -2
- package/dist/index.js +14 -1
- package/dist/relay/adapters.js +110 -10
- package/dist/relay/commands.js +322 -29
- package/dist/relay/daemon.js +333 -43
- package/dist/relay/held.js +184 -0
- package/dist/relay/mailbox.js +1 -0
- package/dist/relay/owner-pid.js +169 -0
- package/dist/relay/profiles.js +210 -0
- package/dist/relay/provider-env.js +180 -0
- package/dist/relay/registry.js +11 -0
- package/dist/relay/socket.js +48 -0
- package/dist/runtime-binary.js +14 -0
- package/dist/runtimes.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,6 +131,31 @@ history, which the relay searches in both places.
|
|
|
131
131
|
If you hit something else, `baychat doctor --json` plus what you expected is
|
|
132
132
|
everything we need.
|
|
133
133
|
|
|
134
|
+
## Which agents can use this?
|
|
135
|
+
|
|
136
|
+
**Probably yours.** BayChat reaches an agent on two levels, and the first one asks almost nothing:
|
|
137
|
+
|
|
138
|
+
- **Level 1 — while it is listening.** The agent runs one command and waits; a message is handed
|
|
139
|
+
straight over. Any agent that can run a shell command and wait qualifies.
|
|
140
|
+
- **Level 2 — when it is *not* listening.** BayChat restarts the agent and drops it back into the
|
|
141
|
+
right conversation. This needs the agent to be able to say which session it is, and to have a
|
|
142
|
+
way to resume that session without a UI (`--resume <id>` and friends).
|
|
143
|
+
|
|
144
|
+
Adapters ship for **Claude Code**, **Codex**, **Cursor** and **Hermes**. Many others — Gemini CLI,
|
|
145
|
+
Copilot CLI, Goose, OpenCode/Crush, Qwen Code, Kimi Code CLI, CodeBuddy, iFlow, Trae, Aider — look
|
|
146
|
+
compatible on paper, with per-agent detail, exact flags, known bugs and a **date on every row** in
|
|
147
|
+
[`RUNTIME_COMPATIBILITY.md`](https://github.com/SeaQuestdev/BayChat/blob/main/docs/features/RUNTIME_COMPATIBILITY.md).
|
|
148
|
+
|
|
149
|
+
**Using a different model inside one of those agents changes nothing.** GLM, DeepSeek, MiniMax,
|
|
150
|
+
Kimi and others ship Anthropic-compatible endpoints, and people run them inside Claude Code. The
|
|
151
|
+
relay wakes a *program* — it has no concept of which model is behind it. Point your agent at your
|
|
152
|
+
provider, then join a Bay normally.
|
|
153
|
+
|
|
154
|
+
> That compatibility table is **read from vendor documentation**, not run by us, for everything
|
|
155
|
+
> except the four with adapters — and these projects move fast. If a row is wrong,
|
|
156
|
+
> [tell us](https://github.com/SeaQuestdev/BayChat/issues) rather than assuming it cannot work.
|
|
157
|
+
> Treat any row older than about three months as unverified.
|
|
158
|
+
|
|
134
159
|
## Relay
|
|
135
160
|
|
|
136
161
|
A Claude Code or Codex session has **no background listener**. It runs when a
|
package/dist/args.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
28
|
exports.flag = flag;
|
|
29
29
|
exports.positional = positional;
|
|
30
|
+
exports.rejectUnknownFlags = rejectUnknownFlags;
|
|
30
31
|
/**
|
|
31
32
|
* The value of `--name`, or undefined when it was not given one.
|
|
32
33
|
*
|
|
@@ -55,3 +56,17 @@ function flag(args, name) {
|
|
|
55
56
|
function positional(args) {
|
|
56
57
|
return args.find((a) => !a.startsWith("--"));
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Refuse a flag this subcommand does not implement.
|
|
61
|
+
*
|
|
62
|
+
* Silently ignoring one is the worst option available: `relay status --json`
|
|
63
|
+
* printed the human table, exited as if it had honoured the request, and left
|
|
64
|
+
* the caller to discover the absence by parsing prose. A wrong answer that
|
|
65
|
+
* looks right survives far longer than an error.
|
|
66
|
+
*/
|
|
67
|
+
function rejectUnknownFlags(args, allowed, usage) {
|
|
68
|
+
const unknown = args.filter((a) => a.startsWith("--") && !allowed.includes(a));
|
|
69
|
+
if (unknown.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
throw new Error(`unknown option${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}\nUsage: ${usage}`);
|
|
72
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -410,7 +410,8 @@ function skillCheck(runtime, env) {
|
|
|
410
410
|
return { name: "skill", status: "skip", detail: spec.fallback ?? "no command mechanism" };
|
|
411
411
|
}
|
|
412
412
|
const file = `${env.home}/${spec.command.dir}/${spec.command.file}`;
|
|
413
|
-
|
|
413
|
+
const installed = env.readText(file);
|
|
414
|
+
if (installed === null) {
|
|
414
415
|
return {
|
|
415
416
|
name: "skill",
|
|
416
417
|
status: "fail",
|
|
@@ -418,7 +419,25 @@ function skillCheck(runtime, env) {
|
|
|
418
419
|
remedy: setupCommand(runtime),
|
|
419
420
|
};
|
|
420
421
|
}
|
|
421
|
-
|
|
422
|
+
// PRESENT IS NOT CURRENT. Existence was the whole check, so a skill installed
|
|
423
|
+
// months ago passed while telling its agent something this package has since
|
|
424
|
+
// corrected — and `npm publish` does not rewrite an installed file, so the
|
|
425
|
+
// stale copy simply stays. That is not hypothetical: the file on the machine
|
|
426
|
+
// where this was written still said Codex exports no thread id and should
|
|
427
|
+
// background its attach, both of which cost an afternoon of unreachability.
|
|
428
|
+
//
|
|
429
|
+
// A doc that is wrong is worse than one that is missing, because a missing one
|
|
430
|
+
// sends the agent looking.
|
|
431
|
+
const current = (0, runtimes_1.renderCommandFor)(runtime);
|
|
432
|
+
if (current !== null && installed.trim() !== current.trim()) {
|
|
433
|
+
return {
|
|
434
|
+
name: "skill",
|
|
435
|
+
status: "fail",
|
|
436
|
+
detail: `${display(file, env)} is out of date — it does not match the skill this version installs`,
|
|
437
|
+
remedy: setupCommand(runtime),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return { name: "skill", status: "pass", detail: `${display(file, env)} (current)` };
|
|
422
441
|
}
|
|
423
442
|
function binaryCheck(runtime, env) {
|
|
424
443
|
const command = HAS_BINARY[runtime];
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ const mcp_config_1 = require("./mcp-config");
|
|
|
10
10
|
const commands_2 = require("./relay/commands");
|
|
11
11
|
const help_topics_1 = require("./help-topics");
|
|
12
12
|
const args_1 = require("./args");
|
|
13
|
+
const owner_pid_1 = require("./relay/owner-pid");
|
|
14
|
+
const profiles_1 = require("./relay/profiles");
|
|
13
15
|
const HELP = `baychat — BayChat connector CLI for agent sessions (Claude Code, Codex)
|
|
14
16
|
|
|
15
17
|
Usage:
|
|
@@ -254,23 +256,34 @@ async function main() {
|
|
|
254
256
|
const rest = args.filter((a) => a !== sub);
|
|
255
257
|
switch (sub) {
|
|
256
258
|
case "start":
|
|
259
|
+
(0, args_1.rejectUnknownFlags)(rest, ["--foreground"], "baychat relay start [--foreground]");
|
|
257
260
|
await (0, commands_2.cmdRelayStart)({ foreground: args.includes("--foreground") });
|
|
258
261
|
return 0;
|
|
259
262
|
case "status":
|
|
263
|
+
(0, args_1.rejectUnknownFlags)(rest, [], "baychat relay status");
|
|
260
264
|
return await (0, commands_2.cmdRelayStatus)();
|
|
261
265
|
case "stop":
|
|
266
|
+
(0, args_1.rejectUnknownFlags)(rest, [], "baychat relay stop");
|
|
262
267
|
return await (0, commands_2.cmdRelayStop)();
|
|
263
268
|
case "attach": {
|
|
264
269
|
const session = (0, args_1.flag)(rest, "--session");
|
|
265
270
|
if (!session) {
|
|
266
271
|
throw new Error("Usage: baychat relay attach --session <name> [--runtime claude|codex|hermes] [--resume-id <id>] [--timeout <sec>]");
|
|
267
272
|
}
|
|
273
|
+
(0, args_1.rejectUnknownFlags)(rest, ["--session", "--runtime", "--resume-id", "--timeout", "--owner-pid"], "baychat relay attach --session <name> [--runtime claude|codex|hermes] [--resume-id <id>] [--timeout <sec>]");
|
|
268
274
|
const timeoutSec = numberFlag(rest, "--timeout");
|
|
269
275
|
return await (0, commands_2.cmdRelayAttach)({
|
|
270
276
|
session,
|
|
271
|
-
|
|
277
|
+
// Detected from the process tree when not given, so a person
|
|
278
|
+
// connecting Gemini or Kimi does not have to know the flag exists.
|
|
279
|
+
// Defaulting to "claude" for everyone was fine when four runtimes
|
|
280
|
+
// were all we served; it is a wrong answer now.
|
|
281
|
+
runtime: (0, args_1.flag)(rest, "--runtime") ?? (0, owner_pid_1.detectRuntime)(profiles_1.RUNTIME_PROFILES) ?? "claude",
|
|
272
282
|
resumeId: (0, args_1.flag)(rest, "--resume-id"),
|
|
273
283
|
timeoutMs: timeoutSec ? timeoutSec * 1000 : undefined,
|
|
284
|
+
// Passed by `relay rearm`, which reads it where the runtime is still
|
|
285
|
+
// an ancestor and hands it to a child that has been detached.
|
|
286
|
+
ownerPid: numberFlag(rest, "--owner-pid"),
|
|
274
287
|
});
|
|
275
288
|
}
|
|
276
289
|
default:
|
package/dist/relay/adapters.js
CHANGED
|
@@ -10,6 +10,7 @@ const attachments_1 = require("../attachments");
|
|
|
10
10
|
const codex_app_server_1 = require("./codex-app-server");
|
|
11
11
|
const codex_queue_1 = require("./codex-queue");
|
|
12
12
|
const resume_1 = require("./resume");
|
|
13
|
+
const profiles_1 = require("./profiles");
|
|
13
14
|
const spawn_env_1 = require("./spawn-env");
|
|
14
15
|
/** How long a headless turn may run before the relay gives up on it. */
|
|
15
16
|
const HEADLESS_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -64,8 +65,24 @@ function buildWakePrompt(session, conversationId, batch, reArm) {
|
|
|
64
65
|
function reArmLines(session, reArm) {
|
|
65
66
|
if (!reArm)
|
|
66
67
|
return [];
|
|
67
|
-
const
|
|
68
|
-
|
|
68
|
+
const attach = `${reArm.node} ${reArm.cli} relay attach --session "${session}" --runtime ${reArm.runtime}`;
|
|
69
|
+
const command = ` ${attach}`;
|
|
70
|
+
// BOUNDED, and it has to be. A foreground attach blocks until a message
|
|
71
|
+
// arrives — `cmdRelayAttach` installs a timer only when `--timeout` is given,
|
|
72
|
+
// and the mailbox path blocks in `readFile(fifo)` with no deadline at all. Told
|
|
73
|
+
// to run that unbounded inside a HEADLESS turn, the session cannot finish, and
|
|
74
|
+
// the daemon sits on it to its own 10-minute limit. Registration takes
|
|
75
|
+
// milliseconds; the wait is not what this is for. Same policy the skill
|
|
76
|
+
// generator states for `arm-once` — see `reachabilityFor` in runtimes.ts.
|
|
77
|
+
const boundedCommand = ` ${attach} --timeout 30`;
|
|
78
|
+
// FOREGROUND for the fifo rung — and for Codex on ANY transport.
|
|
79
|
+
//
|
|
80
|
+
// Branching on transport alone was not enough: a detached Codex whose recorded
|
|
81
|
+
// transport is "socket" or absent fell through to the background text, which
|
|
82
|
+
// is the instruction that started this whole outage. Codex's sandbox kills a
|
|
83
|
+
// backgrounded process when the tool command returns, so the runtime — not the
|
|
84
|
+
// rung — decides this.
|
|
85
|
+
if (reArm.transport === "fifo" || reArm.runtime === "codex") {
|
|
69
86
|
return [
|
|
70
87
|
"",
|
|
71
88
|
"To stay reachable, run this IN THE FOREGROUND before ending your turn. It",
|
|
@@ -73,7 +90,9 @@ function reArmLines(session, reArm) {
|
|
|
73
90
|
"Do NOT put it in the background: your sandbox kills backgrounded processes",
|
|
74
91
|
"when the command returns, so a backgrounded attach listens to nothing while",
|
|
75
92
|
"looking like it worked.",
|
|
76
|
-
command,
|
|
93
|
+
"`--timeout 30` is part of the command, not a suggestion: registering is the",
|
|
94
|
+
"point, and an unbounded wait would hold this turn open for no benefit.",
|
|
95
|
+
boundedCommand,
|
|
77
96
|
];
|
|
78
97
|
}
|
|
79
98
|
return [
|
|
@@ -253,18 +272,99 @@ const ADAPTERS = {
|
|
|
253
272
|
cursor: cursorAdapter,
|
|
254
273
|
hermes: hermesAdapter,
|
|
255
274
|
};
|
|
275
|
+
/**
|
|
276
|
+
* An agent we ship no knowledge of at all.
|
|
277
|
+
*
|
|
278
|
+
* Level 1 and honest about it. This exists so that an unrecognised runtime is
|
|
279
|
+
* SERVED rather than refused: while its attach is listening it is reached over
|
|
280
|
+
* the socket like anything else, and when it is not, the message is recorded
|
|
281
|
+
* pending with a reason a human can act on — instead of `relay attach` exiting
|
|
282
|
+
* 1 and the agent never joining at all.
|
|
283
|
+
*/
|
|
284
|
+
function unknownAdapter(runtime) {
|
|
285
|
+
return {
|
|
286
|
+
runtime,
|
|
287
|
+
canResume: () => ({
|
|
288
|
+
ok: false,
|
|
289
|
+
reason: `no resume is known for "${runtime}" — it is reachable while \`baychat relay attach\` is running, and a message arriving otherwise waits here. If it has a way to continue a session without a UI, tell us and we will ship it: https://github.com/SeaQuestdev/BayChat/issues`,
|
|
290
|
+
}),
|
|
291
|
+
discoverResume: async () => ({
|
|
292
|
+
ok: false,
|
|
293
|
+
reason: `no on-disk layout is known for "${runtime}", so its sessions cannot be identified from here`,
|
|
294
|
+
}),
|
|
295
|
+
headlessCommand() {
|
|
296
|
+
throw new Error(`${runtime} has no headless command`);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* An adapter built from a shipped PROFILE rather than hand-written code.
|
|
302
|
+
*
|
|
303
|
+
* The research behind `profiles.ts` found that the shape is universal and only
|
|
304
|
+
* the spelling differs — so for most runtimes an adapter is an argv template,
|
|
305
|
+
* and this turns one into the interface the daemon already speaks.
|
|
306
|
+
*/
|
|
307
|
+
function profileAdapter(profile) {
|
|
308
|
+
return {
|
|
309
|
+
runtime: profile.id,
|
|
310
|
+
canResume(target) {
|
|
311
|
+
if (!profile.headless) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
reason: `${profile.label} cannot be resumed without a UI: ${profile.noHeadlessReason} (checked ${profile.checked}, from ${profile.source} — if this has changed, tell us)`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// `session-name` runtimes are resumed by the name the human chose, which
|
|
318
|
+
// IS the BayChat session name — so there is nothing to discover and
|
|
319
|
+
// nothing that could name someone else's session.
|
|
320
|
+
const id = profile.sessionId.kind === "session-name" ? target.name : target.resumeId;
|
|
321
|
+
if (!id) {
|
|
322
|
+
return {
|
|
323
|
+
ok: false,
|
|
324
|
+
reason: `no ${profile.label} session id recorded — ${profile.sessionId.kind === "flag-only" ? profile.sessionId.how : "the session did not report one"}. Pass it with \`relay attach --resume-id\`.`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return { ok: true };
|
|
328
|
+
},
|
|
329
|
+
// Deliberately never guesses. A profile describes how to USE an id, not how
|
|
330
|
+
// to find one on disk, and "the newest session in this folder" is not an
|
|
331
|
+
// identification — it is how an agent ends up answering a room it has no
|
|
332
|
+
// memory of.
|
|
333
|
+
discoverResume: async () => ({
|
|
334
|
+
ok: false,
|
|
335
|
+
reason: `${profile.label} sessions cannot be identified from disk by this CLI — pass \`--resume-id\` at attach time`,
|
|
336
|
+
}),
|
|
337
|
+
headlessCommand(target, prompt) {
|
|
338
|
+
if (!profile.headless)
|
|
339
|
+
throw new Error(`${profile.id} has no headless command`);
|
|
340
|
+
const id = profile.sessionId.kind === "session-name" ? target.name : target.resumeId;
|
|
341
|
+
return {
|
|
342
|
+
file: target.runtimeBin ?? profile.bin,
|
|
343
|
+
args: (0, profiles_1.fillTemplate)(profile.headless.args, { id, prompt }),
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
256
348
|
function adapterFor(runtime) {
|
|
257
|
-
|
|
349
|
+
const builtin = ADAPTERS[runtime];
|
|
350
|
+
if (builtin)
|
|
351
|
+
return builtin;
|
|
352
|
+
const profile = (0, profiles_1.profileFor)(runtime);
|
|
353
|
+
return profile ? profileAdapter(profile) : unknownAdapter(runtime);
|
|
258
354
|
}
|
|
259
|
-
/**
|
|
260
|
-
exports.KNOWN_RUNTIMES = Object.keys(ADAPTERS);
|
|
355
|
+
/** The runtimes we ship knowledge of, for help text. NOT a list of what is accepted. */
|
|
356
|
+
exports.KNOWN_RUNTIMES = [...Object.keys(ADAPTERS), ...profiles_1.RUNTIME_PROFILES.map((p) => p.id)];
|
|
261
357
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
358
|
+
* Do we ship knowledge of this runtime?
|
|
359
|
+
*
|
|
360
|
+
* ⚠️ This is NO LONGER a gate on attaching — `relay attach` accepts any name and
|
|
361
|
+
* serves an unknown one at Level 1. It answers "will we do better than the
|
|
362
|
+
* floor for this?", which is a different question and must not be used to
|
|
363
|
+
* refuse. Refusing on a name is what kept agents out that needed nothing from
|
|
364
|
+
* us, and the Cursor outage was the same mistake from the other side.
|
|
265
365
|
*/
|
|
266
366
|
function isKnownRuntime(value) {
|
|
267
|
-
return Object.prototype.hasOwnProperty.call(ADAPTERS, value);
|
|
367
|
+
return Object.prototype.hasOwnProperty.call(ADAPTERS, value) || (0, profiles_1.profileFor)(value) !== undefined;
|
|
268
368
|
}
|
|
269
369
|
/**
|
|
270
370
|
* Run a headless turn to completion.
|