baychat 0.29.1 → 0.29.3

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.29.1",
3
+ "version": "0.29.3",
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": {
@@ -37,6 +37,7 @@ exports.TRUST_KEY = void 0;
37
37
  exports.claudeConfigPath = claudeConfigPath;
38
38
  exports.trustKeysFor = trustKeysFor;
39
39
  exports.isFolderTrusted = isFolderTrusted;
40
+ exports.trustWalk = trustWalk;
40
41
  exports.trustProblemText = trustProblemText;
41
42
  exports.trustFolder = trustFolder;
42
43
  const fs = __importStar(require("fs"));
@@ -113,16 +114,48 @@ function readConfig(file) {
113
114
  }
114
115
  return { ok: true, raw, json: json };
115
116
  }
116
- /** Read-only: has Claude Code's trust dialog been accepted for `folder` (under any of its names)? */
117
- function isFolderTrusted(folder, file = claudeConfigPath()) {
117
+ /**
118
+ * Read-only: would Claude Code skip its trust dialog in `folder` (under any of its names)?
119
+ *
120
+ * Trust is INHERITED, as Claude Code checks it (verified in 2.1.289): a folder counts as trusted
121
+ * when it or any folder above it was accepted — but inside a git repository the walk stops at the
122
+ * repository's root, so a trusted home folder does not reach into a repo below it. Checking only
123
+ * the folder's own key refused every spot under an already-trusted home, where Claude Code never
124
+ * asks and so the person had no way to record it.
125
+ */
126
+ function isFolderTrusted(folder, file = claudeConfigPath(), gitRootOf = findGitRoot) {
118
127
  const parsed = readConfig(file);
119
128
  if (!parsed.ok)
120
129
  return false;
121
130
  const projects = (parsed.json.projects ?? {});
122
- return trustKeysFor(folder).some((key) => {
131
+ const accepted = (key) => {
123
132
  const entry = Object.prototype.hasOwnProperty.call(projects, key) ? projects[key] : undefined;
124
133
  return !!entry && typeof entry === "object" && entry[exports.TRUST_KEY] === true;
125
- });
134
+ };
135
+ return trustKeysFor(folder).some((key) => trustWalk(key, gitRootOf).some(accepted));
136
+ }
137
+ /**
138
+ * `start` and the folders above it that Claude Code consults: up to the enclosing git root
139
+ * (inclusive) when there is one, else up to the filesystem root.
140
+ */
141
+ function trustWalk(start, gitRootOf = findGitRoot) {
142
+ const from = path.resolve(start);
143
+ const stop = gitRootOf(from);
144
+ const out = [];
145
+ for (let dir = from;; dir = path.dirname(dir)) {
146
+ out.push(dir);
147
+ if (dir === stop || path.dirname(dir) === dir)
148
+ return out;
149
+ }
150
+ }
151
+ /** The nearest folder at or above `dir` holding a `.git` (a directory, or a file for worktrees). */
152
+ function findGitRoot(dir) {
153
+ for (let d = dir;; d = path.dirname(d)) {
154
+ if (fs.existsSync(path.join(d, ".git")))
155
+ return d;
156
+ if (path.dirname(d) === d)
157
+ return null;
158
+ }
126
159
  }
127
160
  /** A sentence for each problem, for the person at the terminal. */
128
161
  function trustProblemText(reason, file) {
@@ -209,13 +242,14 @@ function serialiseLike(raw, value) {
209
242
  return raw.endsWith("\n") ? `${body}\n` : body;
210
243
  }
211
244
  /**
212
- * A number JSON.parse cannot hold exactly (more than 15 significant digits) would be silently
213
- * changed by the rewrite. Strings are blanked first so ids and tokens inside quotes don't count.
245
+ * A number the rewrite would not write back character for character: a 20-digit integer
246
+ * JSON.parse rounds, or a `1.0` / `1E5` JSON.stringify respells. Strings are blanked first so ids
247
+ * and tokens inside quotes don't count.
248
+ *
249
+ * Not a digit count: Claude Code itself writes costs like `41.32949860000002` (16+ significant
250
+ * digits) that round-trip exactly, so counting digits refused nearly every real file.
214
251
  */
215
252
  function hasLossyNumbers(raw) {
216
253
  const outsideStrings = raw.replace(/"(?:[^"\\]|\\.)*"/g, '""');
217
- return (outsideStrings.match(/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g) ?? []).some((n) => {
218
- const digits = n.replace(/^-/, "").split(/[eE]/)[0].replace(".", "").replace(/^0+/, "");
219
- return digits.length > 15;
220
- });
254
+ return (outsideStrings.match(/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g) ?? []).some((n) => JSON.stringify(Number(n)) !== n);
221
255
  }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.TERMINAL_SETTINGS_FILE = exports.JOIN_ALLOW = void 0;
36
+ exports.TERMINAL_SETTINGS_FILE = exports.UNATTENDED_SETTINGS = exports.JOIN_ALLOW = void 0;
37
37
  exports.approvalPathFor = approvalPathFor;
38
38
  exports.launchDir = launchDir;
39
39
  exports.approvalSettingsPath = approvalSettingsPath;
@@ -94,10 +94,24 @@ function permissionHook(cli, flags) {
94
94
  ],
95
95
  };
