baychat 0.34.1 → 0.35.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 +1 -1
  2. package/README.md +13 -2
  3. package/dist/api.js +30 -0
  4. package/dist/bays-command.js +6 -0
  5. package/dist/channel/feed.js +48 -4
  6. package/dist/channel/log-file.js +37 -0
  7. package/dist/channel/server.js +34 -4
  8. package/dist/claude-context-flush.js +31 -8
  9. package/dist/claude-context.js +19 -5
  10. package/dist/claude-onboarding.js +21 -8
  11. package/dist/cli-help.js +3 -0
  12. package/dist/client-paths.js +6 -1
  13. package/dist/client-scope.js +34 -0
  14. package/dist/commands.js +16 -8
  15. package/dist/connect-link.js +205 -0
  16. package/dist/connect-plan.js +2 -2
  17. package/dist/connect.js +8 -0
  18. package/dist/device-login.js +19 -0
  19. package/dist/launch/command.js +6 -3
  20. package/dist/main.js +18 -2
  21. package/dist/mcp-files.js +2 -0
  22. package/dist/mcp-register.js +2 -0
  23. package/dist/protocol-content.js +1 -1
  24. package/dist/refresh-clients.js +7 -0
  25. package/dist/relay/acp/approval.js +99 -30
  26. package/dist/relay/acp/daemon-glue.js +2 -1
  27. package/dist/relay/adapters.js +5 -1
  28. package/dist/relay/bay-relays.js +18 -5
  29. package/dist/relay/codex-allowances.js +113 -0
  30. package/dist/relay/codex-app-server.js +637 -157
  31. package/dist/relay/codex-approvals.js +390 -0
  32. package/dist/relay/codex-commands.js +61 -0
  33. package/dist/relay/codex-log.js +260 -0
  34. package/dist/relay/codex-runner.js +505 -0
  35. package/dist/relay/codex-version.js +50 -0
  36. package/dist/relay/daemon.js +190 -6
  37. package/dist/relay/fresh-start.js +2 -1
  38. package/dist/relay/launcher/capabilities.js +24 -9
  39. package/dist/relay/launcher/codex-launch.js +9 -2
  40. package/dist/relay/launcher/codex-relay-launch.js +80 -0
  41. package/dist/relay/launcher/handle.js +101 -30
  42. package/dist/relay/launcher/loop.js +5 -1
  43. package/dist/relay/launcher/stop-registered.js +124 -9
  44. package/dist/relay/registry.js +30 -0
  45. package/dist/relay/runtime-limits.js +167 -0
  46. package/dist/relay/spawn-env.js +40 -3
  47. package/dist/relay/terminal-pane.js +1 -0
  48. package/dist/sea-names.js +34 -0
  49. package/dist/session-command.js +3 -1
  50. package/dist/session-name.js +25 -2
  51. package/dist/skill-bootstrap.js +2 -0
  52. package/dist/tool-defs.js +30 -0
  53. package/package.json +1 -1
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ // What a relay-run Codex session is doing, written down where its owner can watch it (2026-10-23).
3
+ //
4
+ // A Codex the relay runs has no terminal: the TUI that used to show its commands, diffs and
5
+ // answers is gone, and the room only sees what Codex chooses to say. So the runner writes a
6
+ // plain-text log per session — `~/.baychat/codex-logs/<session>.log`, 0600 — and
7
+ // `baychat relay log <session> [-f]` shows it (in Node, so it works the same on Windows). A
8
+ // session started from the app also gets a `bc-<name>` tmux window running exactly that command,
9
+ // where tmux exists, so "attach and watch" still works.
10
+ //
11
+ // WHAT IS WRITTEN, AND WHAT IS NOT. Turn start (who woke it, how many messages, under which
12
+ // policy) and end (status, duration, tokens); what Codex said; each command with its folder, exit
13
+ // code and last 20 lines of output; each file change as paths, kinds and +/- counts; each card
14
+ // asked, answered or timed out; MCP calls as `server.tool` only — NEVER their arguments, which can
15
+ // carry message text and tokens; errors. Every line is stripped of ANSI escapes and C0/C1 control
16
+ // characters first: this is text Codex (and so, indirectly, anyone in the room) produced, and a
17
+ // terminal reading the log must not be steered by it.
18
+ //
19
+ // BOUNDED. At 5 MB the log is rotated to `<session>.log.1` (one generation kept).
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.CodexSessionLog = void 0;
55
+ exports.codexLogDir = codexLogDir;
56
+ exports.codexLogFile = codexLogFile;
57
+ exports.cleanLogText = cleanLogText;
58
+ exports.showCodexLog = showCodexLog;
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
61
+ const config_1 = require("../config");
62
+ const MAX_BYTES = 5 * 1024 * 1024;
63
+ const OUTPUT_TAIL_LINES = 20;
64
+ const TEXT_MAX = 4_000;
65
+ function codexLogDir() {
66
+ return path.join((0, config_1.configDir)(), "codex-logs");
67
+ }
68
+ /** A session name as a file name: never a path, never hidden, never empty. */
69
+ function codexLogFile(session, dir = codexLogDir()) {
70
+ const safe = session.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "_").slice(0, 80) || "session";
71
+ return path.join(dir, `${safe}.log`);
72
+ }
73
+ // ANSI CSI/OSC sequences, then every C0 control but tab and newline, DEL and C1.
74
+ const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)?|\u001B[@-Z\\-_]/g;
75
+ const CONTROL = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
76
+ /** Text safe to put in front of a terminal: escapes and control characters removed. */
77
+ function cleanLogText(text) {
78
+ return text.replace(ANSI, "").replace(/\r\n?/g, "\n").replace(CONTROL, "");
79
+ }
80
+ const clip = (text, max) => (text.length <= max ? text : `${text.slice(0, max - 1)}…`);
81
+ const str = (v) => (typeof v === "string" ? v : "");
82
+ class CodexSessionLog {
83
+ file;
84
+ maxBytes;
85
+ now;
86
+ tokens;
87
+ startedAt = 0;
88
+ broken = false;
89
+ constructor(session, opts = {}) {
90
+ this.file = codexLogFile(session, opts.dir);
91
+ this.maxBytes = opts.maxBytes ?? MAX_BYTES;
92
+ this.now = opts.now ?? (() => new Date());
93
+ }
94
+ /** One entry. Continuation lines are indented so an entry can never pass for another. NEVER THROWS. */
95
+ line(text) {
96
+ if (this.broken)
97
+ return;
98
+ const body = clip(cleanLogText(text), TEXT_MAX).replace(/\n/g, "\n ");
99
+ const entry = `${this.now().toISOString()} ${body}\n`;
100
+ try {
101
+ fs.mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 });
102
+ let size = 0;
103
+ try {
104
+ size = fs.statSync(this.file).size;
105
+ }
106
+ catch {
107
+ // Not there yet.
108
+ }
109
+ if (size + entry.length > this.maxBytes && size > 0)
110
+ fs.renameSync(this.file, `${this.file}.1`);
111
+ fs.appendFileSync(this.file, entry, { mode: 0o600 });
112
+ }
113
+ catch (err) {
114
+ // A disk that will not take the log must not take the turn with it. Said once, to stderr.
115
+ this.broken = true;
116
+ process.stderr.write(`baychat relay: cannot write ${this.file} — ${err instanceof Error ? err.message : String(err)}\n`);
117
+ }
118
+ }
119
+ turnStart(who, addressed, policy) {
120
+ this.startedAt = this.now().getTime();
121
+ this.tokens = undefined;
122
+ this.line(`── turn: ${addressed} message(s) from ${who} — ${policy}`);
123
+ }
124
+ turnEnd(status) {
125
+ const seconds = this.startedAt ? Math.round((this.now().getTime() - this.startedAt) / 1000) : 0;
126
+ this.line(`── turn ${status} after ${seconds}s${this.tokens !== undefined ? `, ${this.tokens} tokens in total on this thread` : ""}`);
127
+ }
128
+ /** A codex app-server notification, as far as the log cares. Never throws. */
129
+ notification(method, params) {
130
+ try {
131
+ this.describe(method, params);
132
+ }
133
+ catch {
134
+ // A shape this log did not expect: the turn matters more than the line.
135
+ }
136
+ }
137
+ describe(method, params) {
138
+ if (method === "thread/tokenUsage/updated") {
139
+ const usage = (params.tokenUsage ?? {});
140
+ const total = Number(usage.total?.totalTokens);
141
+ if (Number.isFinite(total))
142
+ this.tokens = total;
143
+ return;
144
+ }
145
+ if (method === "error") {
146
+ const error = (params.error ?? params);
147
+ this.line(`error: ${str(error.message) || "Codex reported an error"}`);
148
+ return;
149
+ }
150
+ if (method !== "item/completed")
151
+ return;
152
+ const item = (params.item ?? {});
153
+ switch (item.type) {
154
+ case "agentMessage":
155
+ if (str(item.text))
156
+ this.line(`codex: ${str(item.text)}`);
157
+ return;
158
+ case "commandExecution": {
159
+ const output = str(item.aggregatedOutput).replace(/\n+$/, "").split("\n").slice(-OUTPUT_TAIL_LINES).join("\n");
160
+ const exit = typeof item.exitCode === "number" ? `exit ${item.exitCode}` : str(item.status) || "ended";
161
+ this.line(`ran: ${str(item.command)} (in ${str(item.cwd)}) → ${exit}${output ? `\n${output}` : ""}`);
162
+ return;
163
+ }
164
+ case "fileChange": {
165
+ const changes = Array.isArray(item.changes) ? item.changes : [];
166
+ const parts = changes.map((c) => {
167
+ const change = (c ?? {});
168
+ const kind = str((change.kind ?? {}).type) || "update";
169
+ const lines = str(change.diff).replace(/\n$/, "").split("\n").filter((l, i, all) => !(all.length === 1 && l === ""));
170
+ // In 0.160 an add's or a delete's `diff` is the whole file, unprefixed: every line counts.
171
+ const plus = kind === "add" ? lines.length : kind === "delete" ? 0 : lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
172
+ const minus = kind === "delete" ? lines.length : kind === "add" ? 0 : lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
173
+ return `${kind} ${str(change.path)} (+${plus}/-${minus})`;
174
+ });
175
+ this.line(`files ${str(item.status) || "changed"}: ${parts.join(", ") || "(none listed)"}`);
176
+ return;
177
+ }
178
+ case "mcpToolCall":
179
+ // Server and tool only: the arguments can carry message text and secrets.
180
+ this.line(`tool: ${str(item.server)}.${str(item.tool)} → ${str(item.status) || "done"}`);
181
+ return;
182
+ default:
183
+ return;
184
+ }
185
+ }
186
+ }
187
+ exports.CodexSessionLog = CodexSessionLog;
188
+ /**
189
+ * `baychat relay log <session> [-f]`: print the session's log, and with `follow` keep printing
190
+ * what is added (a rotation starts over at the new file). Resolves when not following, or when
191
+ * `signal` aborts. Reads by polling — the same on every platform, including Windows.
192
+ */
193
+ async function showCodexLog(session, opts) {
194
+ const write = opts.write ?? ((text) => void process.stdout.write(text));
195
+ const file = codexLogFile(session, opts.dir);
196
+ const tailBytes = opts.tailBytes ?? 64 * 1024;
197
+ let position = 0;
198
+ const readFrom = (from) => {
199
+ let fd;
200
+ try {
201
+ const size = fs.statSync(file).size;
202
+ if (size < from)
203
+ from = 0; // rotated: the new file from its start
204
+ if (size === from)
205
+ return from;
206
+ fd = fs.openSync(file, "r");
207
+ const buffer = Buffer.alloc(size - from);
208
+ fs.readSync(fd, buffer, 0, buffer.length, from);
209
+ write(cleanLogText(buffer.toString("utf8")));
210
+ return size;
211
+ }
212
+ catch {
213
+ return from;
214
+ }
215
+ finally {
216
+ if (fd !== undefined)
217
+ fs.closeSync(fd);
218
+ }
219
+ };
220
+ let size = -1;
221
+ try {
222
+ size = fs.statSync(file).size;
223
+ }
224
+ catch {
225
+ if (!opts.follow) {
226
+ write(`No log for "${session}" on this computer yet (${file}).\n`);
227
+ return 1;
228
+ }
229
+ write(`Waiting for "${session}" to write its log (${file})…\n`);
230
+ }
231
+ if (size >= 0) {
232
+ // Start a whole line in, a screenful or so from the end.
233
+ position = Math.max(0, size - tailBytes);
234
+ if (position > 0) {
235
+ const head = Buffer.alloc(Math.min(4096, size - position));
236
+ const fd = fs.openSync(file, "r");
237
+ try {
238
+ fs.readSync(fd, head, 0, head.length, position);
239
+ }
240
+ finally {
241
+ fs.closeSync(fd);
242
+ }
243
+ const newline = head.indexOf(0x0a);
244
+ position += newline >= 0 ? newline + 1 : 0;
245
+ }
246
+ position = readFrom(position);
247
+ }
248
+ if (!opts.follow)
249
+ return 0;
250
+ await new Promise((resolve) => {
251
+ const timer = setInterval(() => {
252
+ position = readFrom(position);
253
+ }, opts.pollMs ?? 500);
254
+ opts.signal?.addEventListener("abort", () => {
255
+ clearInterval(timer);
256
+ resolve();
257
+ });
258
+ });
259
+ return 0;
260
+ }