baychat 0.40.0 → 0.41.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
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
@@ -918,6 +918,10 @@ instruction):
918
918
  | `get_note` | Read one note in full (and the version to edit from) |
919
919
  | `create_note` | Write a note for the project or the chat; it lands on the shelf (people pin) |
920
920
  | `update_note` | Edit a note from the version you read; if someone saved since, nothing is overwritten and you get theirs to merge |
921
+ | `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 |
922
+ | `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 |
923
+ | `move_board_card` | Move one of your to-dos to another column (Doing, Done, Blocked) |
924
+ | `update_board_card` | Edit the title, notes, dates or table values of one of your to-dos (money in integer cents) |
921
925
  | `get_conversation_summary` | The rolling catch-up summary (decisions, tasks, questions, facts + source ids) |
922
926
  | `get_messages` | Recent messages enriched with sender, mentions, and `shouldRespond` |
923
927
  | `send_message` | Send a message into a conversation |
@@ -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 ONLY from `baychat launch add` after a person said y.
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
+ }
@@ -33,18 +33,22 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.trustByHand = exports.CODEX_UNATTENDED_NOTICE = exports.AGENT_STARTS_NOTICE = exports.APPROVALS_NOTICE = exports.LAUNCH_USAGE = void 0;
36
+ exports.trustByHand = exports.codexTrustByHand = exports.CODEX_UNATTENDED_NOTICE = exports.PHONE_ALLOW_NOTICE = exports.AGENT_STARTS_NOTICE = exports.APPROVALS_NOTICE = exports.LAUNCH_USAGE = void 0;
37
37
  exports.cmdLaunch = cmdLaunch;
38
38
  const path = __importStar(require("path"));
39
39
  const args_1 = require("../args");
40
40
  const multiplexer_launch_1 = require("../multiplexer-launch");
41
41
  const policy_1 = require("../relay/acp/policy");
42
42
  const claude_trust_1 = require("./claude-trust");
43
+ const codex_trust_1 = require("./codex-trust");
44
+ const capabilities_1 = require("../relay/launcher/capabilities");
45
+ const runtime_binary_1 = require("../runtime-binary");
43
46
  const policy_2 = require("./policy");
44
- exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>] [--unattended] [--agent-starts]
47
+ exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>] [--unattended] [--agent-starts] [--phone-may-allow | --no-phone-allow]
45
48
  Let BayChat start sessions in <folder> from the app
46
49
  (--unattended: also let it start Codex there, which then never asks)
47
50
  (--agent-starts: agents you allowed "Always" may start sessions there without asking you each time)
51
+ (--phone-may-allow: you may allow Claude Code or Codex there later from your phone)
48
52
  baychat launch list The folders BayChat may start sessions in
