baychat 0.35.4 → 0.37.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.
@@ -22,17 +22,19 @@
22
22
  // The card text is built from `shown`, which masks obvious secrets; the rule is built from the
23
23
  // real text, because a rule that differs from the call matches nothing.
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.MAX_RULE_COMMAND_LENGTH = void 0;
25
+ exports.CHAINED_LIMITATION = exports.MAX_RULE_COMMAND_LENGTH = void 0;
26
26
  exports.planDeniedCall = planDeniedCall;
27
27
  exports.callKeyOf = callKeyOf;
28
28
  exports.sameCall = sameCall;
29
29
  exports.redactSecrets = redactSecrets;
30
+ const compound_command_1 = require("./compound-command");
30
31
  /** Longer than this is not a command a person can check on a phone, nor one a rule should hold. */
31
32
  exports.MAX_RULE_COMMAND_LENGTH = 1000;
32
33
  /** Claude Code itself saves at most five rules for one approved chain (permissions docs). */
33
34
  const MAX_CHAIN_PARTS = 5;
35
+ /** What the card says for every chain: the agent reads it too (the deny message, approve-hook). */
36
+ exports.CHAINED_LIMITATION = `${compound_command_1.CHAINED_LINE} Ask the session to split it into single commands or a script it runs with one command.`;
34
37
  /** `&&`, `||`, `|&`, `;`, `|`, and a lone `&` that is not part of a redirection (`2>&1`, `&>`). */
35
- const CHAIN_OPERATOR = /&&|\|\||\|&|;|\||(?<![>&])&(?![>&])/;
36
38
  const CHAIN_SPLIT = /\s*(?:&&|\|\||\|&|;|\||(?<![>&])&(?![>&]))\s*/;
37
39
  const NESTED_COMMAND = /`|\$\(|[()]/;
38
40
  const MCP_TOOL = /^mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_.-]+$/;
@@ -75,23 +77,22 @@ function planBash(command) {
75
77
  "approve it at the computer.");
76
78
  }
77
79
  const call = { tool: "Bash", subject: command };
78
- if (CHAIN_OPERATOR.test(command)) {
80
+ // A chain as the shell reads it (compound-command.ts): `echo "a && b"` is one command.
81
+ if ((0, compound_command_1.compoundKind)(command) === "chain") {
79
82
  if (/["'\\]/.test(command)) {
80
- return nothing(shown, "It chains several commands and quotes text, so BayChat cannot split it safely; ask the session to " +
81
- "run them one at a time.");
83
+ return nothing(shown, `${exports.CHAINED_LIMITATION} It also quotes text, so BayChat cannot split it safely; approve it at the computer.`);
82
84
  }
83
85
  const parts = command.split(CHAIN_SPLIT);
84
86
  if (parts.some((p) => p === "") || parts.length > MAX_CHAIN_PARTS) {
85
- return nothing(shown, "BayChat cannot split this chain into exact commands; approve it at the computer.");
87
+ return nothing(shown, `${exports.CHAINED_LIMITATION} BayChat cannot split this one; approve it at the computer.`);
86
88
  }
87
89
  return {
88
90
  shown,
89
91
  onceRules: [...new Set(parts.map((p) => `Bash(${p})`))],
90
92
  alwaysAllowed: false,
91
93
  // One plain line on WHY, because the usual cause is the agent's own habit (`cd … && x | tail`)
92
- // and the fix is for the session to run the command by itself.
93
- limitation: `"Always" isn't offered: this chains ${parts.length} commands, and a saved rule only ever covers one plain ` +
94
- `command. Run it on its own (no cd … &&, no | tail) to be offered "Always".`,
94
+ // and the fix is for the session to run single commands.
95
+ limitation: exports.CHAINED_LIMITATION,
95
96
  call,
96
97
  };
97
98
  }
@@ -88,11 +88,13 @@ const approve_hook_1 = require("./approve-hook");
88
88
  const approval_wait_1 = require("./approval-wait");
89
89
  const config_1 = require("./config");
90
90
  const hook_stdin_1 = require("./hook-stdin");
91
+ const claims_1 = require("./denied/claims");
91
92
  const confirm_1 = require("./denied/confirm");
92
93
  const grants_1 = require("./denied/grants");
93
94
  const hook_log_1 = require("./denied/hook-log");
