baychat 0.27.1 → 0.29.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 (53) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/README.md +42 -4
  3. package/dist/approve-hook.js +93 -3
  4. package/dist/channel/claude-link.js +145 -0
  5. package/dist/channel/detect.js +10 -2
  6. package/dist/channel/feed.js +27 -13
  7. package/dist/channel/launch.js +67 -12
  8. package/dist/channel/permission.js +118 -0
  9. package/dist/channel/plugin.js +182 -0
  10. package/dist/channel/relay-link.js +257 -0
  11. package/dist/channel/server.js +191 -10
  12. package/dist/claude-onboarding.js +7 -5
  13. package/dist/cli-help.js +235 -0
  14. package/dist/commands.js +4 -4
  15. package/dist/connect-acp.js +3 -2
  16. package/dist/connect-claude.js +31 -10
  17. package/dist/device-login.js +2 -2
  18. package/dist/doctor-command.js +2 -0
  19. package/dist/doctor.js +97 -3
  20. package/dist/help-topics.js +4 -3
  21. package/dist/launch/claude-trust.js +221 -0
  22. package/dist/launch/command.js +177 -0
  23. package/dist/launch/policy.js +152 -0
  24. package/dist/machine-name.js +76 -0
  25. package/dist/main.js +16 -170
  26. package/dist/multiplexer-launch.js +271 -0
  27. package/dist/protocol-content.js +1 -1
  28. package/dist/relay/acp/agents.js +42 -19
  29. package/dist/relay/acp/daemon-glue.js +3 -2
  30. package/dist/relay/acp/runner.js +8 -7
  31. package/dist/relay/channel-sessions.js +66 -0
  32. package/dist/relay/commands.js +9 -2
  33. package/dist/relay/daemon.js +160 -9
  34. package/dist/relay/launcher/approval-settings.js +140 -0
  35. package/dist/relay/launcher/argv.js +83 -0
  36. package/dist/relay/launcher/capabilities.js +76 -0
  37. package/dist/relay/launcher/dev-channel-screen.fixtures.js +41 -0
  38. package/dist/relay/launcher/dev-channel-screen.js +47 -0
  39. package/dist/relay/launcher/handle.js +303 -0
  40. package/dist/relay/launcher/loop.js +231 -0
  41. package/dist/relay/launcher/multiplexer.js +168 -0
  42. package/dist/relay/launcher/started.js +63 -0
  43. package/dist/relay/profiles.js +1 -1
  44. package/dist/relay/registry.js +26 -0
  45. package/dist/relay/terminal-pane.js +31 -2
  46. package/dist/relay/transport.js +7 -2
  47. package/dist/relay/ws.js +18 -1
  48. package/dist/runtime-install.js +19 -3
  49. package/dist/runtimes.js +3 -0
  50. package/dist/skill-bootstrap.js +1 -1
  51. package/dist/start-command.js +12 -56
  52. package/dist/tool-defs.js +23 -36
  53. package/package.json +4 -3
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.launchAgent = launchAgent;
37
+ exports.runHere = runHere;
38
+ exports.multiplexerArgv = multiplexerArgv;
39
+ exports.offerTmux = offerTmux;
40
+ exports.tmuxTip = tmuxTip;
41
+ exports.tmuxInstallerFor = tmuxInstallerFor;
42
+ exports.askLine = askLine;
43
+ exports.realLaunchDeps = realLaunchDeps;
44
+ exports.onPath = onPath;
45
+ /**
46
+ * Starting an agent where `/clear` from the phone can reach it — ONE launcher for `baychat start`
47
+ * and `baychat claude`, so the two cannot drift apart.
48
+ *
49
+ * ── WHY A MULTIPLEXER ─────────────────────────────────────────────────────────────────
50
+ *
51
+ * `/clear`, `/new`, `/compact` and the app's Fresh start work by TYPING into the session's
52
+ * terminal (relay/terminal-pane.ts), and only tmux or screen give a terminal an address that
53
+ * can be typed into. So when one is installed the agent starts inside it; when the person is
54
+ * already inside one, the agent just runs (nesting sends every prefix key to the wrong one).
55
+ *
56
+ * ── WHEN NEITHER IS INSTALLED (the owner's "A + C", 2026-10-05) ───────────────────────
57
+ *
58
+ * The agent ALWAYS starts. Before it does, one line says how to get the commands
59
+ * (`brew install tmux`, `sudo apt install tmux`, …), and — once, at a real terminal — the
60
+ * person is asked whether to install tmux now. A "no" is remembered in the config directory,
61
+ * so after that it is the one line and nothing else. Plain Windows has no tmux at all: nothing
62
+ * is said there.
63
+ *
64
+ * ── WHAT NEVER GOES INTO A MULTIPLEXER ───────────────────────────────────────────────
65
+ *
66
+ * Anything that is not an interactive session at a terminal: with stdin or stdout redirected,
67
+ * tmux refuses to start ("not a terminal"); and a one-shot command (`claude -p`, `claude mcp
68
+ * list`) would print into a tmux session that closes the moment it exits. The caller says
69
+ * which it is launching; this module only checks the terminal.
70
+ */
71
+ const child_process_1 = require("child_process");
72
+ const fs = __importStar(require("fs"));
73
+ const path = __importStar(require("path"));
74
+ const readline = __importStar(require("readline"));
75
+ const config_1 = require("./config");
76
+ const terminal_pane_1 = require("./relay/terminal-pane");
77
+ /**
78
+ * Start `program` where session commands can reach it — inside tmux or screen when possible —
79
+ * and return its exit status. Never refuses to start the agent: every failure on the way falls
80
+ * back to running it right here.
81
+ */
82
+ async function launchAgent(program, naming, deps = realLaunchDeps()) {
83
+ if (deps.platform === "win32" || !deps.atTerminal || (0, terminal_pane_1.detectPane)(deps.env))
84
+ return runHere(program, deps);
85
+ const multiplexer = firstWorking(deps) ?? (await offerTmux(deps));
86
+ if (!multiplexer)
87
+ return runHere(program, deps);
88
+ const argv = multiplexerArgv(multiplexer, naming, program, deps);
89
+ const result = deps.run(multiplexer, argv, { ...deps.env, ...program.env });
90
+ // Falling back rather than failing: the multiplexer is an enhancement, and refusing to start
91
+ // somebody's agent because of one would be the tail wagging the dog.
92
+ if (result.error)
93
+ return runHere(program, deps);
94
+ return result.status ?? 0;
95
+ }
96
+ /** Run the agent in this terminal, as it would run without BayChat. */
97
+ function runHere(program, deps = realLaunchDeps()) {
98
+ const result = deps.run(program.binary, program.args, { ...deps.env, ...program.env });
99
+ if (result.error) {
100
+ deps.say(`Could not start ${path.basename(program.binary)}: ${result.error.message}`);
101
+ return 1;
102
+ }
103
+ return result.status ?? 0;
104
+ }
105
+ /**
106
+ * The multiplexer's argv. The agent's own variables ride on `env`, because a tmux server that
107
+ * is already running starts new panes with ITS environment, not the client's — so the channel
108
+ * marker set only on the client would never reach Claude Code. `env` then execs the agent, so
109
+ * the pane's foreground program is still the agent itself (what the pane check looks for).
110
+ */
111
+ function multiplexerArgv(multiplexer, naming, program,
112
+ /** Needed only for a `fresh` name (its pid suffix and the tmux uniqueness check). */
113
+ deps) {
114
+ const vars = Object.entries(program.env).map(([k, v]) => `${k}=${v}`);
115
+ const command = vars.length ? ["env", ...vars, program.binary, ...program.args] : [program.binary, ...program.args];
116
+ if (naming.kind === "detached") {
117
+ return multiplexer === "tmux" ? ["new", "-d", "-s", naming.name, "-c", naming.cwd, ...command] : ["-dmS", naming.name, ...command];
118
+ }
119
+ if (!deps)
120
+ throw new Error("multiplexerArgv: a fresh or reattached session needs the launch deps");
121
+ if (multiplexer === "tmux") {
122
+ return naming.kind === "reattach"
123
+ ? // `-A` attaches to an existing session of that name instead of failing, so running this
124
+ // twice returns you to your agent rather than erroring at you.
125
+ ["new", "-A", "-s", naming.name, ...command]
126
+ : ["new", "-s", uniqueTmuxName(naming.prefix, deps), ...command];
127
+ }
128
+ // screen tells same-named sessions apart by pid, so a fresh one needs no uniqueness check.
129
+ return naming.kind === "reattach" ? ["-S", naming.name, "-R", ...command] : ["-S", `${naming.prefix}-${deps.pid}`, ...command];
130
+ }
131
+ /** `claude-<pid>`, or with a suffix when a tmux session of that name already exists. */
132
+ function uniqueTmuxName(prefix, deps) {
133
+ const base = `${prefix}-${deps.pid}`;
134
+ for (let n = 1; n < 100; n++) {
135
+ const name = n === 1 ? base : `${base}-${n}`;
136
+ if (!deps.tmuxHasSession(name))
137
+ return name;
138
+ }
139
+ return `${base}-${Date.now()}`;
140
+ }
141
+ /**
142
+ * The first multiplexer that RUNS, not the first on the PATH. The distinction has already cost
143
+ * this codebase a week: a probe that picked a binary which existed and could not start.
144
+ */
145
+ function firstWorking(deps) {
146
+ return ["tmux", "screen"].find((m) => deps.works(m));
147
+ }
148
+ /**
149
+ * Neither multiplexer is installed: say how to get one, and — once, at a terminal — offer to
150
+ * install tmux. Resolves "tmux" only when it was installed now and runs.
151
+ */
152
+ async function offerTmux(deps) {
153
+ const installer = deps.installer();
154
+ deps.say(tmuxTip(deps.platform, installer));
155
+ if (!installer || deps.declinedTmux())
156
+ return undefined;
157
+ const answer = await deps.ask("Install tmux now? (y/N) ");
158
+ if (!/^\s*y(es)?\s*$/i.test(answer)) {
159
+ deps.rememberDeclined();
160
+ return undefined;
161
+ }
162
+ const result = deps.run(installer.file, installer.args, deps.env);
163
+ if (result.error || result.status !== 0) {
164
+ deps.say(`tmux was not installed (${result.error?.message ?? `exit ${result.status}`}). Starting without it.`);
165
+ return undefined;
166
+ }
167
+ return deps.works("tmux") ? "tmux" : undefined;
168
+ }
169
+ /** The one line shown when no multiplexer is installed. */
170
+ function tmuxTip(platform, installer) {
171
+ const how = installer
172
+ ? `install it with \`${installer.display}\``
173
+ : platform === "darwin"
174
+ ? "install Homebrew (https://brew.sh), then `brew install tmux`"
175
+ : "install tmux with your system's package manager";
176
+ return `BayChat: /clear and /new from the app need tmux, which is not installed here — ${how}, and BayChat will start your sessions inside it.`;
177
+ }
178
+ /**
179
+ * How to install tmux on this computer, or undefined when there is no way BayChat knows to do
180
+ * it unattended: Homebrew on macOS (never with sudo), else apt, dnf or pacman through sudo —
181
+ * or directly when this already runs as root.
182
+ */
183
+ function tmuxInstallerFor(platform, onPath, isRoot) {
184
+ if (platform === "darwin") {
185
+ return onPath("brew") ? { file: "brew", args: ["install", "tmux"], display: "brew install tmux" } : undefined;
186
+ }
187
+ if (platform !== "linux")
188
+ return undefined;
189
+ const managers = [
190
+ { cmd: "apt-get", args: ["install", "-y", "tmux"], display: "apt install tmux" },
191
+ { cmd: "dnf", args: ["install", "-y", "tmux"], display: "dnf install tmux" },
192
+ { cmd: "pacman", args: ["-S", "--noconfirm", "tmux"], display: "pacman -S tmux" },
193
+ ];
194
+ const manager = managers.find((m) => onPath(m.cmd));
195
+ if (!manager)
196
+ return undefined;
197
+ if (isRoot)
198
+ return { file: manager.cmd, args: manager.args, display: manager.display };
199
+ if (!onPath("sudo"))
200
+ return undefined;
201
+ return { file: "sudo", args: [manager.cmd, ...manager.args], display: `sudo ${manager.display}` };
202
+ }
203
+ /** Ask one question at the terminal. */
204
+ function askLine(question) {
205
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
206
+ return new Promise((resolve) => rl.question(question, (answer) => {
207
+ rl.close();
208
+ resolve(answer);
209
+ }));
210
+ }
211
+ const TMUX_OFFER_FILE = "tmux-offer.json";
212
+ /**
213
+ * How each program is asked for its version. tmux has no `--version`: it prints its usage and
214
+ * exits 1 (tmux 3.4, measured 2026-10-05), which made the old probe skip an installed tmux and
215
+ * fall through to screen.
216
+ */
217
+ const VERSION_FLAG = { tmux: "-V" };
218
+ function realLaunchDeps() {
219
+ const offerPath = () => path.join((0, config_1.configDir)(), TMUX_OFFER_FILE);
220
+ return {
221
+ platform: process.platform,
222
+ env: process.env,
223
+ atTerminal: !!process.stdin.isTTY && !!process.stdout.isTTY,
224
+ pid: process.pid,
225
+ run: (file, args, env) => {
226
+ const r = (0, child_process_1.spawnSync)(file, args, { stdio: "inherit", env });
227
+ return { status: r.status, ...(r.error ? { error: r.error } : {}) };
228
+ },
229
+ works: (program) => {
230
+ const probe = (0, child_process_1.spawnSync)(program, [VERSION_FLAG[program] ?? "--version"], { stdio: "ignore" });
231
+ return !probe.error && probe.status === 0;
232
+ },
233
+ tmuxHasSession: (name) => (0, child_process_1.spawnSync)("tmux", ["has-session", "-t", `=${name}`], { stdio: "ignore" }).status === 0,
234
+ installer: () => tmuxInstallerFor(process.platform, (cmd) => onPath(cmd, process.env), process.getuid?.() === 0),
235
+ ask: askLine,
236
+ say: (line) => console.error(line),
237
+ declinedTmux: () => {
238
+ try {
239
+ return fs.existsSync(offerPath());
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ },
245
+ rememberDeclined: () => {
246
+ try {
247
+ fs.mkdirSync((0, config_1.configDir)(), { recursive: true });
248
+ fs.writeFileSync(offerPath(), JSON.stringify({ declinedAt: new Date().toISOString() }, null, 2));
249
+ }
250
+ catch (err) {
251
+ // Not remembering costs one more question next time — never worth failing a start for.
252
+ console.error(`(could not remember that answer: ${err instanceof Error ? err.message : String(err)})`);
253
+ }
254
+ },
255
+ };
256
+ }
257
+ /** Is an executable `cmd` on PATH? A file check only: nothing is run. */
258
+ function onPath(cmd, env) {
259
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) {
260
+ if (!dir)
261
+ continue;
262
+ try {
263
+ fs.accessSync(path.join(dir, cmd), fs.constants.X_OK);
264
+ return true;
265
+ }
266
+ catch {
267
+ // Not in this directory; the next one may have it.
268
+ }
269
+ }
270
+ return false;
271
+ }