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 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
- if (env.readText(file) === null) {
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
- return { name: "skill", status: "pass", detail: display(file, env) };
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
- runtime: (0, args_1.flag)(rest, "--runtime") ?? "claude",
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:
@@ -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 command = ` ${reArm.node} ${reArm.cli} relay attach --session "${session}" --runtime ${reArm.runtime}`;
68
- if (reArm.transport === "fifo") {
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
- return ADAPTERS[runtime];
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
- /** Every value `relay attach --runtime` accepts, for the error that lists them. */
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
- * Derived from the adapter table rather than restated, because a second copy of
263
- * this list is exactly how Cursor came to be told to attach with a value the
264
- * relay rejected. Having an adapter IS being attachable.
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.