96
96
  }
97
- /** The settings object for a session started from BayChat. Pure; the test pins its exact content. */
98
- function approvalSettings(sessionName, cli) {
97
+ /**
98
+ * Claude Code nudges that stop a session with a menu nobody can answer, switched off for
99
+ * launched sessions only. "Teach auto mode about your environment?" (1 Yes / 2 Not now / 3 Don't
100
+ * show again) stopped Karmen's first WSL launch (2026-10-06). In Claude Code 2.1.289 that nudge,
101
+ * and the /auto-mode-setup tip, are offered only while `skillOverrides["auto-mode-setup"]` is not
102
+ * "off" (read from its code: the gate `x7()`). Off here costs nothing: the skill needs a person.
103
+ */
104
+ exports.UNATTENDED_SETTINGS = { skillOverrides: { "auto-mode-setup": "off" } };
105
+ /**
106
+ * The settings object for a session started from BayChat. Pure; the test pins its exact content.
107
+ * `bayEntry`: the MCP connection the session joins on, when it is not the default `baychat`
108
+ * (a computer logged in to several Bays) — its tools are join tools too.
109
+ */
110
+ function approvalSettings(sessionName, cli, bayEntry) {
111
+ const extra = bayEntry && bayEntry !== "baychat" ? [`mcp__${bayEntry}__*`] : [];
99
112
  return {
100
- permissions: { allow: [...exports.JOIN_ALLOW] },
113
+ ...exports.UNATTENDED_SETTINGS,
114
+ permissions: { allow: [...exports.JOIN_ALLOW, ...extra] },
101
115
  hooks: permissionHook(cli, ["--session", sessionName]),
102
116
  };
103
117
  }
@@ -129,9 +143,9 @@ function currentCliLaunch() {
129
143
  return { node: fs.realpathSync(process.execPath), entry: fs.realpathSync(process.argv[1] ?? "") };
130
144
  }
131
145
  /** Write the session's settings file (0600) and return its path. `sessionName` must already be validated. */
132
- function writeApprovalSettings(sessionName, cli, dir = launchDir()) {
146
+ function writeApprovalSettings(sessionName, cli, dir = launchDir(), bayEntry) {
133
147
  const file = approvalSettingsPath(sessionName, dir);
134
- writePrivateJson(dir, file, approvalSettings(sessionName, cli));
148
+ writePrivateJson(dir, file, approvalSettings(sessionName, cli, bayEntry));
135
149
  return file;
136
150
  }
137
151
  /** Delete the session's settings file. Idempotent: an absent file is already deleted. */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LAUNCH_SESSION_PREFIX = void 0;
3
+ exports.UNATTENDED_NOTE = exports.LAUNCH_SESSION_PREFIX = void 0;
4
4
  exports.multiplexerSessionName = multiplexerSessionName;
5
5
  exports.baychatPrompt = baychatPrompt;
6
6
  exports.buildLaunchCommand = buildLaunchCommand;
@@ -29,13 +29,22 @@ exports.LAUNCH_SESSION_PREFIX = "bc-";
29
29
  function multiplexerSessionName(sessionName) {
30
30
  return `${exports.LAUNCH_SESSION_PREFIX}${sessionName}`;
31
31
  }
32
- /** The first message Claude Code receives: the BayChat skill, joining the chosen room. */
33
- function baychatPrompt(sessionName, join) {
34
- if (!join)
35
- return `/baychat ${sessionName}`;
36
- if ("sessions" in join)
37
- return `/baychat ${sessionName} --sessions`;
38
- return `/baychat ${sessionName} --group "${join.group}"`;
32
+ /**
33
+ * Said after every launched `/baychat` line. Nobody is at a launched session's keyboard, and
34
+ * before it joins nothing it asks can reach a phone (the approve-hook finds the session by name,
35
+ * so it refuses everything until then). A question there waits forever in a pane nobody sees —
36
+ * Karmen's first WSL test sat at "Which Bay?" (2026-10-06).
37
+ */
38
+ exports.UNATTENDED_NOTE = "(Started from the BayChat app: nobody is at this keyboard. Never ask anything before you have joined; if you cannot join, say why in one line and stop.)";
39
+ /**
40
+ * The first message Claude Code receives: the BayChat skill, joining the chosen room — on the
41
+ * named Bay connection when the computer has several, so the skill never asks which.
42
+ */
43
+ function baychatPrompt(sessionName, join, bayEntry) {
44
+ const room = !join ? "" : "sessions" in join ? " --sessions" : ` --group "${join.group}"`;
45
+ const bay = bayEntry ? ` --bay ${bayEntry}` : "";
46
+ const connection = bayEntry ? ` Use only the ${bayEntry} connection (its mcp__${bayEntry}__ tools).` : "";
47
+ return `/baychat ${sessionName}${room}${bay} ${exports.UNATTENDED_NOTE}${connection}`;
39
48
  }
40
49
  function buildLaunchCommand(input) {
41
50
  const name = multiplexerSessionName(input.sessionName);
@@ -44,7 +53,7 @@ function buildLaunchCommand(input) {
44
53
  args: [
45
54
  ...input.channelArgs,
46
55
  ...(input.settingsFile ? ["--settings", input.settingsFile] : []),
47
- baychatPrompt(input.sessionName, input.join),
56
+ baychatPrompt(input.sessionName, input.join, input.bayEntry),
48
57
  ],
49
58
  env: input.env,
50
59
  });
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CONFIRM_DEADLINE_MS = exports.WARNING_POLL_MS = exports.WARNING_WATCH_MS = void 0;
4
4
  exports.handleLaunch = handleLaunch;
5
5
  exports.parseLaunch = parseLaunch;
6
+ exports.launchBayEntry = launchBayEntry;
7
+ const bays_1 = require("../../bays");
6
8
  const policy_1 = require("../../launch/policy");
7
9
  const argv_1 = require("./argv");
8
10
  const approval_settings_1 = require("./approval-settings");
@@ -49,6 +51,11 @@ function startSession(launch, deps) {
49
51
  return refused("RUNTIME_NOT_YET");
50
52
  if (launch.join && "group" in launch.join && (0, argv_1.roomTitleProblem)(launch.join.group))
51
53
  return refused("ROOM_INVALID");
54
+ const bay = launchBayEntry(launch.bay, deps.loadBays());
55
+ if (!bay) {
56
+ deps.log(`launch ${launch.id}: refused, this computer is not logged in to the Bay "${launch.bay?.name ?? ""}". Run \`baychat login\` here.`);
57
+ return refused("BAY_NOT_CONNECTED");
58
+ }
52
59
  const folder = deps.resolveFolder(spot);
53
60
  if (!folder.ok) {
54
61
  deps.log(`launch ${launch.id}: refused, ${folder.reason}`);
@@ -79,7 +86,7 @@ function startSession(launch, deps) {
79
86
  return refused("RUNTIME_MISSING");
80
87
  // Monitor: no channel flag and no channel marker — the session listens with listen_messages + Monitor.
81
88
  const channel = launch.listen === "monitor" ? { args: [], env: {} } : deps.channelLaunch(binary.path);
82
- const settingsFile = (0, approval_settings_1.approvalPathFor)(launch.sessionName) === "hook" ? (0, approval_settings_1.writeApprovalSettings)(launch.sessionName, deps.cli, deps.launchDir) : undefined;
89
+ const settingsFile = (0, approval_settings_1.approvalPathFor)(launch.sessionName) === "hook" ? (0, approval_settings_1.writeApprovalSettings)(launch.sessionName, deps.cli, deps.launchDir, bay.entry) : undefined;
83
90
  const command = (0, argv_1.buildLaunchCommand)({
84
91
  multiplexer,
85
92
  sessionName: launch.sessionName,
@@ -89,6 +96,7 @@ function startSession(launch, deps) {
89
96
  env: { ...channel.env, PATH: deps.pathEnv },
90
97
  settingsFile,
91
98
  join: launch.join,
99
+ bayEntry: bay.entry,
92
100
  });
93
101
  const res = deps.run(command.file, command.args, { cwd: command.cwd, env: command.env });
94
102
  if (res.error || res.status !== 0) {
@@ -286,10 +294,31 @@ function parseLaunch(raw) {
286
294
  listen,
287
295
  requestedBy: { id: requestedBy.id, name: requestedBy.name },
288
296
  requestedByIsOwner: l.requestedByIsOwner === true,
297
+ ...parseBay(l.bay),
289
298
  // Only an explicit `true` authorises; a missing or odd value is a refusal.
290
299
  commandAuthorized: l.commandAuthorized === true,
291
300
  };
292
301
  }
302
+ /**
303
+ * Which of this computer's Bay connections a launch joins on. `entry` is the MCP connection to
304
+ * name in the `/baychat` line — undefined when there is only one, so there is nothing to choose
305
+ * (a one-Bay computer's line stays exactly as before). Null: the launch's Bay has no connection
306
+ * here, and the session must not be started into some other Bay.
307
+ *
308
+ * The launcher runs in the DEFAULT Bay's relay, so a launch normally names that Bay; an older
309
+ * server names none, which means the same.
310
+ */
311
+ function launchBayEntry(bay, bays) {
312
+ // No saved Bay set (an older login, or --token): the one `baychat` connection, as always.
313
+ if (bays.length === 0)
314
+ return { entry: undefined };
315
+ const found = bay ? bays.find((b) => b.bayId === bay.id) : bays.find((b) => b.isDefault);
316
+ if (!found)
317
+ return null;
318
+ if (bays.length < 2)
319
+ return { entry: undefined };
320
+ return { entry: found.isDefault ? bays_1.DEFAULT_MCP_ENTRY : (0, bays_1.bayEntryName)(found, bays) };
321
+ }
293
322
  /** `null` is "join nothing"; undefined is a join that is present but malformed. */
294
323
  function parseJoin(raw) {
295
324
  if (raw === null || raw === undefined)
@@ -301,3 +330,8 @@ function parseJoin(raw) {
301
330
  return { sessions: true };
302
331
  return undefined;
303
332
  }
333
+ /** An optional `{id, name}`; anything else is treated as absent (an older server sends none). */
334
+ function parseBay(raw) {
335
+ const b = raw;
336
+ return b && typeof b.id === "string" && b.id !== "" ? { bay: { id: b.id, name: typeof b.name === "string" ? b.name : "" } } : {};
337
+ }
@@ -38,6 +38,7 @@ exports.startLauncher = startLauncher;
38
38
  const fs = __importStar(require("fs"));
39
39
  const api_1 = require("../../api");
40
40
  const launch_1 = require("../../channel/launch");
41
+ const config_1 = require("../../config");
41
42
  const claude_trust_1 = require("../../launch/claude-trust");
42
43
  const policy_1 = require("../../launch/policy");
43
44
  const runtime_binary_1 = require("../../runtime-binary");
@@ -148,6 +149,7 @@ function realLoopDeps(log, signal) {
148
149
  loadPolicy: () => (0, policy_1.loadLaunchPolicy)(),
149
150
  resolveFolder: policy_1.resolveSpotFolder,
150
151
  isFolderTrusted: (folder) => (0, claude_trust_1.isFolderTrusted)(folder),
152
+ loadBays: () => (0, config_1.loadBayLogins)(),
151
153
  resolveBinary: (name) => (0, runtime_binary_1.resolveRuntimeBinary)(name, binaryEnv()),
152
154
  channelLaunch: (claudePath) => {
153
155
  const launch = (0, launch_1.channelLaunchFor)(claudePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
5
5
  "bin": {
6
6
  "baychat": "dist/index.js"