baychat 0.40.0 → 0.41.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +12 -0
- package/dist/attach-command.js +250 -0
- package/dist/cli-help.js +5 -2
- package/dist/launch/claude-trust.js +3 -1
- package/dist/launch/codex-trust.js +177 -0
- package/dist/launch/command.js +105 -5
- package/dist/launch/policy.js +11 -3
- package/dist/main.js +4 -0
- package/dist/mcp-board-cards.js +115 -0
- package/dist/mcp.js +24 -0
- package/dist/multiplexer-launch.js +9 -13
- package/dist/protocol-content.js +1 -1
- package/dist/relay/launcher/capabilities.js +13 -2
- package/dist/relay/launcher/codex-launch.js +12 -1
- package/dist/relay/launcher/loop.js +76 -4
- package/dist/relay/launcher/spot-request.js +70 -0
- package/dist/relay/launcher/stop-registered.js +10 -4
- package/dist/runtimes.js +3 -0
- package/dist/start-command.js +14 -2
- package/dist/tool-defs.js +96 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"displayName": "BayChat",
|
|
5
5
|
"description": "Wakes this Claude Code session the moment a BayChat message arrives: no Monitor, no 30-minute deadline. Uses the baychat CLI already on this computer (npm install -g baychat, then baychat login).",
|
|
6
6
|
"author": {
|
package/README.md
CHANGED
|
@@ -171,6 +171,14 @@ it asks every time, lists every installed agent and marks the last one chosen; E
|
|
|
171
171
|
`--runtime claude` (or `codex`, …) skips the question. With no terminal (the relay, a script) it
|
|
172
172
|
never asks: it starts the last choice, or the best-supported installed agent.
|
|
173
173
|
|
|
174
|
+
**`baychat start` always opens a new session; `baychat attach` goes back to one.** Since 0.41.1
|
|
175
|
+
`start` never resumes an open session (it used to reattach to one called `baychat`, so the agent
|
|
176
|
+
you picked could be ignored). `baychat start polish` names the new session `polish` (`polish-2`
|
|
177
|
+
when that is taken); with no name it is `<agent>-<pid>`. `baychat attach` lists the BayChat
|
|
178
|
+
sessions open on this computer — name, agent, how long running — and attaches to the one you pick
|
|
179
|
+
(`baychat attach polish` goes straight there). Only sessions BayChat started are listed, never
|
|
180
|
+
your own tmux or screen sessions.
|
|
181
|
+
|
|
174
182
|
To keep the system Node at 20 for other projects, run only `baychat` on Node 22 (Linux/WSL with
|
|
175
183
|
systemd; details in [`CLAUDE_CODE_CHANNEL.md`](../../docs/features/CLAUDE_CODE_CHANNEL.md)):
|
|
176
184
|
unpack Node 22 into `~/.local/node22`, then put a wrapper `~/.local/bin/baychat` ahead of npm's
|
|
@@ -918,6 +926,10 @@ instruction):
|
|
|
918
926
|
| `get_note` | Read one note in full (and the version to edit from) |
|
|
919
927
|
| `create_note` | Write a note for the project or the chat; it lands on the shelf (people pin) |
|
|
920
928
|
| `update_note` | Edit a note from the version you read; if someone saved since, nothing is overwritten and you get theirs to merge |
|
|
929
|
+
| `list_board_cards` | The to-dos on the boards of the chat's project, by column, with the board's table fields and each to-do's values; `mine` marks the ones you may move or edit |
|
|
930
|
+
| `add_board_card` | Add a to-do: yours by default (claim the work you are doing); one for a person or nobody is only a suggestion until a person accepts it |
|
|
931
|
+
| `move_board_card` | Move one of your to-dos to another column (Doing, Done, Blocked) |
|
|
932
|
+
| `update_board_card` | Edit the title, notes, dates or table values of one of your to-dos (money in integer cents) |
|
|
921
933
|
| `get_conversation_summary` | The rolling catch-up summary (decisions, tasks, questions, facts + source ids) |
|
|
922
934
|
| `get_messages` | Recent messages enriched with sender, mentions, and `shouldRespond` |
|
|
923
935
|
| `send_message` | Send a message into a conversation |
|
|
@@ -0,0 +1,250 @@
|
|
|
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.SESSION_AGENT_VAR = void 0;
|
|
37
|
+
exports.findOpenSessions = findOpenSessions;
|
|
38
|
+
exports.attachArgv = attachArgv;
|
|
39
|
+
exports.cmdAttachWith = cmdAttachWith;
|
|
40
|
+
exports.cmdAttach = cmdAttach;
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const multiplexer_launch_1 = require("./multiplexer-launch");
|
|
44
|
+
const started_1 = require("./relay/launcher/started");
|
|
45
|
+
/**
|
|
46
|
+
* `baychat attach [name]` — back into a BayChat session already open on this computer.
|
|
47
|
+
*
|
|
48
|
+
* Karmen, 9 Nov 2026: `baychat start` always opens a NEW session (it used to reattach to one
|
|
49
|
+
* called `baychat`, and dropped her into an old Codex after she had picked Claude). Going back to
|
|
50
|
+
* a running session is this command: a numbered list of the sessions open here — name, agent,
|
|
51
|
+
* how long it has run — and the one picked is attached. One open: straight in. None: say so.
|
|
52
|
+
* No terminal: print the list, attach nothing.
|
|
53
|
+
*
|
|
54
|
+
* ── ONLY OURS, NEVER A PERSON'S OWN ──────────────────────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* A tmux or screen session counts only when BayChat made it, proven, never guessed from a name:
|
|
57
|
+
* - tmux: the agent in it carries `BAYCHAT_TMUX_SESSION=<that very session>` — set by `baychat
|
|
58
|
+
* start` and `baychat claude` since October, so sessions started before this command exist
|
|
59
|
+
* are found too;
|
|
60
|
+
* - screen: the agent carries `BAYCHAT_SESSION_AGENT` (set by `start` from 0.41.1);
|
|
61
|
+
* - a session started from the app: the relay launcher's own record (`launch/started.json`).
|
|
62
|
+
* The variable is read from the agent process's environment (Linux `/proc`, macOS `ps -E`) — on
|
|
63
|
+
* the pane's process or its child, because a pane may run a shell that started the agent.
|
|
64
|
+
*/
|
|
65
|
+
/** Set on the agent by `baychat start`: which agent it is, for `attach`'s list (and screen's proof). */
|
|
66
|
+
exports.SESSION_AGENT_VAR = "BAYCHAT_SESSION_AGENT";
|
|
67
|
+
function findOpenSessions(input) {
|
|
68
|
+
/** The pane's process, then its children: the first one carrying BayChat's variables. */
|
|
69
|
+
const agentEnv = (pid) => [pid, ...input.childrenOf(pid)].map((p) => input.envOf(p)).find((env) => env && (env[multiplexer_launch_1.TMUX_SESSION_VAR] || env[exports.SESSION_AGENT_VAR] || env.STY));
|
|
70
|
+
const launched = (multiplexer, name) => input.launcher.find((l) => l.multiplexer === multiplexer && l.name === name);
|
|
71
|
+
const found = [];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const line of (input.tmuxPanes ?? "").split("\n")) {
|
|
74
|
+
const [session, pidText, command, created] = line.split("\t");
|
|
75
|
+
if (!session || seen.has(session))
|
|
76
|
+
continue;
|
|
77
|
+
const pid = Number(pidText);
|
|
78
|
+
const env = Number.isInteger(pid) ? agentEnv(pid) : undefined;
|
|
79
|
+
const record = launched("tmux", session);
|
|
80
|
+
if (!record && env?.[multiplexer_launch_1.TMUX_SESSION_VAR] !== session)
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(session);
|
|
83
|
+
const createdSecs = Number(created);
|
|
84
|
+
found.push({
|
|
85
|
+
multiplexer: "tmux",
|
|
86
|
+
name: session,
|
|
87
|
+
agent: record?.runtime ?? env?.[exports.SESSION_AGENT_VAR] ?? (command || "agent"),
|
|
88
|
+
...(Number.isFinite(createdSecs) && createdSecs > 0 ? { runningSeconds: Math.max(0, Math.floor(input.now / 1000 - createdSecs)) } : {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (const line of (input.screenList ?? "").split("\n")) {
|
|
92
|
+
const match = /^\s+(\d+)\.(\S+)\s/.exec(line);
|
|
93
|
+
if (!match)
|
|
94
|
+
continue;
|
|
95
|
+
const pid = Number(match[1]);
|
|
96
|
+
const target = `${match[1]}.${match[2]}`;
|
|
97
|
+
const env = agentEnv(pid);
|
|
98
|
+
const record = launched("screen", match[2]);
|
|
99
|
+
if (!record && !(env?.[exports.SESSION_AGENT_VAR] && env.STY === target))
|
|
100
|
+
continue;
|
|
101
|
+
const elapsed = input.elapsedSecondsOf(pid);
|
|
102
|
+
found.push({
|
|
103
|
+
multiplexer: "screen",
|
|
104
|
+
name: target,
|
|
105
|
+
agent: record?.runtime ?? env?.[exports.SESSION_AGENT_VAR] ?? "agent",
|
|
106
|
+
...(elapsed !== undefined ? { runningSeconds: elapsed } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return found.sort((a, b) => (b.runningSeconds ?? 0) - (a.runningSeconds ?? 0));
|
|
110
|
+
}
|
|
111
|
+
/** The command that puts this terminal into `session`. Inside tmux already, switch instead of nesting. */
|
|
112
|
+
function attachArgv(session, insideTmux) {
|
|
113
|
+
if (session.multiplexer === "screen")
|
|
114
|
+
return ["screen", ["-r", session.name]];
|
|
115
|
+
return ["tmux", [insideTmux ? "switch-client" : "attach", "-t", `=${session.name}`]];
|
|
116
|
+
}
|
|
117
|
+
function age(seconds) {
|
|
118
|
+
if (seconds === undefined)
|
|
119
|
+
return "";
|
|
120
|
+
if (seconds < 60)
|
|
121
|
+
return "just started";
|
|
122
|
+
const minutes = Math.floor(seconds / 60);
|
|
123
|
+
if (minutes < 60)
|
|
124
|
+
return `${minutes} min`;
|
|
125
|
+
const hours = Math.floor(minutes / 60);
|
|
126
|
+
if (hours < 48)
|
|
127
|
+
return minutes % 60 ? `${hours} h ${minutes % 60} min` : `${hours} h`;
|
|
128
|
+
return `${Math.floor(hours / 24)} days`;
|
|
129
|
+
}
|
|
130
|
+
async function cmdAttachWith(args, deps) {
|
|
131
|
+
const sessions = deps.sessions();
|
|
132
|
+
if (sessions.length === 0) {
|
|
133
|
+
deps.say("No BayChat sessions are open on this computer. Start one with `baychat start`.");
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const named = args.find((a) => !a.startsWith("-"));
|
|
137
|
+
if (named) {
|
|
138
|
+
const session = sessions.find((s) => s.name === named || s.name.endsWith(`.${named}`));
|
|
139
|
+
if (!session) {
|
|
140
|
+
deps.say(`"${named}" is not one of the BayChat sessions open here. Run \`baychat attach\` to see them.`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
return attach(session, deps);
|
|
144
|
+
}
|
|
145
|
+
const width = Math.max(...sessions.map((s) => s.name.length));
|
|
146
|
+
const lines = sessions.map((s, i) => ` ${i + 1}. ${s.name.padEnd(width)} ${s.agent.padEnd(8)} ${age(s.runningSeconds)}`.trimEnd());
|
|
147
|
+
if (!deps.atTerminal) {
|
|
148
|
+
deps.say("BayChat sessions open on this computer:");
|
|
149
|
+
lines.forEach((line) => deps.say(line));
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (sessions.length === 1)
|
|
153
|
+
return attach(sessions[0], deps);
|
|
154
|
+
deps.say("BayChat sessions open on this computer:");
|
|
155
|
+
lines.forEach((line) => deps.say(line));
|
|
156
|
+
const answer = (await deps.ask(`Attach to which one? 1-${sessions.length}: `)).trim();
|
|
157
|
+
const picked = sessions[Number(answer) - 1] ?? sessions.find((s) => s.name === answer);
|
|
158
|
+
if (!picked) {
|
|
159
|
+
deps.say("Not one of the options.");
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
return attach(picked, deps);
|
|
163
|
+
}
|
|
164
|
+
function attach(session, deps) {
|
|
165
|
+
const [file, argv] = attachArgv(session, deps.insideTmux);
|
|
166
|
+
const result = deps.run(file, argv);
|
|
167
|
+
if (result.error) {
|
|
168
|
+
deps.say(`Could not attach to ${session.name}: ${result.error.message}`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
return result.status ?? 0;
|
|
172
|
+
}
|
|
173
|
+
function cmdAttach(args) {
|
|
174
|
+
return cmdAttachWith(args, {
|
|
175
|
+
sessions: () => findOpenSessions({
|
|
176
|
+
tmuxPanes: capture("tmux", ["list-panes", "-a", "-F", "#{session_name}\t#{pane_pid}\t#{pane_current_command}\t#{session_created}"]),
|
|
177
|
+
screenList: capture("screen", ["-ls"], true),
|
|
178
|
+
launcher: (0, started_1.loadStarted)(),
|
|
179
|
+
envOf: processEnv,
|
|
180
|
+
childrenOf: processChildren,
|
|
181
|
+
elapsedSecondsOf: processElapsed,
|
|
182
|
+
now: Date.now(),
|
|
183
|
+
}),
|
|
184
|
+
atTerminal: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
185
|
+
insideTmux: !!process.env.TMUX,
|
|
186
|
+
ask: multiplexer_launch_1.askLine,
|
|
187
|
+
run: (file, args) => {
|
|
188
|
+
const r = (0, child_process_1.spawnSync)(file, args, { stdio: "inherit" });
|
|
189
|
+
return { status: r.status, ...(r.error ? { error: r.error } : {}) };
|
|
190
|
+
},
|
|
191
|
+
say: (line) => console.log(line),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** A program's stdout, or undefined when it is not installed. `anyStatus`: screen -ls exits 1 with sessions listed. */
|
|
195
|
+
function capture(file, args, anyStatus = false) {
|
|
196
|
+
const r = (0, child_process_1.spawnSync)(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
197
|
+
if (r.error)
|
|
198
|
+
return undefined;
|
|
199
|
+
if (r.status !== 0 && !anyStatus)
|
|
200
|
+
return "";
|
|
201
|
+
return r.stdout ?? "";
|
|
202
|
+
}
|
|
203
|
+
/** A process's environment: Linux reads /proc; macOS asks `ps -E` and keeps only BayChat's variables and STY. */
|
|
204
|
+
function processEnv(pid) {
|
|
205
|
+
try {
|
|
206
|
+
const raw = fs.readFileSync(`/proc/${pid}/environ`, "utf8");
|
|
207
|
+
return Object.fromEntries(raw.split("\0").filter(Boolean).map((kv) => [kv.slice(0, kv.indexOf("=")), kv.slice(kv.indexOf("=") + 1)]));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Not Linux, or not ours to read.
|
|
211
|
+
}
|
|
212
|
+
if (process.platform !== "darwin")
|
|
213
|
+
return undefined;
|
|
214
|
+
try {
|
|
215
|
+
const line = (0, child_process_1.execFileSync)("ps", ["-E", "-ww", "-o", "command=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
216
|
+
const env = {};
|
|
217
|
+
for (const m of line.matchAll(/\b(BAYCHAT_[A-Z_]+|STY)=(\S+)/g))
|
|
218
|
+
env[m[1]] = m[2];
|
|
219
|
+
return env;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function processChildren(pid) {
|
|
226
|
+
try {
|
|
227
|
+
return fs.readFileSync(`/proc/${pid}/task/${pid}/children`, "utf8").trim().split(/\s+/).filter(Boolean).map(Number);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Not Linux: ask pgrep.
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return (0, child_process_1.execFileSync)("pgrep", ["-P", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split(/\s+/).filter(Boolean).map(Number);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function processElapsed(pid) {
|
|
240
|
+
try {
|
|
241
|
+
const out = (0, child_process_1.execFileSync)("ps", ["-o", "etime=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
242
|
+
// [[dd-]hh:]mm:ss
|
|
243
|
+
const [days, rest] = out.includes("-") ? out.split("-") : ["0", out];
|
|
244
|
+
const parts = rest.split(":").map(Number).reverse();
|
|
245
|
+
return Number(days) * 86400 + (parts[2] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[0] ?? 0);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
package/dist/cli-help.js
CHANGED
|
@@ -29,8 +29,11 @@ Usage:
|
|
|
29
29
|
baychat setup Look again and set up any agent app installed since;
|
|
30
30
|
starts nothing
|
|
31
31
|
baychat start [name] [--runtime <runtime>]
|
|
32
|
-
Start a session (what bare \`baychat\` does),
|
|
33
|
-
a name for it
|
|
32
|
+
Start a NEW session (what bare \`baychat\` does),
|
|
33
|
+
with a name for it; never returns to an old one
|
|
34
|
+
baychat attach [name] Go back to a BayChat session open on this computer:
|
|
35
|
+
lists them (name, agent, how long running) and
|
|
36
|
+
attaches to the one you pick
|
|
34
37
|
baychat update Bring this machine onto the current version —
|
|
35
38
|
the package, every connected client's config, and
|
|
36
39
|
the relay daemon (which otherwise keeps running
|
|
@@ -176,7 +176,9 @@ function trustProblemText(reason, file) {
|
|
|
176
176
|
}
|
|
177
177
|
/**
|
|
178
178
|
* Set `projects[folder].hasTrustDialogAccepted = true` and nothing else. `folder` should be the
|
|
179
|
-
* realpath (the spot's stored path). Called
|
|
179
|
+
* realpath (the spot's stored path). Called from `baychat launch add` after a person said y, and from
|
|
180
|
+
* the relay for the owner's "Allow… in this folder" on their phone — only for a folder that person
|
|
181
|
+
* opened to it (`phoneMayAllow`, relay/launcher/spot-request.ts).
|
|
180
182
|
*/
|
|
181
183
|
function trustFolder(folder, file = claudeConfigPath()) {
|
|
182
184
|
// Write through a symlinked ~/.claude.json (dotfile managers) instead of replacing the link.
|
|
@@ -0,0 +1,177 @@
|
|
|
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.codexConfigPath = void 0;
|
|
37
|
+
exports.isCodexFolderTrusted = isCodexFolderTrusted;
|
|
38
|
+
exports.codexTrustProblemText = codexTrustProblemText;
|
|
39
|
+
exports.trustCodexFolder = trustCodexFolder;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const codex_launch_1 = require("../relay/launcher/codex-launch");
|
|
43
|
+
Object.defineProperty(exports, "codexConfigPath", { enumerable: true, get: function () { return codex_launch_1.codexConfigPath; } });
|
|
44
|
+
function read(file) {
|
|
45
|
+
try {
|
|
46
|
+
return { ok: true, raw: fs.readFileSync(file, "utf8") };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { ok: false, reason: err.code === "ENOENT" ? "CONFIG_MISSING" : "CONFIG_UNREADABLE" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Read-only, by the relay's own rule: the folder's own table says trusted. */
|
|
53
|
+
function isCodexFolderTrusted(folder, file = (0, codex_launch_1.codexConfigPath)()) {
|
|
54
|
+
const r = read(file);
|
|
55
|
+
if (!r.ok)
|
|
56
|
+
return false;
|
|
57
|
+
// codexLaunchProblem also wants the MCP entry; ask it only about trust by handing it a config that has one.
|
|
58
|
+
const withEntry = `[mcp_servers.baychat]\n${r.raw}`;
|
|
59
|
+
return (0, codex_launch_1.codexLaunchProblem)(folder, () => withEntry) === undefined;
|
|
60
|
+
}
|
|
61
|
+
/** A sentence for each problem, for the person at the terminal. */
|
|
62
|
+
function codexTrustProblemText(reason, file) {
|
|
63
|
+
switch (reason) {
|
|
64
|
+
case "CONFIG_MISSING":
|
|
65
|
+
return `${file} does not exist yet (Codex has not run on this computer).`;
|
|
66
|
+
case "CONFIG_UNREADABLE":
|
|
67
|
+
return `${file} could not be read.`;
|
|
68
|
+
case "PATH_UNSAFE":
|
|
69
|
+
return "the folder's name has a line break, or both a quote and a backslash, which BayChat will not write into Codex's config.";
|
|
70
|
+
case "CONFIG_UNKNOWN_FORM":
|
|
71
|
+
return `${file} already names this folder in a form BayChat does not edit, so it was left alone. Set the trust in Codex itself.`;
|
|
72
|
+
case "CONFIG_CHANGING":
|
|
73
|
+
return `${file} kept changing while BayChat tried to write it (is Codex busy?).`;
|
|
74
|
+
case "WRITE_FAILED":
|
|
75
|
+
return `${file} could not be written.`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The folder as a TOML key: a basic string, else a literal string, else null (neither can hold it). */
|
|
79
|
+
function tomlKey(folder) {
|
|
80
|
+
if (/[\n\r]/.test(folder))
|
|
81
|
+
return null;
|
|
82
|
+
if (!/["\\]/.test(folder))
|
|
83
|
+
return `"${folder}"`;
|
|
84
|
+
if (!folder.includes("'"))
|
|
85
|
+
return `'${folder}'`;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The config with `folder` trusted: its table's level set in place, or one table appended — or null when the
|
|
90
|
+
* folder is already a key in some other form, where an appended table would be a duplicate key.
|
|
91
|
+
*/
|
|
92
|
+
function withTrust(raw, folder) {
|
|
93
|
+
const header = new RegExp(`^[ \\t]*\\[projects\\.${(0, codex_launch_1.codexProjectKeyRe)(folder)}\\][ \\t]*$`, "m");
|
|
94
|
+
const found = header.exec(raw);
|
|
95
|
+
if (found) {
|
|
96
|
+
const start = found.index + found[0].length;
|
|
97
|
+
const next = raw.slice(start).search(/^[ \t]*\[/m);
|
|
98
|
+
const end = next === -1 ? raw.length : start + next;
|
|
99
|
+
const body = raw.slice(start, end);
|
|
100
|
+
const level = /^([ \t]*trust_level[ \t]*=[ \t]*)("[^"\n]*"|'[^'\n]*')/m;
|
|
101
|
+
const nextBody = level.test(body) ? body.replace(level, '$1"trusted"') : `\ntrust_level = "trusted"${body}`;
|
|
102
|
+
return raw.slice(0, start) + nextBody + raw.slice(end);
|
|
103
|
+
}
|
|
104
|
+
const otherForm = new RegExp(`${(0, codex_launch_1.codexProjectKeyRe)(folder)}\\s*(?:=|\\.)`);
|
|
105
|
+
if (otherForm.test(raw))
|
|
106
|
+
return null;
|
|
107
|
+
const sep = raw === "" || raw.endsWith("\n") ? "" : "\n";
|
|
108
|
+
return `${raw}${sep}\n[projects.${tomlKey(folder)}]\ntrust_level = "trusted"\n`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Trust `folder` (the spot's stored realpath) for Codex. Called from `baychat launch add` after a person at this
|
|
112
|
+
* computer said y, and from the relay for the owner's "Allow… in this folder" on their phone — only for a folder
|
|
113
|
+
* that person opened to it (`phoneMayAllow`, relay/launcher/spot-request.ts).
|
|
114
|
+
*/
|
|
115
|
+
function trustCodexFolder(folder, file = (0, codex_launch_1.codexConfigPath)()) {
|
|
116
|
+
if (tomlKey(folder) === null)
|
|
117
|
+
return { ok: false, reason: "PATH_UNSAFE" };
|
|
118
|
+
// Write through a symlinked config (dotfile managers) instead of replacing the link.
|
|
119
|
+
let target = file;
|
|
120
|
+
try {
|
|
121
|
+
target = fs.realpathSync(file);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Missing: read() reports it below.
|
|
125
|
+
}
|
|
126
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
127
|
+
const r = read(target);
|
|
128
|
+
// A fresh Codex install has its folder but no config.toml yet (vps, 4 Nov): create it holding only this
|
|
129
|
+
// table, 0600, and only if nothing appeared there meanwhile (`wx`). No folder: Codex has not run here.
|
|
130
|
+
if (!r.ok && r.reason === "CONFIG_MISSING" && fs.existsSync(path.dirname(target))) {
|
|
131
|
+
try {
|
|
132
|
+
fs.writeFileSync(target, (withTrust("", folder) ?? "").replace(/^\n/, ""), { mode: 0o600, flag: "wx" });
|
|
133
|
+
fs.chmodSync(target, 0o600); // writeFile's mode is masked by the umask
|
|
134
|
+
return { ok: true, changed: true };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err.code === "EEXIST")
|
|
138
|
+
continue; // Codex wrote it first: start over.
|
|
139
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!r.ok)
|
|
143
|
+
return r;
|
|
144
|
+
if (isCodexFolderTrusted(folder, target))
|
|
145
|
+
return { ok: true, changed: false };
|
|
146
|
+
const text = withTrust(r.raw, folder);
|
|
147
|
+
if (text === null)
|
|
148
|
+
return { ok: false, reason: "CONFIG_UNKNOWN_FORM" };
|
|
149
|
+
let mode;
|
|
150
|
+
try {
|
|
151
|
+
mode = fs.statSync(target).mode & 0o777;
|
|
152
|
+
const bak = `${target}.bak`;
|
|
153
|
+
fs.copyFileSync(target, bak);
|
|
154
|
+
fs.chmodSync(bak, mode);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
158
|
+
}
|
|
159
|
+
const tmp = path.join(path.dirname(target), `.${path.basename(target)}.baychat-${process.pid}-${Date.now()}.tmp`);
|
|
160
|
+
try {
|
|
161
|
+
fs.writeFileSync(tmp, text, { mode });
|
|
162
|
+
fs.chmodSync(tmp, mode); // writeFile's mode is masked by the umask
|
|
163
|
+
// If Codex rewrote it since we read it, start over rather than rename our older copy over its newer one.
|
|
164
|
+
if (fs.readFileSync(target, "utf8") !== r.raw) {
|
|
165
|
+
fs.rmSync(tmp, { force: true });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
fs.renameSync(tmp, target);
|
|
169
|
+
return { ok: true, changed: true };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
fs.rmSync(tmp, { force: true });
|
|
173
|
+
return { ok: false, reason: "WRITE_FAILED" };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ok: false, reason: "CONFIG_CHANGING" };
|
|
177
|
+
}
|