49
53
  baychat launch remove <id> Stop offering one of them`;
50
54
  /** Printed by every `add`: the one thing a person must know before saying yes. */
@@ -53,6 +57,9 @@ exports.APPROVALS_NOTICE = "Claude Code sessions started here from BayChat ask f
53
57
  exports.AGENT_STARTS_NOTICE = "Agents you allowed with \"Always for this agent here\" on their card may start sessions here without asking you each time — " +
54
58
  "only for this folder, computer and runtime, for 30 days, and only with no task or one of your own messages as the task: a task an agent or anyone else writes still asks. " +
55
59
  "Those sessions ask for approvals in BayChat like any other; they never start Codex unattended. Revoke an agent in Settings → Devices.";
60
+ /** Printed when a folder is opened to the phone: what the owner may then do from it, and what never. */
61
+ exports.PHONE_ALLOW_NOTICE = "From your phone you can then allow Claude Code or Codex in this folder (and trust it for them), one at a time — after your code or passkey if your account has one. " +
62
+ "It never adds a folder or changes anything else here. `baychat launch add <folder> --no-phone-allow` turns it off.";
56
63
  /** Printed when Codex is offered: what it may do without asking, and what it asks on the phone. */
57
64
  exports.CODEX_UNATTENDED_NOTICE = "Codex started here from BayChat is run by the relay (Codex 0.160 or newer): it can change files in this folder without asking, with no network, " +
58
65
  "and asks for approval in BayChat — on the session owner's phone — before anything more (a command outside its sandbox, network access, files elsewhere). " +
@@ -61,10 +68,13 @@ exports.CODEX_UNATTENDED_NOTICE = "Codex started here from BayChat is run by the
61
68
  "MCP servers in its own config run as usual.";
62
69
  const realDeps = () => {
63
70
  const file = (0, claude_trust_1.claudeConfigPath)();
71
+ const codexFile = (0, codex_trust_1.codexConfigPath)();
64
72
  return {
65
73
  print: (line) => console.log(line),
66
74
  policyFile: (0, policy_2.launchPolicyPath)(),
67
75
  trust: { isTrusted: (folder) => (0, claude_trust_1.isFolderTrusted)(folder, file), trust: (folder) => (0, claude_trust_1.trustFolder)(folder, file), file },
76
+ codexTrust: { isTrusted: (folder) => (0, codex_trust_1.isCodexFolderTrusted)(folder, codexFile), trust: (folder) => (0, codex_trust_1.trustCodexFolder)(folder, codexFile), file: codexFile },
77
+ foundRuntimes: () => policy_2.LAUNCH_RUNTIMES.filter((r) => (0, runtime_binary_1.resolveRuntimeBinary)(capabilities_1.RUNTIME_BINARY[r], (0, runtime_binary_1.currentBinaryEnv)()).ok),
68
78
  interactive: !!process.stdin.isTTY && !!process.stdout.isTTY,
69
79
  ask: multiplexer_launch_1.askLine,
70
80
  };
@@ -93,8 +103,9 @@ async function addSpot(args, deps) {
93
103
  const label = (0, args_1.flag)(args, "--label");
94
104
  const runtimeList = (0, args_1.flag)(args, "--runtime");
95
105
  const maxRaw = (0, args_1.flag)(args, "--max");
96
- const unattended = args.includes("--unattended");
106
+ let unattended = args.includes("--unattended");
97
107
  const agentStarts = args.includes("--agent-starts");
108
+ const phoneFlag = args.includes("--phone-may-allow") ? true : args.includes("--no-phone-allow") ? false : undefined;
98
109
  const valued = new Set([label, runtimeList, maxRaw].filter((v) => v !== undefined));
99
110
  const folderArg = args.find((a) => !a.startsWith("--") && !valued.has(a));
100
111
  if (unattended && !(runtimeList ?? "").split(",").map((r) => r.trim()).includes("codex")) {
@@ -113,7 +124,13 @@ async function addSpot(args, deps) {
113
124
  deps.print(`A label is 1-${policy_2.SPOT_LABEL_MAX} characters.`);
114
125
  return 1;
115
126
  }
116
- const requested = (runtimeList ?? "claude").split(",").map((r) => r.trim()).filter(Boolean);
127
+ const requested = runtimeList !== undefined ? runtimeList.split(",").map((r) => r.trim()).filter(Boolean) : await defaultRuntimes(deps);
128
+ if (runtimeList === undefined && requested.includes("codex") && !unattended) {
129
+ if (await allowCodexUnattended(deps))
130
+ unattended = true;
131
+ else
132
+ requested.splice(requested.indexOf("codex"), 1);
133
+ }
117
134
  const unknown = requested.filter((r) => !(0, policy_2.isLaunchRuntime)(r));
118
135
  if (requested.length === 0 || unknown.length > 0) {
119
136
  deps.print(`Unknown runtime ${unknown.join(", ") || "(none given)"}. Choose from: ${policy_2.LAUNCH_RUNTIMES.join(", ")}.`);
@@ -126,6 +143,8 @@ async function addSpot(args, deps) {
126
143
  }
127
144
  const policy = (0, policy_2.loadLaunchPolicy)(deps.policyFile);
128
145
  const existing = policy?.spots.find((s) => s.path === folder.path);
146
+ // Without a flag or a terminal, a folder added again keeps what it said; a new one stays closed.
147
+ const keptPhone = phoneFlag ?? (deps.interactive ? undefined : existing?.phoneMayAllow === true);
129
148
  const spot = {
130
149
  id: existing?.id ?? (0, policy_2.suggestSpotId)(policy, folder.path),
131
150
  label: spotLabel,
@@ -134,6 +153,8 @@ async function addSpot(args, deps) {
134
153
  maxSessions,
135
154
  ...(unattended ? { unattendedCodex: true } : {}),
136
155
  ...(agentStarts ? { agentStarts: true } : {}),
156
+ ...(keptPhone ? { phoneMayAllow: true } : {}),
157
+ ...(keptPhone && existing?.phoneAllowedAt ? { phoneAllowedAt: existing.phoneAllowedAt } : {}),
137
158
  };
138
159
  (0, policy_2.saveLaunchPolicy)((0, policy_2.upsertSpot)(policy, spot), deps.policyFile);
139
160
  deps.print(`${existing ? "Updated" : "Added"} "${spot.label}" (${spot.id}): ${spot.path}`);
@@ -148,8 +169,82 @@ async function addSpot(args, deps) {
148
169
  }
149
170
  if (spot.runtimes.includes("claude"))
150
171
  await offerClaudeTrust(spot.path, deps);
172
+ if (spot.runtimes.includes("codex") && spot.unattendedCodex)
173
+ await offerCodexTrust(spot.path, deps);
174
+ if (keptPhone === undefined) {
175
+ // Asked last, once, at a terminal only — Enter is yes (Karmen, 4 Nov: (a), asked once at setup).
176
+ const answer = await deps.ask("Let you allow more agents in this folder from your phone later? (Y/n) ");
177
+ if (!/^\s*n(o)?\s*$/i.test(answer)) {
178
+ const opened = { ...spot, phoneMayAllow: true, ...(existing?.phoneAllowedAt ? { phoneAllowedAt: existing.phoneAllowedAt } : {}) };
179
+ (0, policy_2.saveLaunchPolicy)((0, policy_2.upsertSpot)((0, policy_2.loadLaunchPolicy)(deps.policyFile), opened), deps.policyFile);
180
+ deps.print(` ${exports.PHONE_ALLOW_NOTICE}`);
181
+ }
182
+ }
183
+ else if (keptPhone) {
184
+ deps.print(` ${exports.PHONE_ALLOW_NOTICE}`);
185
+ }
186
+ else if (phoneFlag === undefined) {
187
+ deps.print(" To allow more agents here later from your phone, add --phone-may-allow.");
188
+ }
151
189
  return 0;
152
190
  }
191
+ /** The runtimes' names a person reads, for the questions below. */
192
+ const RUNTIME_NAMES = { claude: "Claude Code", codex: "Codex", dsh: "DeepSeek Harness", "cursor-cli": "Cursor CLI" };
193
+ /**
194
+ * `add` without --runtime (Karmen, 4 Nov: "when we launch baychat every agent works"): every runtime found here,
195
+ * asked once at a terminal — Enter for all, or a list. Nothing found: Claude Code, as before.
196
+ */
197
+ async function defaultRuntimes(deps) {
198
+ const found = deps.foundRuntimes();
199
+ if (found.length === 0)
200
+ return ["claude"];
201
+ if (!deps.interactive || found.length === 1)
202
+ return [...found];
203
+ const answer = await deps.ask(`Found here: ${found.map((r) => RUNTIME_NAMES[r]).join(", ")}. Allow these in this folder? [Enter = all, or a list like ${found.join(",")}] `);
204
+ const picked = answer.split(",").map((r) => r.trim()).filter(Boolean);
205
+ return picked.length > 0 ? picked : [...found];
206
+ }
207
+ /**
208
+ * Codex runs here unattended (it never stops to ask), so it is never part of a default without its own y.
209
+ * Without a terminal it is left out, and the person is told the one line that adds it.
210
+ */
211
+ async function allowCodexUnattended(deps) {
212
+ if (!deps.interactive) {
213
+ const all = deps.foundRuntimes().join(",");
214
+ deps.print(`Codex is installed here but was left out: it runs without asking. To allow it, run baychat launch add <folder> --runtime ${all} --unattended`);
215
+ return false;
216
+ }
217
+ deps.print(` ${exports.CODEX_UNATTENDED_NOTICE}`);
218
+ const answer = await deps.ask("Allow Codex in this folder too, on those terms? (y/N) ");
219
+ return /^\s*y(es)?\s*$/i.test(answer);
220
+ }
221
+ /** How to trust a folder for Codex by hand: what Codex itself asks on its first start there. */
222
+ const codexTrustByHand = (folder) => `run \`codex\` in ${folder} once and answer that you trust it.`;
223
+ exports.codexTrustByHand = codexTrustByHand;
224
+ /**
225
+ * Codex's twin of `offerClaudeTrust`: a Codex started from the phone in a folder it has not trusted is refused
226
+ * (FOLDER_NOT_TRUSTED), so `add` offers it here, to the person at this computer, after they typed y.
227
+ */
228
+ async function offerCodexTrust(folder, deps) {
229
+ if (deps.codexTrust.isTrusted(folder))
230
+ return;
231
+ deps.print("Codex hasn't trusted this folder yet, so it can't be started here from BayChat.");
232
+ if (!deps.interactive) {
233
+ deps.print(`To fix that, ${(0, exports.codexTrustByHand)(folder)}`);
234
+ return;
235
+ }
236
+ const answer = await deps.ask("Trust this folder for Codex now? (y/N) ");
237
+ if (!/^\s*y(es)?\s*$/i.test(answer)) {
238
+ deps.print(`Not trusted. Codex can't start here from BayChat until you ${(0, exports.codexTrustByHand)(folder)}`);
239
+ return;
240
+ }
241
+ const result = deps.codexTrust.trust(folder);
242
+ if (result.ok) {
243
+ deps.print(`Trusted. Codex will start in ${folder} (a copy of its old config, if it had one, is in ${deps.codexTrust.file}.bak).`);
244
+ return;
245
+ }
246
+ deps.print(`Couldn't record it: ${(0, codex_trust_1.codexTrustProblemText)(result.reason, deps.codexTrust.file)} Instead, ${(0, exports.codexTrustByHand)(folder)}`);
247
+ }
153
248
  /** How to trust a folder by hand: the one answer Claude Code itself accepts. */