94
95
  const live_session_1 = require("./denied/live-session");
95
96
  const notice_1 = require("./denied/notice");
97
+ const opt_in_1 = require("./denied/opt-in");
96
98
  const permission_rule_1 = require("./denied/permission-rule");
97
99
  const settings_rules_1 = require("./denied/settings-rules");
98
100
  const transcript_1 = require("./denied/transcript");
@@ -180,6 +182,7 @@ function parseArgs(argv) {
180
182
  return {
181
183
  session: at("--session"),
182
184
  fromClaude: argv.includes("--session-from-claude"),
185
+ plugin: argv.includes("--plugin"),
183
186
  waitSeconds: Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : approve_hook_1.DEFAULT_WAIT_SECONDS,
184
187
  };
185
188
  }
@@ -227,6 +230,22 @@ async function decideDenied(rawStdin, argv = [], deps = {}) {
227
230
  log(`${LOG_LABEL}: this session has not joined BayChat yet; the denial stands.`);
228
231
  return null;
229
232
  }
233
+ // The switch decides on every denial: the plugin's hook runs in every session, and a launched
234
+ // session's own hook was written while the switch was not off (it may have changed since).
235
+ const mode = (deps.cardMode ?? opt_in_1.deniedCardMode)();
236
+ if (mode === "off") {
237
+ log(`${LOG_LABEL}: the denied-command card is off on this computer; the denial stands.`);
238
+ return null;
239
+ }
240
+ const eventId = str(input.tool_use_id);
241
+ if (opts.plugin && !eventId) {
242
+ log(`${LOG_LABEL}: the event has no tool_use_id to take it by; the denial stands.`);
243
+ return null;
244
+ }
245
+ if (eventId && !(deps.claimEvent ?? claims_1.claimDeniedEvent)(eventId)) {
246
+ log(`${LOG_LABEL}: another BayChat hook already took this denial.`);
247
+ return null;
248
+ }
230
249
  const device = (deps.loadDevice ?? config_1.loadDeviceCredentials)();
