baychat 0.29.0 → 0.29.2

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.0",
3
+ "version": "0.29.2",
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
  }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AUTOSTART_IMPLEMENTATIONS = exports.WINDOWS_RELAY_LAUNCHER = exports.MAC_SEARCH_PATH = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
37
+ exports.replaceLaunchAgent = replaceLaunchAgent;
37
38
  exports.launchdLogPath = launchdLogPath;
38
39
  exports.autostartForPlatform = autostartForPlatform;
39
40
  exports.relayCommand = relayCommand;
@@ -194,14 +195,7 @@ const launchd = {
194
195
  fs.mkdirSync(path.dirname(target), { recursive: true });
195
196
  fs.writeFileSync(target, launchd.unit(cmd), { mode: 0o644 });
196
197
  prepareLaunchdLog();
197
- const domain = `gui/${os.userInfo().uid}`;
198
- // Replacing an agent means removing the old one first; bootout on a label
199
- // that was never loaded exits non-zero, which is not a failure here.
200
- await execFileAsync("launchctl", [
201
- "bootout",
202
- `${domain}/${exports.SERVICE_LABEL}`,
203
- ]).catch(() => undefined);
204
- await execFileAsync("launchctl", ["bootstrap", domain, target]);
198
+ await replaceLaunchAgent(`gui/${os.userInfo().uid}`, exports.SERVICE_LABEL, target);
205
199
  },
206
200
  async uninstall() {
207
201
  const domain = `gui/${os.userInfo().uid}`;
@@ -212,6 +206,51 @@ const launchd = {
212
206
  fs.rmSync(launchd.unitPath(), { force: true });
213
207
  },
214
208
  };
209
+ const realLaunchctl = {
210
+ launchctl: async (args) => {
211
+ await execFileAsync("launchctl", args);
212
+ },
213
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
214
+ };
215
+ /** How long to wait for launchd to finish removing the old agent, and how often to look. */
216
+ const UNLOAD_WAIT_MS = 10_000;
217
+ const UNLOAD_POLL_MS = 250;
218
+ /** Bootstrap attempts, and the pause before each retry. */
219
+ const BOOTSTRAP_RETRY_DELAYS_MS = [1_000, 2_000];
220
+ /**
221
+ * Remove the loaded agent `label` (if any) and bootstrap `plist` in its place.
222
+ *
223
+ * ── WHY IT WAITS AND RETRIES ──────────────────────────────────────────────────────────
224
+ * `baychat update` on a Mac mini (0.28.0 → 0.29.0, 2026-10-05) failed the restart with
225
+ * "bootstrap failed: 5: Input/output error"; `baychat relay start` a moment later worked.
226
+ * launchd answers a bootstrap with EIO while the same label is still being torn down, and
227
+ * `bootout` can return before that teardown has finished. So: bootout, then wait until
228
+ * `launchctl print` no longer finds the label (bounded), then bootstrap — retrying twice,
229
+ * because "no longer printed" and "accepts a new bootstrap" are not quite the same instant.
230
+ * The last failure is rethrown unchanged, so a real refusal (a bad plist) still surfaces.
231
+ */
232
+ async function replaceLaunchAgent(domain, label, plist, deps = realLaunchctl) {
233
+ // Bootout on a label that was never loaded exits non-zero, which is not a failure here.
234
+ await deps.launchctl(["bootout", `${domain}/${label}`]).catch(() => undefined);
235
+ for (let waited = 0; waited < UNLOAD_WAIT_MS; waited += UNLOAD_POLL_MS) {
236
+ const stillLoaded = await deps.launchctl(["print", `${domain}/${label}`]).then(() => true, () => false);
237
+ if (!stillLoaded)
238
+ break;
239
+ await deps.sleep(UNLOAD_POLL_MS);
240
+ }
241
+ for (let attempt = 0;; attempt++) {
242
+ try {
243
+ await deps.launchctl(["bootstrap", domain, plist]);
244
+ return;
245
+ }
246
+ catch (err) {
247
+ const delay = BOOTSTRAP_RETRY_DELAYS_MS[attempt];
248
+ if (delay === undefined)
249
+ throw err;
250
+ await deps.sleep(delay);
251
+ }
252
+ }
253
+ }
215
254
  /**
216
255
  * The PATH the relay — and every session it wakes — runs with on macOS.
217
256
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.29.0",
3
+ "version": "0.29.2",
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"