baychat 0.33.3 → 0.34.1

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.
@@ -29,6 +29,8 @@
29
29
  // does not acquire privileges on their behalf.
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
31
  exports.runCodexTurn = runCodexTurn;
32
+ exports.readCodexRateLimits = readCodexRateLimits;
33
+ exports.parseCodexRateLimits = parseCodexRateLimits;
32
34
  const runtime_binary_1 = require("../runtime-binary");
33
35
  const spawn_env_1 = require("./spawn-env");
34
36
  /** How long a whole wake may take — resume, turn, and completion. */
@@ -248,3 +250,103 @@ function firstLine(text) {
248
250
  }
249
251
  return "";
250
252
  }
253
+ /**
254
+ * Read the Codex account's rate limits through `codex app-server` (`account/rateLimits/read`) —
255
+ * the one reader both the session-lifecycle batch and Phase 1's limits report use (2026-10-22, so
256
+ * the two never write it twice). One short-lived process, like a wake, and no thread is touched.
257
+ *
258
+ * Resolves null rather than throwing: a Codex too old for the method, not logged in, or not
259
+ * answering in time has no limits to show, and that is not an error anyone can act on.
260
+ */
261
+ function readCodexRateLimits(binaryPath, deps) {
262
+ const timeout = deps.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS;
263
+ return new Promise((resolve) => {
264
+ const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(binaryPath, process.platform), ["app-server"]);
265
+ if (!run.ok)
266
+ return resolve(null);
267
+ const child = deps.spawn(run.file, run.args, {
268
+ windowsHide: true,
269
+ windowsVerbatimArguments: run.verbatim,
270
+ env: (0, spawn_env_1.headlessSpawnEnv)(),
271
+ shell: false,
272
+ stdio: ["pipe", "pipe", "pipe"],
273
+ });
274
+ let settled = false;
275
+ let buffer = "";
276
+ const finish = (limits) => {
277
+ if (settled)
278
+ return;
279
+ settled = true;
280
+ clearTimeout(timer);
281
+ child.kill("SIGTERM");
282
+ setTimeout(() => child.kill("SIGKILL"), 5_000).unref();
283
+ resolve(limits);
284
+ };
285
+ const timer = setTimeout(() => finish(null), timeout);
286
+ const send = (message) => {
287
+ child.stdin?.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
288
+ };
289
+ child.stdout?.on("data", (chunk) => {
290
+ buffer += chunk.toString();
291
+ let newline = buffer.indexOf("\n");
292
+ while (newline >= 0) {
293
+ const line = buffer.slice(0, newline).trim();
294
+ buffer = buffer.slice(newline + 1);
295
+ newline = buffer.indexOf("\n");
296
+ let message;
297
+ try {
298
+ message = JSON.parse(line);
299
+ }
300
+ catch {
301
+ continue;
302
+ }
303
+ if (message.id === 0) {
304
+ if (message.error)
305
+ return finish(null);
306
+ send({ method: "initialized", params: {} });
307
+ send({ id: 1, method: "account/rateLimits/read", params: {} });
308
+ }
309
+ else if (message.id === 1) {
310
+ return finish(message.error ? null : parseCodexRateLimits(message.result));
311
+ }
312
+ }
313
+ });
314
+ // A child that died before reading: its stdin errors (EPIPE), which must not crash the relay.
315
+ child.stdin?.on("error", () => finish(null));
316
+ // Read and dropped, so a chatty Codex never fills the pipe and stalls before it answers.
317
+ child.stderr?.on("data", () => { });
318
+ child.on("error", () => finish(null));
319
+ child.on("close", () => finish(null));
320
+ send({ id: 0, method: "initialize", params: { clientInfo: { name: "baychat-relay", title: "BayChat relay", version: CLIENT_VERSION } } });
321
+ });
322
+ }
323
+ /**
324
+ * The method's answer, read defensively: `{rateLimits: {primary, secondary, planType}}`, each
325
+ * window `{usedPercent, windowDurationMins, resetsAt}` (resetsAt in unix seconds). snake_case
326
+ * spellings of older builds are read too. Anything unreadable is left out, never guessed.
327
+ */
328
+ function parseCodexRateLimits(raw) {
329
+ const outer = (raw ?? {});
330
+ const limits = ((outer.rateLimits ?? outer.rate_limits ?? outer) ?? {});
331
+ const windows = [];
332
+ for (const kind of ["primary", "secondary"]) {
333
+ const w = limits[kind];
334
+ if (!w || typeof w !== "object")
335
+ continue;
336
+ const used = Number(w.usedPercent ?? w.used_percent);
337
+ if (!Number.isFinite(used))
338
+ continue;
339
+ const minutes = Number(w.windowDurationMins ?? w.window_minutes);
340
+ const resets = Number(w.resetsAt ?? w.resets_at);
341
+ windows.push({
342
+ kind,
343
+ usedPercent: used,
344
+ ...(Number.isFinite(minutes) && minutes > 0 ? { windowMinutes: minutes } : {}),
345
+ ...(Number.isFinite(resets) && resets > 0 ? { resetsAt: new Date(resets * 1000).toISOString() } : {}),
346
+ });
347
+ }
348
+ const plan = limits.planType ?? limits.plan_type;
349
+ if (windows.length === 0)
350
+ return null;
351
+ return { windows, ...(typeof plan === "string" && plan ? { plan } : {}) };
352
+ }
@@ -55,6 +55,7 @@ const context_notice_1 = require("./context-notice");
55
55
  const fresh_start_verify_1 = require("./fresh-start-verify");