154
249
  const trustByHand = (folder) => `run \`claude\` in ${folder} once and choose "Yes, I trust this folder".`;
155
250
  exports.trustByHand = trustByHand;
@@ -186,7 +281,12 @@ function listSpots(deps) {
186
281
  return 0;
187
282
  }
188
283
  for (const s of policy.spots) {
189
- const flags = [s.unattendedCodex ? "unattended Codex" : "", s.agentStarts ? "agent starts" : ""].filter(Boolean);
284
+ const flags = [
285
+ s.unattendedCodex ? "unattended Codex" : "",
286
+ s.agentStarts ? "agent starts" : "",
287
+ s.phoneMayAllow ? "phone may allow" : "",
288
+ s.phoneAllowedAt ? `allowed from phone ${new Date(s.phoneAllowedAt).toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" })}` : "",
289
+ ].filter(Boolean);
190
290
  deps.print(`${s.id} "${s.label}" ${s.path} [${s.runtimes.join(", ")}] max ${s.maxSessions}${flags.length ? ` (${flags.join(", ")})` : ""}`);
191
291
  }
192
292
  return 0;
@@ -50,8 +50,10 @@ const policy_1 = require("../relay/acp/policy");
50
50
  * Where BayChat may START sessions on this computer: `~/.baychat/launch-policy.json` (0600).
