baychat 0.21.4 → 0.22.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.
Files changed (44) hide show
  1. package/README.md +74 -13
  2. package/dist/approve-hook.js +5 -2
  3. package/dist/claude-onboarding.js +6 -12
  4. package/dist/commands.js +11 -6
  5. package/dist/connect-claude.js +19 -4
  6. package/dist/connect-dsh.js +165 -0
  7. package/dist/connect-plan.js +47 -1
  8. package/dist/connect.js +40 -9
  9. package/dist/doctor-command.js +7 -0
  10. package/dist/doctor.js +94 -1
  11. package/dist/dsh-config.js +147 -0
  12. package/dist/index.js +37 -2
  13. package/dist/relay/acp/agents.js +186 -0
  14. package/dist/relay/acp/approval.js +67 -0
  15. package/dist/relay/acp/client.js +253 -0
  16. package/dist/relay/acp/commands.js +69 -0
  17. package/dist/relay/acp/daemon-glue.js +201 -0
  18. package/dist/relay/acp/dump-config.js +31 -0
  19. package/dist/relay/acp/modes.js +36 -0
  20. package/dist/relay/acp/permissions.js +42 -0
  21. package/dist/relay/acp/policy.js +137 -0
  22. package/dist/relay/acp/presence.js +87 -0
  23. package/dist/relay/acp/prompt.js +69 -0
  24. package/dist/relay/acp/runner.js +311 -0
  25. package/dist/relay/acp/sdk.js +19 -0
  26. package/dist/relay/acp/turn-queue.js +163 -0
  27. package/dist/relay/acp/types.js +2 -0
  28. package/dist/relay/commands.js +28 -0
  29. package/dist/relay/daemon.js +195 -0
  30. package/dist/relay/mailbox.js +1 -0
  31. package/dist/relay/profiles.js +22 -0
  32. package/dist/relay/registry.js +46 -1
  33. package/dist/relay/session-commands.js +89 -0
  34. package/dist/relay/session-reply.js +25 -0
  35. package/dist/relay/terminal-pane.js +197 -0
  36. package/dist/relay/types.js +0 -9
  37. package/dist/runtime-install.js +9 -2
  38. package/dist/runtimes.js +16 -0
  39. package/dist/session-command.js +16 -3
  40. package/dist/session-setup.js +38 -0
  41. package/dist/skill-bootstrap.js +47 -0
  42. package/dist/start-command.js +235 -0
  43. package/dist/update-command.js +201 -0
  44. package/package.json +3 -1
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_COMMANDS = void 0;
4
+ exports.parseSessionCommand = parseSessionCommand;
5
+ exports.sessionCommandHelp = sessionCommandHelp;
6
+ exports.noTerminalReason = noTerminalReason;
7
+ /** Claude has no `/new`; `/clear` is how it starts a fresh chat. */
8
+ const freshChat = (runtime) => (runtime === "codex" ? "/new" : "/clear");
9
+ exports.SESSION_COMMANDS = [
10
+ {
11
+ id: "clear",
12
+ trigger: "/clear",
13
+ summary: "Clear this session's context, exactly as typing /clear in its terminal does.",
14
+ keys: () => "/clear",
15
+ },
16
+ {
17
+ id: "new",
18
+ trigger: "/new",
19
+ summary: "Start a fresh chat in this session, keeping the terminal open.",
20
+ keys: freshChat,
21
+ },
22
+ // ── ACP sessions (run by the relay, no terminal) ──────────────────────────────────────
23
+ // Four ROWS rather than `/mode <x>`, on purpose. The rule above says the wire carries a NAME
24
+ // and never an argument; an enum argument would be the first crack in it. These select a row
25
+ // like every other command, type nothing, and are answered by the relay.
26
+ { id: "mode-chat", trigger: "/mode-chat", mode: "chat", summary: "Relay-run agents: BayChat tools only — nothing on the computer. Owner or anyone with command access." },
27
+ { id: "mode-read", trigger: "/mode-read", mode: "read", summary: "Relay-run agents: may read files, no commands, no writes. Owner or anyone with command access." },
28
+ { id: "mode-ask", trigger: "/mode-ask", mode: "ask", summary: "Relay-run agents: may run commands; writes ask on your phone first. Owner or anyone with command access." },
29
+ { id: "mode-full", trigger: "/mode-full", mode: "full", summary: "Relay-run agents: may write inside its folder without asking. Owner or anyone with command access." },
30
+ { id: "stop", trigger: "/stop", summary: "Relay-run agents: stop the turn that is running now." },
31
+ {
32
+ id: "exit",
33
+ trigger: "/exit",
34
+ summary: "Stop this session being reachable from the Bay. Its terminal stays open — this does not close it.",
35
+ },
36
+ {
37
+ id: "status",
38
+ trigger: "/status",
39
+ summary: "What this relay knows about the session: runtime, terminal, and whether it is live.",
40
+ },
41
+ {
42
+ id: "help",
43
+ trigger: "/help",
44
+ summary: "This list.",
45
+ },
46
+ ];
47
+ /**
48
+ * Is this message one of the commands, or is it chat?
49
+ *
50
+ * EXACT MATCH on the trimmed text, deliberately. "can you /clear please" is a sentence
51
+ * about a command and must reach the agent as one; anything looser would turn discussing
52
+ * the feature into using it. Case-insensitive because a phone keyboard capitalises the
53
+ * start of a line without being asked.
54
+ *
55
+ * Says nothing about whether the sender MAY run it — that is the server's answer, checked
56
+ * by the caller. Parsing and permission are kept apart so neither can be mistaken for the
57
+ * other.
58
+ */
59
+ function parseSessionCommand(text) {
60
+ const normalised = text.trim().toLowerCase();
61
+ if (!normalised.startsWith("/"))
62
+ return undefined;
63
+ return exports.SESSION_COMMANDS.find((command) => command.trigger === normalised);
64
+ }
65
+ /** The reply to `/help`, and the thing to say when a command cannot run. */
66
+ function sessionCommandHelp() {
67
+ return [
68
+ "Commands this session understands:",
69
+ ...exports.SESSION_COMMANDS.map((command) => ` ${command.trigger} — ${command.summary}`),
70
+ "",
71
+ "Only you, and anyone you have given permission to, can run them.",
72
+ ].join("\n");
73
+ }
74
+ /**
75
+ * The refusal for a session whose terminal cannot be typed into.
76
+ *
77
+ * NAMES THE FIX. A person whose session is not in a multiplexer has done nothing wrong and
78
+ * cannot be expected to know why a chat command needs one; "it did not work" would leave
79
+ * them with no next step, which is the failure this whole codebase keeps writing comments
80
+ * about.
81
+ */
82
+ function noTerminalReason(detail) {
83
+ return [
84
+ detail ??
85
+ "This session is not running inside a terminal multiplexer, so there is nothing to type into.",
86
+ "Start it with `baychat` next time and they will work — that runs your agent somewhere these commands can reach.",
87
+ "/status, /exit and /help work either way.",
88
+ ].join(" ");
89
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.postSessionReply = postSessionReply;
4
+ const api_1 = require("../api");
5
+ /**
6
+ * Say, in the room, what a session command did.
7
+ *
8
+ * ── WHY A REPLY IS PART OF THE FEATURE AND NOT A NICETY ───────────────────────────────
9
+ *
10
+ * Everything else the relay does is witnessed by the agent answering: you know the wake
11
+ * landed because a reply appears. A session command produces NO agent turn — `/clear` is
12
+ * absorbed by the runtime's own TUI and says nothing back — so without this the sender
13
+ * sees precisely the same thing whether the command cleared their session, refused because
14
+ * the terminal is not in tmux, or was silently ignored because the relay is a version
15
+ * behind. That is the failure mode this codebase keeps writing comments about, and it would
16
+ * be the default here.
17
+ *
18
+ * So every outcome is reported, refusals included, and the wording distinguishes them. The
19
+ * message is posted AS THE SESSION, which is right: the session is the thing that did (or
20
+ * did not do) it, and a reply from anyone else would be a second voice in the room
21
+ * explaining somebody's terminal.
22
+ */
23
+ async function postSessionReply(auth, conversationId, sessionName, text, request = api_1.apiRequest) {
24
+ await request(auth, "POST", `/api/device-api/conversations/${encodeURIComponent(conversationId)}/messages`, { session: sessionName, content: text });
25
+ }
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectPane = detectPane;
4
+ exports.verifyPane = verifyPane;
5
+ exports.typeIntoPane = typeIntoPane;
6
+ const child_process_1 = require("child_process");
7
+ const profiles_1 = require("./profiles");
8
+ /**
9
+ * Read the pane out of the environment — INSIDE the session, never in the daemon.
10
+ *
11
+ * Same principle as `runtimeBin` and `resumeId`: the process that is actually in the
12
+ * terminal is the only one that can answer, and the daemon guessing is how this codebase
13
+ * has been bitten before. `relay attach` runs in the session; its environment carries
14
+ * `$TMUX` and `$TMUX_PANE` (measured: `/tmp/tmux-1003/default,1381496,1` and `%1`, matching
15
+ * `display-message -p '#{pane_id}'` from outside).
16
+ *
17
+ * Every multiplexer we can RECOGNISE is returned, including ones we cannot yet type into.
18
+ * That is deliberate: "you are in screen, and tmux is what this supports" is a far better
19
+ * answer than the silence of not looking.
20
+ */
21
+ function detectPane(env = process.env) {
22
+ const tmux = env.TMUX;
23
+ const tmuxPane = env.TMUX_PANE;
24
+ if (tmux && tmuxPane) {
25
+ // "<socket>,<server pid>,<session>" — only the first field addresses the server.
26
+ const socket = tmux.split(",")[0];
27
+ if (socket)
28
+ return { kind: "tmux", socket, pane: tmuxPane };
29
+ }
30
+ if (env.STY)
31
+ return { kind: "screen", session: env.STY, window: env.WINDOW ?? "0" };
32
+ if (env.ZELLIJ)
33
+ return { kind: "zellij", pane: env.ZELLIJ_PANE_ID };
34
+ return undefined;
35
+ }
36
+ /** The binary whose name must still be in the pane for a send to be allowed. */
37
+ function expectedCommand(runtime) {
38
+ return (0, profiles_1.profileFor)(runtime)?.bin ?? runtime;
39
+ }
40
+ /**
41
+ * Is this pane still the session we recorded?
42
+ *
43
+ * ⚠️ THE CHECK THAT KEEPS THIS FROM BEING A REMOTE SHELL. A pane outlives the program in
44
+ * it: an agent that exited leaves a **shell prompt** on the same pane id, and typing into a
45
+ * shell is running a command. So the foreground command is compared against the runtime
46
+ * recorded at attach, and anything else — dead pane, different program, tmux not answering
47
+ * — is a refusal.
48
+ *
49
+ * `#{pane_current_command}` is the FOREGROUND process, which is what a keystroke would
50
+ * actually reach. Using it also means a pane sitting in a subprocess refuses rather than
51
+ * typing into that subprocess, which is fail-closed in the direction we want.
52
+ */
53
+ function verifyPane(handle, runtime, run = (file, args) => (0, child_process_1.execFileSync)(file, args, { encoding: "utf8", timeout: 5_000 }), ownerPid) {
54
+ if (handle.kind === "zellij") {
55
+ return {
56
+ ok: false,
57
+ reason: "this session is running in zellij, which cannot be typed into yet — tmux and screen can.",
58
+ };
59
+ }
60
+ if (handle.kind === "screen") {
61
+ // SCREEN CANNOT BE ASKED WHAT IS RUNNING IN A WINDOW. tmux answers
62
+ // `#{pane_current_command}`; screen has no equivalent, and `screen -ls` says only that
63
+ // a session exists. So the same question is put to the PROCESS instead: is the runtime
64
+ // this session recorded at attach still alive, and still that runtime?
65
+ //
66
+ // This is a weaker check than tmux's in one specific way, and it is worth naming: it
67
+ // proves the agent is RUNNING, not that it is the foreground program of that window.
68
+ // A human who switched that window to a shell would be typed at. The window number is
69
+ // pinned from `$WINDOW` precisely to keep that to the window the agent started in.
70
+ if (ownerPid === undefined) {
71
+ return { ok: false, reason: "this screen session did not record its runtime process, so it cannot be typed into safely." };
72
+ }
73
+ return runtimeStillRunning(ownerPid, runtime, run);
74
+ }
75
+ let out;
76
+ try {
77
+ out = run("tmux", [
78
+ "-S",
79
+ handle.socket,
80
+ "display-message",
81
+ "-p",
82
+ "-t",
83
+ handle.pane,
84
+ "#{pane_dead} #{pane_current_command}",
85
+ ]).trim();
86
+ }
87
+ catch (err) {
88
+ return {
89
+ ok: false,
90
+ reason: `tmux could not find pane ${handle.pane} any more (${errText(err)}).`,
91
+ };
92
+ }
93
+ const [dead, current] = out.split(/\s+/);
94
+ if (dead !== "0")
95
+ return { ok: false, reason: `pane ${handle.pane} has exited.` };
96
+ const want = expectedCommand(runtime);
97
+ if (current !== want) {
98
+ return {
99
+ ok: false,
100
+ reason: `pane ${handle.pane} is running ${current ?? "nothing"}, not ${want} — refusing to type into it.`,
101
+ };
102
+ }
103
+ return { ok: true };
104
+ }
105
+ /**
106
+ * Is `pid` still running, and still the runtime we recorded?
107
+ *
108
+ * Both halves matter. A pid alone proves nothing — pids are reused, and the reused one is
109
+ * exactly as likely to be a shell as anything else. The command name is what makes it an
110
+ * identification rather than a hope.
111
+ */
112
+ function runtimeStillRunning(pid, runtime, run) {
113
+ const want = expectedCommand(runtime);
114
+ let comm;
115
+ try {
116
+ comm = run("ps", ["-p", String(pid), "-o", "comm="]).trim();
117
+ }
118
+ catch {
119
+ return { ok: false, reason: `the ${want} process for this session is no longer running.` };
120
+ }
121
+ if (!comm)
122
+ return { ok: false, reason: `the ${want} process for this session has exited.` };
123
+ // `comm` is truncated to 15 characters by the kernel, so compare on that basis rather
124
+ // than demanding equality — "claude" and "codex" are short, but a longer runtime name
125
+ // would otherwise never match itself.
126
+ if (comm !== want && !want.startsWith(comm)) {
127
+ return { ok: false, reason: `that process is ${comm}, not ${want} — refusing to type into it.` };
128
+ }
129
+ return { ok: true };
130
+ }
131
+ /**
132
+ * ⚠️ Characters that must never reach a multiplexer's "type this" command.
133
+ *
134
+ * `screen -X stuff` parses `^X` as a control character and `\` as an escape, so a string
135
+ * containing either would send something other than itself. Our text comes from a fixed
136
+ * table and contains neither — this refuses anyway, because the day somebody adds a row
137
+ * with a backslash in it is the day that stops being true, and a test failing is a much
138
+ * better outcome than a control character being typed into a terminal.
139
+ */
140
+ const UNSAFE_TO_TYPE = /[\^\\\r\n\x00-\x1f]/;
141
+ /**
142
+ * Type one line into the pane and press Enter.
143
+ *
144
+ * ⚠️ `-l` IS LOAD-BEARING. Without it `send-keys` reads its arguments as KEY NAMES, so text
145
+ * containing `C-c` or `Escape` would be a signal rather than characters. With it the string
146
+ * is sent literally and Enter is a separate, deliberate key.
147
+ *
148
+ * `text` must come from the command table in `session-commands.ts` and NEVER from a chat
149
+ * message. That is the rule the whole feature rests on: the wire carries a command NAME,
150
+ * and the keystrokes are ours. There is deliberately no argument here that a message could
151
+ * reach.
152
+ */
153
+ async function typeIntoPane(handle, runtime, text, deps = {}) {
154
+ // Checked BEFORE the pane, so a bad row fails on every machine rather than only on the
155
+ // multiplexer that would have mangled it.
156
+ if (UNSAFE_TO_TYPE.test(text)) {
157
+ return { ok: false, reason: "that command contains characters that must never be typed into a terminal." };
158
+ }
159
+ const verify = deps.verify ?? verifyPane;
160
+ const check = verify(handle, runtime, undefined, deps.ownerPid);
161
+ if (!check.ok)
162
+ return check;
163
+ const exec = deps.exec ?? runQuietly;
164
+ try {
165
+ if (handle.kind === "tmux") {
166
+ // `-l` sends the string LITERALLY. Without it send-keys reads its arguments as key
167
+ // NAMES, so text containing `C-c` would be a signal rather than characters.
168
+ await exec("tmux", ["-S", handle.socket, "send-keys", "-t", handle.pane, "-l", "--", text]);
169
+ await exec("tmux", ["-S", handle.socket, "send-keys", "-t", handle.pane, "Enter"]);
170
+ return { ok: true };
171
+ }
172
+ if (handle.kind === "screen") {
173
+ // `-p <window>` pins the window the agent started in; screen's "current window" is
174
+ // whichever the human last looked at, which is not the same thing.
175
+ //
176
+ // Enter goes as its own `stuff` of `^M`, which screen reads as a carriage return.
177
+ // That is the one place a `^` is deliberate — and it is ours, never a message's:
178
+ // `UNSAFE_TO_TYPE` above guarantees the text itself carries none.
179
+ const at = ["-S", handle.session, "-p", handle.window, "-X", "stuff"];
180
+ await exec("screen", [...at, text]);
181
+ await exec("screen", [...at, "^M"]);
182
+ return { ok: true };
183
+ }
184
+ return { ok: false, reason: `${handle.kind} cannot be typed into yet.` };
185
+ }
186
+ catch (err) {
187
+ return { ok: false, reason: `${handle.kind} refused the keystrokes: ${errText(err)}` };
188
+ }
189
+ }
190
+ function runQuietly(file, args) {
191
+ return new Promise((resolve, reject) => {
192
+ (0, child_process_1.execFile)(file, args, { timeout: 5_000 }, (err) => (err ? reject(err) : resolve()));
193
+ });
194
+ }
195
+ function errText(err) {
196
+ return err instanceof Error ? err.message : String(err);
197
+ }
@@ -1,11 +1,2 @@
1
1
  "use strict";
2
- /**
3
- * Shared types for the relay daemon.
4
- *
5
- * The relay's job is narrow: hold `/updates`, decide which local session an
6
- * event belongs to, and wake that session — once, never twice concurrently.
7
- * It deliberately does NOT decide whether the session should reply. That
8
- * judgement is the server's (`shouldRespond`) and the woken session's; the
9
- * relay only carries the message across the process boundary.
10
- */
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -39,6 +39,7 @@ const fs = __importStar(require("fs"));
39
39
  const os = __importStar(require("os"));
40
40
  const path = __importStar(require("path"));
41
41
  const runtimes_1 = require("./runtimes");
42
+ const skill_bootstrap_1 = require("./skill-bootstrap");
42
43
  /**
43
44
  * Install a runtime's BayChat skill/command.
44
45
  *
@@ -61,11 +62,17 @@ function installRuntimeCommand(runtime, home = os.homedir()) {
61
62
  needsRestart: spec.needsRestart,
62
63
  };
63
64
  if (!spec.command) {
64
- return { ...base, written: null, skipped: spec.fallback ?? "no command mechanism for this runtime" };
65
+ return {
66
+ ...base,
67
+ written: null,
68
+ skipped: spec.fallback ?? "no command mechanism for this runtime",
69
+ };
65
70
  }
66
71
  const dir = path.join(home, spec.command.dir);
67
72
  const file = path.join(dir, spec.command.file);
68
- const body = spec.command.render((0, runtimes_1.commandContextFor)(runtime));
73
+ const body = runtime === "codex" || runtime === "claude"
74
+ ? (0, skill_bootstrap_1.renderSkillBootstrap)(runtime)
75
+ : spec.command.render((0, runtimes_1.commandContextFor)(runtime));
69
76
  fs.mkdirSync(dir, { recursive: true });
70
77
  // Back up a hand-edited skill rather than silently overwriting it — the user
71
78
  // may have tuned the room rules for their own setup.
package/dist/runtimes.js CHANGED
@@ -31,6 +31,7 @@ const help_topics_1 = require("./help-topics");
31
31
  const tool_defs_1 = require("./tool-defs");
32
32
  const claude_onboarding_1 = require("./claude-onboarding");
33
33
  const codex_onboarding_1 = require("./codex-onboarding");
34
+ const session_setup_1 = require("./session-setup");
34
35
  exports.RUNTIMES = [
35
36
  "claude",
36
37
  "codex",
@@ -38,6 +39,7 @@ exports.RUNTIMES = [
38
39
  "desktop",
39
40
  "pi",
40
41
  "hermes",
42
+ "dsh",
41
43
  "generic",
42
44
  ];
43
45
  const GENERIC_RESUME_NOTE = `The relay can only wake this session while \`attach\` is running. Re-arm it after
@@ -202,6 +204,8 @@ Answering in the wrong room is the worst failure this feature has.
202
204
 
203
205
  ${help_topics_1.ROOMS_TOPIC}
204
206
 
207
+ ${ctx.runtime === "claude" || ctx.runtime === "codex" ? (0, session_setup_1.sessionSetupSteps)(ctx.runtime) : ""}
208
+
205
209
  ## Steps
206
210
 
207
211
  ${ctx.runtime === "claude"
@@ -480,6 +484,18 @@ DELIVERY PENDING and waits for a human — re-arm attach after every wake, witho
480
484
  fallback: "Hermes is a self-hosted agent: it authenticates with its own agent token and long-polls for messages, so there is nothing to install on this machine. Pair it from the app, then add BayChat under `mcp_servers:` in ~/.hermes/config.yaml to give it tools.",
481
485
  needsRestart: false,
482
486
  },
487
+ dsh: {
488
+ id: "dsh",
489
+ label: "DeepSeek Harness",
490
+ mcp: { kind: "manual", describe: "merged into ~/.dsh/cordis.patch.yml by `baychat connect dsh`" },
491
+ // ~/.dsh/skills is rank 400 in dsh's skill search and ~/.agents/skills is 500 [source], so
492
+ // this copy SHADOWS a Codex skill a client already has — which would tell dsh to attach as
493
+ // `--runtime codex`. It must be written here, never shared with the Codex file.
494
+ command: { dir: ".dsh/skills/baychat", file: "SKILL.md", frontmatter: true, render: (ctx) => renderCommand(ctx, true) },
495
+ invocation: '/baychat <name> ["<Group Title>"]',
496
+ relay: { runtime: "dsh", reArm: "background-every-wake", resumeNote: GENERIC_RESUME_NOTE },
497
+ needsRestart: true,
498
+ },
483
499
  generic: {
484
500
  id: "generic",
485
501
  label: "Generic MCP client",
@@ -7,6 +7,7 @@ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamable
7
7
  const config_1 = require("./config");
8
8
  const session_name_1 = require("./session-name");
9
9
  const owner_pid_1 = require("./relay/owner-pid");
10
+ const terminal_pane_1 = require("./relay/terminal-pane");
10
11
  const profiles_1 = require("./relay/profiles");
11
12
  const message_format_1 = require("./relay/message-format");
12
13
  const commands_1 = require("./relay/commands");
@@ -69,15 +70,16 @@ async function cmdJoinSession(args) {
69
70
  const joining = Boolean(options.session || options.group || options.destination);
70
71
  const runtime = options.runtime ?? (0, owner_pid_1.detectRuntime)(profiles_1.RUNTIME_PROFILES);
71
72
  if (joining && !runtime)
72
- throw new Error("Cannot detect this runtime. Pass --runtime codex or --runtime claude.");
73
+ throw new Error("Cannot detect this runtime. Pass --runtime codex, --runtime claude, --runtime cursor, or --runtime dsh.");
73
74
  if (runtime === "hermes") {
74
75
  throw new Error("Hermes is a persistent agent. Use baychat connect hermes.");
75
76
  }
76
77
  if (joining &&
77
78
  runtime !== "codex" &&
78
79
  runtime !== "claude" &&
79
- runtime !== "cursor") {
80
- throw new Error("Supported coding runtimes: codex, claude, cursor.");
80
+ runtime !== "cursor" &&
81
+ runtime !== "dsh") {
82
+ throw new Error("Supported coding runtimes: codex, claude, cursor, dsh.");
81
83
  }
82
84
  let name = joining
83
85
  ? (options.session ?? (await (0, session_name_1.automaticSessionName)(runtime)))
@@ -189,6 +191,17 @@ async function cmdJoinSession(args) {
189
191
  return 1;
190
192
  }
191
193
  console.log("Connecting incoming messages…");
194
+ // SAID AT JOIN, NOT AT FAILURE. `/clear` and `/new` from the app work by typing into
195
+ // this session's terminal, which needs a multiplexer pane to address (see
196
+ // relay/terminal-pane.ts). Somebody who learns that only when a command refuses has
197
+ // already been let down once; one line here is the whole difference.
198
+ // ONE LINE, AND ONLY WHEN IT IS ACTIONABLE. `baychat start` puts an agent somewhere these
199
+ // commands can reach without anybody learning what a multiplexer is, so the note names
200
+ // the one word that fixes it and never the machinery underneath.
201
+ const pane = (0, terminal_pane_1.detectPane)();
202
+ if (!pane) {
203
+ console.log("Note: /clear and /new from the app cannot reach this session. Start it with `baychat` next time and they will. Everything else works either way.");
204
+ }
192
205
  const attach = () => (0, commands_1.cmdRelayAttach)({
193
206
  session: name,
194
207
  runtime: runtime,
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sessionSetupSteps = sessionSetupSteps;
4
+ /** One setup policy for both coding clients; their delivery adapters remain distinct. */
5
+ function sessionSetupSteps(runtime) {
6
+ const reload = runtime === "claude"
7
+ ? "Open /mcp, select baychat and Reconnect. If tools are still missing, restart Claude and resume this conversation."
8
+ : "Restart Codex and resume this conversation to load the configured BayChat tools.";
9
+ return `## Automatic setup — the session does it
10
+
11
+ If the required tools are already available, skip setup. Use the live remote MCP
12
+ connection; an expired local login alone does not mean that connection is broken.
13
+ For Codex the required chat tools are \`get_messages\` and \`send_message\`;
14
+ Claude also needs the native listener tools listed in its join steps below.
15
+ Search deferred tools once when needed, before creating membership.
16
+
17
+ If required BayChat tools are missing, or the owner asks to configure this runtime,
18
+ run \`baychat connect ${runtime} --start\` yourself in the session's terminal, once.
19
+ Do not ask the user to run connect. It reuses a valid device login and configures
20
+ MCP plus this skill. With no usable login it prints an approval link and exits 2;
21
+ show that link immediately and wait for approval, without a waiting terminal.
22
+ After approval run \`baychat connect ${runtime} --finish\` once. If still pending,
23
+ show the link and wait; do not poll. Report other failures briefly and stop.
24
+
25
+ After setup succeeds, use the host's reconnect facility if exposed, then search
26
+ the deferred tools once more. If they remain unavailable, show this one action:
27
+ "${reload}" Keep the original session name and group for the retry.
28
+ Do not restart the host yourself, extract tokens, hand-write JSON-RPC, or run a
29
+ local MCP server as a workaround. A host reload is not another login.
30
+
31
+ For an actual authentication refusal, first use the connection's own authorization
32
+ link if provided. For a CLI device login, use the same --start/--finish flow above.
33
+ Do not infer a remote authentication failure from a local credential file.
34
+
35
+ Confirm a successful join in one line: the name, room and actual delivery state.
36
+ Keep routine setup narration out of chat; explain only a failure or required action.
37
+ `;
38
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderSkillBootstrap = renderSkillBootstrap;
4
+ /** Keep the installed entry point stable while npm supplies the current workflow. */
5
+ function renderSkillBootstrap(runtime) {
6
+ const invocation = runtime === "codex" ? "$baychat" : "/baychat";
7
+ const tools = runtime === "claude"
8
+ ? "allowed-tools:\n - Bash\n - Monitor\n - mcp__baychat\n"
9
+ : "";
10
+ return `---
11
+ name: baychat
12
+ description: Join this coding session to BayChat, chat with agents, or update BayChat from this session.
13
+ ${tools}---
14
+
15
+ # BayChat
16
+
17
+ Use \`${invocation} <name> --group "<title>"\` to join, or
18
+ \`${invocation} --update\` to update. Preserve the user's name and group literally.
19
+
20
+ For each join or other non-update request, run \`baychat skill --runtime ${runtime}\` in the session's
21
+ terminal and follow the current workflow it returns. This reads local package
22
+ instructions; it does not log in, join a room, or start a receiver. Do not inspect
23
+ installed source or credential files. Package updates supply the new workflow
24
+ automatically; there is no separate skill-refresh command for the user.
25
+
26
+ For an update request (\`--update\`), run \`baychat update\` and report what it prints.
27
+ That one command does every part: the npm package, every connected client's config, and
28
+ a relay restart. The relay is the step that matters most and the easiest to miss — a
29
+ running daemon holds its old code in memory, so an npm install alone leaves the machine
30
+ on the previous version with nothing saying so.
31
+
32
+ Do not run npm yourself, and do not run the steps separately. A sequence the model has
33
+ to remember is a sequence the model will one day do half of; \`baychat update\` is
34
+ idempotent, refreshes only what is already connected, and never sets anything up.
35
+ If BayChat is not installed at all, install it once with
36
+ \`npm install --global baychat@latest\` and retry. Stop on failure with its short error;
37
+ do not retry installs or change npm permissions.
38
+
39
+ On an update-only request, stop after reporting — do not load the join workflow, join, or
40
+ send messages. If a join was also requested, load the new workflow afterwards and keep the
41
+ original name and group.
42
+
43
+ Missing MCP setup is handled by the returned workflow inside this session.
44
+ Do not ask the user to run connect. Authentication approvals and host reloads
45
+ may need the user; show the exact link or single required action promptly.
46
+ `;
47
+ }