56
56
  const commands_1 = require("./acp/commands");
57
57
  const daemon_glue_1 = require("./acp/daemon-glue");
58
+ const presence_1 = require("./acp/presence");
58
59
  const runner_1 = require("./acp/runner");
59
60
  const turn_queue_1 = require("./acp/turn-queue");
60
61
  const session_reply_1 = require("./session-reply");
@@ -73,6 +74,7 @@ const socket_1 = require("./socket");
73
74
  const transport_1 = require("./transport");
74
75
  const watermarks_1 = require("./watermarks");
75
76
  const bay_relays_1 = require("./bay-relays");
77
+ const acp_launch_1 = require("./launcher/acp-launch");
76
78
  const loop_1 = require("./launcher/loop");
77
79
  const log_line_1 = require("./log-line");
78
80
  const handle_1 = require("./launcher/handle");
@@ -93,6 +95,8 @@ class RelayDaemon {
93
95
  acpQueue = new turn_queue_1.AcpTurnQueue();
94
96
  acp;
95
97
  acpAlive;
98
+ /** The device MCP connection the relay joins its own (ACP) sessions with (tests inject one). */
99
+ acpCallerFn;
96
100
  /** How full each session's context is, as its runtime reports it (contract §7). */
97
101
  context;
98
102
  /** Reads each Codex session's own rollout for its context numbers. */
@@ -280,6 +284,7 @@ class RelayDaemon {
280
284
  conversationFor: (session) => this.lastRoom.get(session),
281
285
  log: (line) => this.log(line),
282
286
  });
287
+ this.acpCallerFn = opts.acpCaller ?? presence_1.deviceMcpCaller;
283
288
  this.acpAlive = (0, daemon_glue_1.acpKeepAliveScheduler)(() => (0, daemon_glue_1.keepAcpSessionsAlive)({ registry: this.registry, auth: this.auth, caller: opts.acpCaller, log: this.log }));
284
289
  this.acp = new runner_1.AcpSessionRunner({
285
290
  registry: this.registry,
@@ -373,6 +378,21 @@ class RelayDaemon {
373
378
  if (this.registry.remove(name))
374
379
  this.log(`session ${name} stopped by its launcher: forgotten`);
375
380
  },
381
+ // End from the app on a session opened by hand: the pane it registered here.
382
+ registeredSession: (name) => this.registry.get(name),
383
+ // "Start again" for those sessions: the launch spot each runs in.
384
+ registeredSessions: () => this.registry.all(),
385
+ // dsh and Cursor started from the app: registered and run here, ended here.
386
+ acp: {
387
+ start: (launch) => (0, acp_launch_1.launchAcpSession)(launch, {
388
+ registry: this.registry,
389
+ log: (line) => this.log(line),
390
+ resolveBinary: this.resolveBinary,
391
+ caller: () => (this.auth ? this.acpCallerFn(this.auth) : undefined),
392
+ }),
393
+ stop: (name) => (0, acp_launch_1.stopAcpSession)(name, { registry: this.registry, abort: (n) => void this.acp.stop(n), log: (line) => this.log(line) }),
394
+ running: () => (0, acp_launch_1.launchedAcpSessions)(this.registry),
395
+ },
376
396
  }).catch((err) => this.log(`launcher stopped: ${err instanceof Error ? err.message : String(err)}`));