51
51
  *
52
52
  * ── WHAT MAKES THIS A BOUNDARY ────────────────────────────────────────────────────────
53
- * Nothing that arrives over the network writes this file. Only `baychat launch add|remove`, run
54
- * by a person at this machine, changes it. The relay advertises its spots by LABEL only and
53
+ * Only `baychat launch add|remove`, run by a person at this machine, changes it — with ONE narrow
54
+ * exception that person switches on per folder: `phoneMayAllow` lets the computer's owner add one
55
+ * runtime to that existing folder from their phone (relay/launcher/spot-request.ts, which re-checks
56
+ * everything here first). Nothing over the network ever adds a folder. The relay advertises its spots by LABEL only and
55
57
  * re-reads this file for every launch, so a deleted spot stops launches at once. Invalid is
56
58
  * treated as absent: a file we cannot read offers nothing — it never means "allow everything".
57
59
  *
@@ -136,7 +138,7 @@ function resolveSpotFolder(spot) {
136
138
  function parseSpot(entry) {
137
139
  if (!entry || typeof entry !== "object")
138
140
  return null;
139
- const { id, label, path: p, runtimes, maxSessions, unattendedCodex, agentStarts } = entry;
141
+ const { id, label, path: p, runtimes, maxSessions, unattendedCodex, agentStarts, phoneMayAllow, phoneAllowedAt } = entry;
140
142
  if (typeof id !== "string" || !exports.SPOT_ID_RE.test(id))
141
143
  return null;
142
144
  if (typeof label !== "string" || label.trim().length === 0 || label.length > exports.SPOT_LABEL_MAX)
@@ -152,6 +154,10 @@ function parseSpot(entry) {
152
154
  return null;
153
155
  if (agentStarts !== undefined && agentStarts !== true)
154
156
  return null;
157
+ if (phoneMayAllow !== undefined && phoneMayAllow !== true)
158
+ return null;
159
+ if (phoneAllowedAt !== undefined && (typeof phoneAllowedAt !== "string" || Number.isNaN(Date.parse(phoneAllowedAt))))
160
+ return null;
155
161
  return {
156
162
  id,
157
163
  label,
@@ -160,5 +166,7 @@ function parseSpot(entry) {
160
166
  maxSessions,
161
167
  ...(unattendedCodex ? { unattendedCodex } : {}),
162
168
  ...(agentStarts ? { agentStarts } : {}),
169
+ ...(phoneMayAllow ? { phoneMayAllow } : {}),
170
+ ...(typeof phoneAllowedAt === "string" ? { phoneAllowedAt } : {}),
163
171
  };
164
172
  }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ // The board to-do tools of the local stdio MCP server (protocol v2.17): `list_board_cards`,
3
+ // `add_board_card`, `move_board_card`, `update_board_card`. Defined in `tool-defs.ts` (shared with
4
+ // the remote endpoint); each handler here is one agent-API call, rendered for a model to read.
5
+ //
6
+ // A to-do's title and notes are text people and other agents wrote: shown on one line and
7
+ // cleaned like message content, never as structure.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.BOARD_CARD_HANDLERS = void 0;
10
+ exports.renderValue = renderValue;
11
+ exports.renderCard = renderCard;
12
+ exports.renderBoards = renderBoards;
13
+ exports.handleListBoardCards = handleListBoardCards;
14
+ exports.handleAddBoardCard = handleAddBoardCard;
15
+ exports.handleMoveBoardCard = handleMoveBoardCard;
16
+ exports.handleUpdateBoardCard = handleUpdateBoardCard;
17
+ const api_1 = require("./api");
18
+ const mcp_result_1 = require("./mcp-result");
19
+ const terminal_text_1 = require("./relay/terminal-text");
20
+ function oneLine(text) {
21
+ return (0, terminal_text_1.cleanTerminalText)(text).replace(/\s+/g, " ").trim();
22
+ }
23
+ /** A cell as a model reads it: money from cents with its currency, a choice by its label. */
24
+ function renderValue(field, value) {
25
+ if (field.type === "money" && typeof value === "number")
26
+ return `${(value / 100).toFixed(2)} ${field.options?.currency ?? "EUR"}`;
27
+ if (field.type === "select")
28
+ return oneLine(field.options?.choices?.find((c) => c.id === value)?.label ?? String(value));
29
+ return oneLine(String(value));
30
+ }
31
+ /** `- "Title" [id] — Ada, due 2026-11-09, suggestion, yours; Amount 12.50 EUR` */
32
+ function renderCard(card, fields = []) {
33
+ const parts = [
34
+ card.assignee ? oneLine(card.assignee.name) || card.assignee.kind : "nobody",
35
+ ...(card.dueAt ? [`due ${card.dueAt.slice(0, 10)}`] : []),
36
+ ...(card.suggestion ? ["suggestion"] : []),
37
+ ...(card.mine ? ["yours"] : []),
38
+ ];
39
+ const cells = fields.flatMap((f) => (card.values && f.id in card.values ? [`${oneLine(f.name)} ${renderValue(f, card.values[f.id])}`] : []));
40
+ return `- "${oneLine(card.title)}" [${card.id}] — ${parts.join(", ")}${cells.length ? `; ${cells.join(", ")}` : ""}`;
41
+ }
42
+ /** Each board, each column with its to-dos under it. */
43
+ function renderBoards(boards) {
44
+ if (boards.length === 0)
45
+ return "This project has no boards yet.";
46
+ return boards
47
+ .map((board) => {
48
+ const columns = board.columns.map((column) => {
49
+ const cards = board.cards.filter((c) => c.columnId === column.id);
50
+ const head = ` ${oneLine(column.name)} (${column.role}) [${column.id}]`;
51
+ return cards.length === 0 ? `${head}: empty` : `${head}:\n${cards.map((c) => ` ${renderCard(c, board.fields)}`).join("\n")}`;
52
+ });
53
+ const more = board.truncated ? "\n (more to-dos than shown)" : "";
54
+ const fields = board.fields?.length
55
+ ? `\n Fields: ${board.fields.map((f) => `${oneLine(f.name)} (${f.type}${f.type === "money" ? `, ${f.options?.currency ?? "EUR"}, cents` : ""}) [${f.id}]`).join(", ")}`
56
+ : "";
57
+ return `Board "${oneLine(board.name)}" [${board.id}]${fields}\n${columns.join("\n")}${more}`;
58
+ })
59
+ .join("\n\n");
60
+ }
61
+ /** A refusal the model can act on: the code and the reason. */
62
+ function cardError(err) {
63
+ if (err instanceof api_1.ApiError && err.code && /^(CARD_|INVALID_CARD|ASSIGNEE_|BOARD_|COLUMN_|PROJECT_NOT_FOUND)/.test(err.code)) {
64
+ return (0, mcp_result_1.fail)(`${err.code}: ${err.message}`);
65
+ }
66
+ return (0, mcp_result_1.toToolError)(err);
67
+ }
68
+ const base = (conversationId) => `/api/agent-api/conversations/${encodeURIComponent(conversationId)}/board-cards`;
69
+ async function handleListBoardCards(args) {
70
+ try {
71
+ const qs = args.boardId ? `?boardId=${encodeURIComponent(args.boardId)}` : "";
72
+ const res = await (0, api_1.apiRequest)((0, mcp_result_1.requireCredentials)(), "GET", `${base(args.conversationId)}${qs}`);
73
+ return (0, mcp_result_1.ok)(renderBoards(res.boards ?? []), res);
74
+ }
75
+ catch (err) {
76
+ return cardError(err);
77
+ }
78
+ }
79
+ async function handleAddBoardCard(args) {
80
+ try {
81
+ const { conversationId, ...body } = args;
82
+ const res = await (0, api_1.apiRequest)((0, mcp_result_1.requireCredentials)(), "POST", base(conversationId), body);
83
+ const what = res.card.suggestion ? "Suggested" : "Added";
84
+ return (0, mcp_result_1.ok)(`${what} "${oneLine(res.card.title)}" [${res.card.id}].`, res);
85
+ }
86
+ catch (err) {
87
+ return cardError(err);
88
+ }
89
+ }
90
+ async function handleMoveBoardCard(args) {
91
+ try {
92
+ const res = await (0, api_1.apiRequest)((0, mcp_result_1.requireCredentials)(), "POST", `${base(args.conversationId)}/${encodeURIComponent(args.cardId)}/move`, { ...(args.column ? { column: args.column } : {}), ...(args.columnId ? { columnId: args.columnId } : {}) });
93
+ return (0, mcp_result_1.ok)(`Moved "${oneLine(res.card.title)}" [${res.card.id}].`, res);
94
+ }
95
+ catch (err) {
96
+ return cardError(err);
97
+ }
98
+ }
99
+ async function handleUpdateBoardCard(args) {
100
+ try {
101
+ const { conversationId, cardId, ...body } = args;
102
+ const res = await (0, api_1.apiRequest)((0, mcp_result_1.requireCredentials)(), "PATCH", `${base(conversationId)}/${encodeURIComponent(cardId)}`, body);
103
+ return (0, mcp_result_1.ok)(`Saved "${oneLine(res.card.title)}" [${res.card.id}].`, res);
104
+ }
105
+ catch (err) {
106
+ return cardError(err);
107
+ }
108
+ }
109
+ /** Handler per board to-do tool name, registered with the conversation tools (mcp.ts). */
110
+ exports.BOARD_CARD_HANDLERS = {
111
+ list_board_cards: (args) => handleListBoardCards(args),
112
+ add_board_card: (args) => handleAddBoardCard(args),
113
+ move_board_card: (args) => handleMoveBoardCard(args),
114
+ update_board_card: (args) => handleUpdateBoardCard(args),
115
+ };
package/dist/mcp.js CHANGED
@@ -42,6 +42,7 @@ const project_brief_1 = require("./relay/project-brief");
42
42
  const project_notes_1 = require("./relay/project-notes");
43
43
  const context_notice_1 = require("./relay/context-notice");
44
44
  const mcp_notes_1 = require("./mcp-notes");
45
+ const mcp_board_cards_1 = require("./mcp-board-cards");
45
46
  /** The features this server asks the context reads for: it shows the project brief, the notes
46
47
  * delta and BayChat's 90% memory notice (protocol v1.17), so it may be handed them. */
47
48
  const BRIEF_FEATURE = "brief,notes,notice";
@@ -499,6 +500,8 @@ const CONVERSATION_HANDLERS = {
499
500
  get_file: (args) => handleGetFile(args),
500
501
  // Notes (protocol v1.17, mcp-notes.ts).
501
502
  ...mcp_notes_1.NOTE_HANDLERS,
503
+ // Board to-dos (protocol v2.17, mcp-board-cards.ts).
504
+ ...mcp_board_cards_1.BOARD_CARD_HANDLERS,
502
505
  };
503
506
  /**
504
507
  * The conversation definitions as the LOCAL server registers them: the shared