231
250
  if (!device) {
232
251
  log(`${LOG_LABEL}: this machine is not logged in to BayChat (\`baychat login\`); the denial stands.`);
@@ -301,7 +320,13 @@ async function decideDenied(rawStdin, argv = [], deps = {}) {
301
320
  limitation: `BayChat can't allow it from here: ${choice.why}, so no rule can be saved. ` +
302
321
  "Approve it at the computer (/permissions → Recently denied).",
303
322
  }
304
- : planned;
323
+ : mode === "once" && planned.alwaysAllowed
324
+ ? {
325
+ ...planned,
326
+ alwaysAllowed: false,
327
+ limitation: "\"Always\" is off on this computer (`baychat permissions on` at the computer turns it on).",
328
+ }
329
+ : planned;
305
330
  const target = choice.scope === "none" ? undefined : choice;
306
331
  const question = composeDeniedQuestion({
307
332
  who,
@@ -33,7 +33,7 @@ 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.APPROVALS_NOTICE = exports.LAUNCH_USAGE = void 0;
36
+ exports.trustByHand = exports.CODEX_UNATTENDED_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");
@@ -41,13 +41,18 @@ 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
43
  const policy_2 = require("./policy");
44
- exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>] [--unattended]
44
+ exports.LAUNCH_USAGE = `Usage: baychat launch add <folder> [--label <label>] [--runtime claude,codex] [--max <n>] [--unattended] [--agent-starts]
45
45
  Let BayChat start sessions in <folder> from the app
46
46
  (--unattended: also let it start Codex there, which then never asks)
47
+ (--agent-starts: agents you allowed "Always" may start sessions there without asking you each time)
47
48
  baychat launch list The folders BayChat may start sessions in
48
49
  baychat launch remove <id> Stop offering one of them`;
49
50
  /** Printed by every `add`: the one thing a person must know before saying yes. */
50
51
  exports.APPROVALS_NOTICE = "Claude Code sessions started here from BayChat ask for approvals in BayChat.";
52
+ /** Printed by an `add … --agent-starts`: what the second key opens, and what it never does. */
53
+ 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
+ "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
+ "Those sessions ask for approvals in BayChat like any other; they never start Codex unattended. Revoke an agent in Settings → Devices.";
51
56
  /** Printed when Codex is offered: what it may do without asking, and what it asks on the phone. */
52
57
  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, " +
53
58
  "and asks for approval in BayChat — on the session owner's phone — before anything more (a command outside its sandbox, network access, files elsewhere). " +
@@ -89,6 +94,7 @@ async function addSpot(args, deps) {
89
94
  const runtimeList = (0, args_1.flag)(args, "--runtime");
90
95
  const maxRaw = (0, args_1.flag)(args, "--max");
91
96
  const unattended = args.includes("--unattended");
97
+ const agentStarts = args.includes("--agent-starts");
92
98
  const valued = new Set([label, runtimeList, maxRaw].filter((v) => v !== undefined));
93
99
  const folderArg = args.find((a) => !a.startsWith("--") && !valued.has(a));
94
100
  if (unattended && !(runtimeList ?? "").split(",").map((r) => r.trim()).includes("codex")) {
@@ -127,11 +133,14 @@ async function addSpot(args, deps) {
127
133
  runtimes: [...new Set(requested)],
128
134
  maxSessions,
129
135
  ...(unattended ? { unattendedCodex: true } : {}),
136
+ ...(agentStarts ? { agentStarts: true } : {}),
130
137
  };
131
138
  (0, policy_2.saveLaunchPolicy)((0, policy_2.upsertSpot)(policy, spot), deps.policyFile);
132
139
  deps.print(`${existing ? "Updated" : "Added"} "${spot.label}" (${spot.id}): ${spot.path}`);
133
140
  deps.print(` Agents: ${spot.runtimes.join(", ")} · at most ${spot.maxSessions} at once`);
134
141
  deps.print(` ${exports.APPROVALS_NOTICE}`);
142
+ if (spot.agentStarts)
143
+ deps.print(` ${exports.AGENT_STARTS_NOTICE}`);
135
144
  if (spot.runtimes.includes("codex")) {
136
145
  deps.print(spot.unattendedCodex
137
146
  ? ` ${exports.CODEX_UNATTENDED_NOTICE}`
@@ -176,8 +185,10 @@ function listSpots(deps) {
176
185
  deps.print("BayChat may not start sessions on this computer. Add a folder with `baychat launch add <folder>`.");
177
186
  return 0;
178
187
  }
179
- for (const s of policy.spots)
180
- deps.print(`${s.id} "${s.label}" ${s.path} [${s.runtimes.join(", ")}] max ${s.maxSessions}`);
188
+ for (const s of policy.spots) {
189
+ const flags = [s.unattendedCodex ? "unattended Codex" : "", s.agentStarts ? "agent starts" : ""].filter(Boolean);
190
+ deps.print(`${s.id} "${s.label}" ${s.path} [${s.runtimes.join(", ")}] max ${s.maxSessions}${flags.length ? ` (${flags.join(", ")})` : ""}`);
191
+ }
181
192
  return 0;
182
193
  }
183
194
  function removeSpotById(args, deps) {
@@ -136,7 +136,7 @@ function resolveSpotFolder(spot) {
136
136
  function parseSpot(entry) {
137
137
  if (!entry || typeof entry !== "object")
138
138
  return null;
139
- const { id, label, path: p, runtimes, maxSessions, unattendedCodex } = entry;
139
+ const { id, label, path: p, runtimes, maxSessions, unattendedCodex, agentStarts } = entry;
140
140
  if (typeof id !== "string" || !exports.SPOT_ID_RE.test(id))
141
141
  return null;
142
142
  if (typeof label !== "string" || label.trim().length === 0 || label.length > exports.SPOT_LABEL_MAX)
@@ -150,5 +150,15 @@ function parseSpot(entry) {
150
150
  return null;
151
151
  if (unattendedCodex !== undefined && unattendedCodex !== true)
152
152
  return null;
153
- return { id, label, path: p, runtimes: [...new Set(runtimes)], maxSessions, ...(unattendedCodex ? { unattendedCodex } : {}) };
153
+ if (agentStarts !== undefined && agentStarts !== true)
154
+ return null;
155
+ return {
156
+ id,
157
+ label,
158
+ path: p,
159
+ runtimes: [...new Set(runtimes)],
160
+ maxSessions,
161
+ ...(unattendedCodex ? { unattendedCodex } : {}),
162
+ ...(agentStarts ? { agentStarts } : {}),
163
+ };
154
164
  }
package/dist/main.js CHANGED
@@ -6,6 +6,7 @@ const approve_hook_1 = require("./approve-hook");
6
6
  const denied_hook_1 = require("./denied-hook");
7
7
  const permissions_command_1 = require("./permissions-command");
8
8
  const context_hooks_command_1 = require("./context-hooks-command");
9
+ const channel_check_1 = require("./channel/channel-check");
9
10
  const session_name_1 = require("./session-name");
10
11
  const start_command_1 = require("./start-command");
11
12
  const update_command_1 = require("./update-command");
@@ -337,6 +338,9 @@ async function main() {
337
338
  return await (0, permissions_command_1.cmdPermissions)(args);
338
339
  case "context-hooks":
339
340
  return await (0, context_hooks_command_1.cmdContextHooks)(args);
341
+ case "channel-check":
342
+ // The plugin's SessionStart (compact) hook. Never throws, always 0 (channel/channel-check.ts).
343
+ return await (0, channel_check_1.cmdChannelCheck)();
340
344
  case "doctor":
341
345
  // Deliberately placed with the setup commands: it is the thing you run
342
346
  // when one of them did not take, and the one command that inspects all
package/dist/mcp.js CHANGED
@@ -252,6 +252,9 @@ async function handleSendMessage(args) {
252
252
  // message that quotes nothing must not claim to quote something.
253
253
  ...(args.replyToMessageId ? { replyToMessageId: args.replyToMessageId } : {}),
254
254
  ...(args.card ? { card: args.card } : {}),
255
+ ...(args.threadRootId ? { threadRootId: args.threadRootId } : {}),
256
+ ...(args.handoff !== undefined ? { handoff: args.handoff } : {}),
257
+ ...(args.result !== undefined ? { result: args.result } : {}),
255
258
  });
256
259
  const carried = ids.length > 0 ? ` with ${ids.length} attachment${ids.length === 1 ? "" : "s"}` : "";
257
260
  return (0, mcp_result_1.ok)(`Sent message ${message.id} at ${message.createdAt}${carried}.`, message);
@@ -26,9 +26,12 @@ async function cmdPermissions(args, deps = {}) {
26
26
  const [sub = "list", ...rest] = args;
27
27
  switch (sub) {
28
28
  case "list": {
29
- out((0, opt_in_1.deniedCardEnabled)()
30
- ? "The denied-command card is ON for sessions BayChat starts (`baychat permissions off` turns it off)."
31
- : "The denied-command card is OFF (`baychat permissions on` turns it on).");
29
+ const mode = (0, opt_in_1.deniedCardMode)();
30
+ out(mode === "on"
31
+ ? "The denied-command card is ON, with Always, for sessions joined to BayChat (`baychat permissions off` turns it off)."
32
+ : mode === "once"
33
+ ? "The denied-command card offers Allow once for sessions joined to BayChat (`baychat permissions on` adds Always, `off` turns it off)."
34
+ : "The denied-command card is OFF (`baychat permissions on` turns it on).");
32
35
  const grants = (0, grants_1.listGrants)();
33
36
  if (grants.length === 0) {
34
37
  out("BayChat keeps no permission rules on this computer.");
@@ -60,13 +63,13 @@ async function cmdPermissions(args, deps = {}) {
60
63
  return 1;
61
64
  }
62
65
  (0, opt_in_1.enableDeniedCard)();
63
- out("The denied-command card is ON. Sessions BayChat starts from now on will put a card in BayChat when");
66
+ out("The denied-command card is ON, with Always. A session joined to BayChat puts a card in BayChat when");
64
67
  out("Claude Code's auto mode blocks a call. Read docs/features/DENIED_COMMAND_CARD.md for what it can and can't allow.");
65
68
  return 0;
66
69
  }
67
70
  case "off":
68
71
  (0, opt_in_1.disableDeniedCard)();
69
- out("The denied-command card is OFF for sessions BayChat starts from now on. Rules it kept stay until removed.");
72
+ out("The denied-command card is OFF. Rules it kept stay until removed.");
70
73
  return 0;
71
74
  case "sweep": {
72
75
  const now = deps.now ?? Date.now;