377
397
  }
378
398
  this.log(`listening on ${sockPath} (pid ${process.pid}) as device "${device.user.name}"`);
@@ -1626,7 +1646,14 @@ class RelayDaemon {
1626
1646
  (0, socket_1.writeFrame)(previous, { type: "error", message: channel_sessions_1.CHANNEL_REPLACED });
1627
1647
  previous.end();
1628
1648
  }
1629
- this.registry.replace({ name: frame.session, runtime: frame.runtime, delivery: "channel", pane: frame.pane, ownerPid: frame.ownerPid });
1649
+ this.registry.replace({
1650
+ name: frame.session,
1651
+ runtime: frame.runtime,
1652
+ delivery: "channel",
1653
+ pane: frame.pane,
1654
+ ownerPid: frame.ownerPid,
1655
+ ...(typeof frame.cwd === "string" && frame.cwd ? { cwd: frame.cwd } : {}),
1656
+ });
1630
1657
  this.channels.set(frame.session, sock);
1631
1658
  this.registry.setAttached(frame.session, true);
1632
1659
  (0, socket_1.writeFrame)(sock, { type: "attached", session: frame.session, delivery: "channel" });
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACP_LAUNCH_AGENTS = void 0;
4
+ exports.isAcpLaunchRuntime = isAcpLaunchRuntime;
5
+ exports.launchAcpSession = launchAcpSession;
6
+ exports.launchedAcpSessions = launchedAcpSessions;
7
+ exports.stopAcpSession = stopAcpSession;
8
+ const agents_1 = require("../acp/agents");
9
+ const daemon_glue_1 = require("../acp/daemon-glue");
10
+ const policy_1 = require("../acp/policy");
11
+ const presence_1 = require("../acp/presence");
12
+ /**
13
+ * Starting a relay-run agent (DeepSeek's `dsh`, Cursor's `agent`) from the app (2026-10-22, the
14
+ * owner: "other runtimes, not just Claude"). There is no terminal to open: these agents are run
15
+ * by the relay itself, one process per turn over ACP. So a launch does what `baychat connect dsh`
16
+ * does at the computer — minus every question — and the session then answers from the chat.
17
+ *
18
+ * WHAT IT MAY WIDEN. The launch spot is already on this computer's launch allowlist (the owner ran
19
+ * `baychat launch add` for it, offering this runtime). The relay's own rule file lists agents and
20
+ * folders SEPARATELY, so listing an agent there would hand it every folder at that folder's
21
+ * ceiling: an agent the file does not list yet is refused (RUNTIME_NOT_CONNECTED — `baychat connect
22
+ * <agent>` at the computer). For a listed agent, a spot folder that no rule covers is added at the
23
+ * LOWEST ceiling, `chat` (answer, never act); a folder a rule already covers keeps its ceiling.
24
+ */
25
+ /** The launch runtime → the relay's agent row id (Cursor's row is "cursor", its launch runtime "cursor-cli"). */
26
+ exports.ACP_LAUNCH_AGENTS = { dsh: "dsh", "cursor-cli": "cursor" };
27
+ function isAcpLaunchRuntime(runtime) {
28
+ return runtime !== null && runtime in exports.ACP_LAUNCH_AGENTS;
29
+ }
30
+ /** The mode a launched agent starts in, and the ceiling of a rule the launch adds. */
31
+ const LAUNCH_MODE = "chat";
32
+ const refused = (reason) => ({ status: "REFUSED", reason });
33
+ async function launchAcpSession(launch, deps) {
34
+ const row = (0, agents_1.acpAgent)(exports.ACP_LAUNCH_AGENTS[launch.runtime]);
35
+ if (!row)
36
+ return refused("RUNTIME_NOT_OFFERED");
37
+ const existing = deps.registry.get(launch.session);
38
+ if (existing) {
39
+ // A terminal session of that name is somebody's own; a relay-run one of it is already running.
40
+ deps.log(`launch ${launch.launchId}: refused, "${launch.session}" is already a session on this computer`);
41
+ return refused(existing.acp ? "NAME_TAKEN" : "NAME_TAKEN_FOREIGN");
42
+ }
43
+ const binary = deps.resolveBinary(row.bin);
44
+ if (!binary.ok)
45
+ return refused("RUNTIME_MISSING");
46
+ try {
47
+ row.launch(LAUNCH_MODE, deps.patchDir ?? (0, agents_1.acpPatchDir)(), launch.session); // a name its config cannot hold throws
48
+ }
49
+ catch (err) {
50
+ deps.log(`launch ${launch.launchId}: refused, ${err instanceof Error ? err.message : String(err)}`);
51
+ return refused("NAME_INVALID");
52
+ }
53
+ const caller = deps.caller();
54
+ if (!caller)
55
+ return refused("BAY_NOT_CONNECTED");
56
+ const load = deps.loadPolicy ?? (() => (0, policy_1.loadPolicy)());
57
+ const policy = load();
58
+ if (!policy?.agents.includes(row.id)) {
59
+ deps.log(`launch ${launch.launchId}: refused, ${row.label} is not set up for the relay here. Run \`${row.connectCommand}\` at this computer.`);
60
+ return refused("RUNTIME_NOT_CONNECTED");
61
+ }
62
+ if (!(0, policy_1.checkFolder)(policy, row.id, launch.folder).ok) {
63
+ if (policy.roots.some((root) => root.path === launch.folder)) {
64
+ // A rule for exactly this folder that still does not pass: never rewrite it from here.
65
+ deps.log(`launch ${launch.launchId}: refused, the relay's rule for ${launch.folder} does not allow it now. Run \`${row.connectCommand}\` here.`);
66
+ return refused("RUNTIME_NOT_CONNECTED");
67
+ }
68
+ (deps.savePolicy ?? ((p) => (0, policy_1.savePolicy)(p)))((0, policy_1.upsertRoot)(policy, row.id, launch.folder, LAUNCH_MODE));
69
+ deps.log(`launch ${launch.launchId}: ${row.label} may now work in ${launch.folder} (ceiling ${LAUNCH_MODE}; raise it with \`${row.connectCommand}\`)`);
70
+ }
71
+ (0, agents_1.writeModePatch)(row, LAUNCH_MODE, launch.session, deps.patchDir ?? (0, agents_1.acpPatchDir)());
72
+ const room = !launch.join || "direct" in launch.join ? undefined : launch.join;
73
+ try {
74
+ await (0, presence_1.joinSession)(caller, launch.session, room);
75
+ }
76
+ catch (err) {
77
+ deps.log(`launch ${launch.launchId}: ${launch.session} could not join BayChat (${err instanceof Error ? err.message : String(err)})`);
78
+ return { status: "FAILED", reason: "JOIN_FAILED" };
79
+ }
80
+ const registered = (0, daemon_glue_1.registerAcpFrame)({ type: "acp-register", session: launch.session, runtime: row.id, runtimeBin: binary.path, acp: { agent: row.id, cwd: launch.folder, mode: LAUNCH_MODE } }, { registry: deps.registry, log: deps.log, resolveBinary: deps.resolveBinary, loadPolicy: load, platform: deps.platform });
81
+ if (!registered.ok) {
82
+ deps.log(`launch ${launch.launchId}: ${registered.message}`);
83
+ return { status: "FAILED", reason: "SPAWN_FAILED" };
84
+ }
85
+ deps.registry.upsert({ name: launch.session, runtime: row.id, launchedBy: launch.launchId });
86
+ deps.log(`launch ${launch.launchId}: started "${launch.session}" (${row.label}, ${LAUNCH_MODE}) in ${launch.folder}; the relay runs it from now on`);
87
+ return { status: "STARTED" };
88
+ }
89
+ /** The relay-run agents the app started that are still registered, for the launch caps. */
90
+ function launchedAcpSessions(registry) {
91
+ return registry.all().flatMap((t) => (t.relayOwned && t.acp && t.launchedBy ? [{ name: t.name, cwd: t.acp.cwd }] : []));
92
+ }
93
+ /**
94
+ * End on a relay-run agent the app started: forget it, so the relay neither runs it again nor re-joins it (its
95
+ * keep-alive would otherwise bring an ended session straight back), and stop a turn in flight.
96
+ * Its rule and mode patches stay: they are the computer's, not the session's.
97
+ */
98
+ function stopAcpSession(name, deps) {
99
+ const target = deps.registry.get(name);
100
+ // One set up with `baychat connect` at the computer is the computer's: End leaves it registered.
101
+ if (!target?.relayOwned || !target.acp || !target.launchedBy)
102
+ return false;
103
+ deps.abort(name);
104
+ deps.registry.remove(name);
105
+ deps.log(`acp: ${name} ended from BayChat; the relay no longer runs it`);
106
+ return true;
107
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UNATTENDED_NOTE = exports.LAUNCH_SESSION_PREFIX = void 0;
4
4
  exports.multiplexerSessionName = multiplexerSessionName;
5
5
  exports.baychatPrompt = baychatPrompt;
6
+ exports.buildCodexLaunchCommand = buildCodexLaunchCommand;
6
7
  exports.buildLaunchCommand = buildLaunchCommand;
7
8
  exports.launchNameProblem = launchNameProblem;
8
9
  exports.roomTitleProblem = roomTitleProblem;
@@ -25,6 +26,7 @@ exports.roomTitleProblem = roomTitleProblem;
25
26
  */
26
27
  const detect_1 = require("../../channel/detect");
27
28
  const multiplexer_launch_1 = require("../../multiplexer-launch");
29
+ const codex_launch_1 = require("./codex-launch");
28
30
  /** Every launcher-made multiplexer session is named `bc-<session name>`. */
29
31
  exports.LAUNCH_SESSION_PREFIX = "bc-";
30
32
  function multiplexerSessionName(sessionName) {
@@ -43,7 +45,9 @@ exports.UNATTENDED_NOTE = "(Started from the BayChat app: nobody is at this keyb
43
45
  * `ownConnection` (a relay on a setup of its own) the connection is named but no `--bay` is given:
44
46
  * that session has one login, so there is no Bay to choose.
45
47
  */
46
- function baychatPrompt(sessionName, join, bayEntry, ownConnection = false, channel = false) {
48
+ function baychatPrompt(sessionName, join, bayEntry, ownConnection = false, channel = false,
49
+ /** How the runtime calls the skill: Claude Code `/baychat`, Codex `$baychat` (runtimes.ts). */
50
+ invoke = "/baychat") {
47
51
  const room = !join ? "" : "sessions" in join ? " --sessions" : "direct" in join ? " --private" : ` --group "${join.group}"`;
48
52
  // An own connection (`baychat-relay`) is not a Bay: as `--bay` the session passed it on to the
49
53
  // channel's listen, which failed (Mac QA 2026-10-16). It is only named as the connection to use.
@@ -52,7 +56,19 @@ function baychatPrompt(sessionName, join, bayEntry, ownConnection = false, chann
52
56
  // listen_messages + Monitor (Mac QA 2026-10-16). Its channel is named as part of it.
53
57
  const ownChannel = ownConnection && channel ? ` and its channel (call mcp__${detect_1.CHANNEL_MCP_ENTRY}__listen first, with session "${sessionName}")` : "";
54
58
  const connection = bayEntry ? ` Use only the ${bayEntry} connection (its mcp__${bayEntry}__ tools)${ownChannel}.` : "";
55
- return `/baychat ${sessionName}${room}${bay} ${exports.UNATTENDED_NOTE}${connection}`;
59
+ return `${invoke} ${sessionName}${room}${bay} ${exports.UNATTENDED_NOTE}${connection}`;
60
+ }
61
+ /**
62
+ * A launched Codex: the same detached `bc-<name>` session as Claude Code, its unattended flags
63
+ * (codex-launch.ts), and the `$baychat` line as its first prompt — one argv element, never a shell.
64
+ */
65
+ function buildCodexLaunchCommand(input) {
66
+ const args = (0, multiplexer_launch_1.multiplexerArgv)(input.multiplexer, { kind: "detached", name: multiplexerSessionName(input.sessionName), cwd: input.folder }, {
67
+ binary: input.codexBin,
68
+ args: [...codex_launch_1.CODEX_UNATTENDED_FLAGS, baychatPrompt(input.sessionName, input.join, input.bayEntry, false, false, "$baychat")],
69
+ env: input.env,
70
+ });
71
+ return { file: input.multiplexer, args, cwd: input.folder, env: input.env };
56
72
  }
57
73
  function buildLaunchCommand(input) {
58
74
  const name = multiplexerSessionName(input.sessionName);
@@ -0,0 +1,103 @@
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.CODEX_ROOM_ID_RE = exports.CODEX_UNATTENDED_FLAGS = void 0;
37
+ exports.codexConfigPath = codexConfigPath;
38
+ exports.codexLaunchProblem = codexLaunchProblem;
39
+ exports.readCodexConfig = readCodexConfig;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ /**
44
+ * Starting Codex from the app (2026-10-22). Codex runs in a detached `bc-<name>` tmux session like
45
+ * a launched Claude Code, and is told to join with its BayChat skill (`$baychat <name> …`). Once it
46
+ * joined with its thread id, the relay wakes it the way it wakes any open Codex: `codex queue`
47
+ * into that thread's own turn queue (docs/features/AGENT_RELAY.md, "Reachable while open").
48
+ *
49
+ * Nobody is at that keyboard, so anything Codex would stop and ask there waits forever. Two such
50
+ * questions are known before it starts, and refused instead of started into:
51
+ * - its BayChat MCP connection is missing (`baychat connect codex` never ran here): it could not
52
+ * join, so the session would sit unjoined — RUNTIME_NOT_CONNECTED;
53
+ * - it has not trusted the folder ("Do you trust the files in this folder?") — FOLDER_NOT_TRUSTED.
54
+ * And it runs `--ask-for-approval never` inside the `workspace-write` sandbox with no network: it
55
+ * never stops to ask, and what the sandbox does not allow fails and is said in the chat instead.
56
+ * Because nobody approves anything, a spot allows it only after `baychat launch add … --unattended`
57
+ * at the computer (UNATTENDED_NOT_ALLOWED otherwise), and it joins only an existing group by id.
58
+ */
59
+ /** Codex's flags for a session nobody sits at: never stops to ask, writes only its workspace, and
60
+ * has no network whatever its config says (a room member who may wake it must not reach out). */
61
+ exports.CODEX_UNATTENDED_FLAGS = [
62
+ "--sandbox",
63
+ "workspace-write",
64
+ "--ask-for-approval",
65
+ "never",
66
+ "-c",
67
+ "sandbox_workspace_write.network_access=false",
68
+ ];
69
+ /** A group the Codex prompt may name: an id, never a title (a title is text other people chose,
70
+ * and it would be read by Codex as part of its instructions). */
71
+ exports.CODEX_ROOM_ID_RE = /^[a-z0-9]{20,40}$/;
72
+ /** `$CODEX_HOME/config.toml`, else `~/.codex/config.toml` (a snap install's copy is the same file's twin). */
73
+ function codexConfigPath(env = process.env) {
74
+ return path.join(env.CODEX_HOME || path.join(os.homedir(), ".codex"), "config.toml");
75
+ }
76
+ function escapeRe(text) {
77
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
78
+ }
79
+ /**
80
+ * Why Codex cannot start unattended in `folder`, or undefined when it can. Reads `config.toml`
81
+ * as text: two table headers and one key are all it needs, and a TOML parser would be a new
82
+ * dependency for that.
83
+ */
84
+ function codexLaunchProblem(folder, readConfig,
85
+ /** The MCP connection the prompt names (`baychat`, or `baychat-<bay>` on a computer with several). */
86
+ entry = "baychat") {
87
+ const config = readConfig();
88
+ const server = new RegExp(`^\\s*\\[mcp_servers\\.(?:${escapeRe(entry)}|"${escapeRe(entry)}")\\]\\s*$`, "m");
89
+ if (!config || !server.test(config))
90
+ return "RUNTIME_NOT_CONNECTED";
91
+ // `[projects."/path"]` (or single-quoted) followed, before the next table, by trust_level = "trusted".
92
+ const table = new RegExp(`^\\s*\\[projects\\.(?:"${escapeRe(folder)}"|'${escapeRe(folder)}')\\]\\s*$([^\\[]*)`, "m");
93
+ const body = table.exec(config)?.[1] ?? "";
94
+ return /^\s*trust_level\s*=\s*["']trusted["']/m.test(body) ? undefined : "FOLDER_NOT_TRUSTED";
95
+ }
96
+ function readCodexConfig(env = process.env) {
97
+ try {
98
+ return fs.readFileSync(codexConfigPath(env), "utf8");
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }