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.
@@ -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
  }
package/dist/main.js CHANGED
@@ -11,6 +11,7 @@ const notify_hook_1 = require("./notify-hook");
11
11
  const notify_answer_1 = require("./notify-answer");
12
12
  const session_name_1 = require("./session-name");
13
13
  const start_command_1 = require("./start-command");
14
+ const attach_command_1 = require("./attach-command");
14
15
  const setup_command_1 = require("./setup-command");
15
16
  const update_command_1 = require("./update-command");
16
17
  const notice_1 = require("./auto-update/notice");
@@ -87,6 +88,9 @@ async function main() {
87
88
  return (0, session_command_1.cmdJoinSession)(args);
88
89
  case "start":
89
90
  return (0, start_command_1.cmdStart)(args);
91
+ case "attach":
92
+ (0, args_1.rejectUnknownFlags)(args, [], "baychat attach [name]");
93
+ return (0, attach_command_1.cmdAttach)(args);
90
94
  case "setup":
91
95
  // Look again and set up what is new (an app installed since); starts nothing.
92
96
  (0, args_1.rejectUnknownFlags)(args, [], "baychat setup");
@@ -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
@@ -22,6 +22,7 @@ exports.handleGetMessages = handleGetMessages;
22
22
  exports.handleSendMessage = handleSendMessage;
23
23
  exports.handleSetTyping = handleSetTyping;
24
24
  exports.handleCreatePoll = handleCreatePoll;
25
+ exports.handleClosePoll = handleClosePoll;
25
26
  exports.handleVoteInPoll = handleVoteInPoll;
26
27
  exports.handleReactToMessage = handleReactToMessage;
27
28
  exports.fileSourceSuffix = fileSourceSuffix;
@@ -42,6 +43,7 @@ const project_brief_1 = require("./relay/project-brief");
42
43
  const project_notes_1 = require("./relay/project-notes");
43
44
  const context_notice_1 = require("./relay/context-notice");
44
45
  const mcp_notes_1 = require("./mcp-notes");
46
+ const mcp_board_cards_1 = require("./mcp-board-cards");
45
47
  /** The features this server asks the context reads for: it shows the project brief, the notes
46
48
  * delta and BayChat's 90% memory notice (protocol v1.17), so it may be handed them. */
47
49
  const BRIEF_FEATURE = "brief,notes,notice";
@@ -302,6 +304,25 @@ async function handleCreatePoll(args) {
302
304
  return (0, mcp_result_1.toToolError)(err);
303
305
  }
304
306
  }
307
+ /**
308
+ * POST /polls/:id/close — close a poll this session asked (or, seated ADMIN in its own Bay's room,
309
+ * any poll there; never through a visit). It keeps its result and takes no more votes. The answer
310
+ * names the final tally in words, as `vote_in_poll` does.
311
+ */
312
+ async function handleClosePoll(args) {
313
+ try {
314
+ const creds = (0, mcp_result_1.requireCredentials)();
315
+ await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/polls/${args.pollId}/close`);
316
+ const view = await (0, api_1.apiRequest)(creds, "GET", `/api/agent-api/polls/${args.pollId}`);
317
+ const tally = view.options
318
+ .map((label, i) => `${label}: ${view.poll.options.find((o) => o.index === i)?.count ?? 0}`)
319
+ .join(", ");
320
+ return (0, mcp_result_1.ok)(`Poll closed. Final tally — ${tally}.`, { pollId: args.pollId, poll: view.poll });
321
+ }
322
+ catch (err) {
323
+ return (0, mcp_result_1.toToolError)(err);
324
+ }
325
+ }
305
326
  /**
306
327
  * POST /polls/:id/vote — this session's own vote.
307
328
  *
@@ -493,12 +514,15 @@ const CONVERSATION_HANDLERS = {
493
514
  send_message: (args) => handleSendMessage(args),
494
515
  create_poll: (args) => handleCreatePoll(args),
495
516
  vote_in_poll: (args) => handleVoteInPoll(args),
517
+ close_poll: (args) => handleClosePoll(args),
496
518
  set_typing: (args) => handleSetTyping(args),
497
519
  react_to_message: (args) => handleReactToMessage(args),
498
520
  list_files: (args) => handleListFiles(args),
499
521
  get_file: (args) => handleGetFile(args),
500
522
  // Notes (protocol v1.17, mcp-notes.ts).
501
523
  ...mcp_notes_1.NOTE_HANDLERS,
524
+ // Board to-dos (protocol v2.17, mcp-board-cards.ts).
525
+ ...mcp_board_cards_1.BOARD_CARD_HANDLERS,
502
526
  };
503
527
  /**
504
528
  * The conversation definitions as the LOCAL server registers them: the shared
@@ -134,23 +134,19 @@ deps) {
134
134
  return multiplexer === "tmux" ? ["new", "-d", "-s", naming.name, "-c", naming.cwd, ...tmuxCommand(naming.name)] : ["-dmS", naming.name, ...commandWith({})];
135
135
  }
136
136
  if (!deps)
137
- throw new Error("multiplexerArgv: a fresh or reattached session needs the launch deps");
137
+ throw new Error("multiplexerArgv: a fresh or named session needs the launch deps");
138
+ // Never `-A` (tmux) or `-R` (screen): both would resume an existing session instead of starting
139
+ // the agent that was asked for.
140
+ const base = naming.kind === "named" ? naming.name : `${naming.prefix}-${deps.pid}`;
138
141
  if (multiplexer === "tmux") {
139
- if (naming.kind === "reattach") {
140
- // `-A` attaches to an existing session of that name instead of failing, so running this
141
- // twice returns you to your agent rather than erroring at you.
142
- return ["new", "-A", "-s", naming.name, ...tmuxCommand(naming.name)];
143
- }
144
- const name = uniqueTmuxName(naming.prefix, deps);
142
+ const name = uniqueTmuxName(base, deps);
145
143
  return ["new", "-s", name, ...tmuxCommand(name)];
146
144
  }
147
- const command = commandWith({});
148
- // screen tells same-named sessions apart by pid, so a fresh one needs no uniqueness check.
149
- return naming.kind === "reattach" ? ["-S", naming.name, "-R", ...command] : ["-S", `${naming.prefix}-${deps.pid}`, ...command];
145
+ // screen tells same-named sessions apart by pid, so a new one needs no uniqueness check.
146
+ return ["-S", base, ...commandWith({})];
150
147
  }
151
- /** `claude-<pid>`, or with a suffix when a tmux session of that name already exists. */
152
- function uniqueTmuxName(prefix, deps) {
153
- const base = `${prefix}-${deps.pid}`;
148
+ /** `base`, or `base-2`, `base-3`… when a tmux session of that name already exists. */
149
+ function uniqueTmuxName(base, deps) {
154
150
  for (let n = 1; n < 100; n++) {
155
151
  const name = n === 1 ? base : `${base}-${n}`;
156
152
  if (!deps.tmuxHasSession(name))