baychat 0.25.2 → 0.25.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.
@@ -33,9 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AUTOSTART_IMPLEMENTATIONS = exports.WINDOWS_RELAY_LAUNCHER = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
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.launchdLogPath = launchdLogPath;
37
38
  exports.autostartForPlatform = autostartForPlatform;
38
39
  exports.relayCommand = relayCommand;
40
+ exports.stableNodePath = stableNodePath;
39
41
  // Making the relay come back by itself, on all three platforms.
40
42
  //
41
43
  // THE BUG THIS EXISTS TO FIX. `baychat relay start` used to write a systemd user
@@ -174,7 +176,13 @@ const launchd = {
174
176
  <dict>
175
177
  <key>NODE_ENV</key>
176
178
  <string>production</string>
179
+ <key>PATH</key>
180
+ <string>${exports.MAC_SEARCH_PATH}</string>
177
181
  </dict>
182
+ <key>StandardOutPath</key>
183
+ <string>${escapeXml(launchdLogPath())}</string>
184
+ <key>StandardErrorPath</key>
185
+ <string>${escapeXml(launchdLogPath())}</string>
178
186
  <key>ProcessType</key>
179
187
  <string>Background</string>
180
188
  </dict>
@@ -185,6 +193,7 @@ const launchd = {
185
193
  const target = launchd.unitPath();
186
194
  fs.mkdirSync(path.dirname(target), { recursive: true });
187
195
  fs.writeFileSync(target, launchd.unit(cmd), { mode: 0o644 });
196
+ prepareLaunchdLog();
188
197
  const domain = `gui/${os.userInfo().uid}`;
189
198
  // Replacing an agent means removing the old one first; bootout on a label
190
199
  // that was never loaded exits non-zero, which is not a failure here.
@@ -203,6 +212,46 @@ const launchd = {
203
212
  fs.rmSync(launchd.unitPath(), { force: true });
204
213
  },
205
214
  };
215
+ /**
216
+ * The PATH the relay — and every session it wakes — runs with on macOS.
217
+ *
218
+ * launchd starts an agent with `/usr/bin:/bin:/usr/sbin:/sbin` and nothing else
219
+ * (measured on a Mac mini, 2026-09-27), so a woken session could not find `gh`,
220
+ * `docker` or anything else Homebrew installed. This is the PATH a Mac Terminal has:
221
+ * Homebrew's two prefixes (Apple silicon), `/usr/local/bin` (Homebrew on Intel, and
222
+ * most installers), then the system's own. A missing directory in PATH costs nothing.
223
+ * Node's own directory is added again at spawn time — see ./spawn-env.ts.
224
+ */
225
+ exports.MAC_SEARCH_PATH = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
226
+ /** Past this size the log is set aside when the relay is (re)installed. */
227
+ const LAUNCHD_LOG_MAX_BYTES = 10 * 1024 * 1024;
228
+ /**
229
+ * Where the macOS relay's output goes: the per-user Logs folder, which Console.app
230
+ * lists under Log Reports. systemd keeps a unit's output in the journal; launchd
231
+ * throws it away unless the plist names a file.
232
+ */
233
+ function launchdLogPath() {
234
+ return path.join(os.homedir(), "Library", "Logs", "BayChat", "relay.log");
235
+ }
236
+ /**
237
+ * Make sure the log's folder exists, and set a large log aside.
238
+ *
239
+ * launchd appends and never rotates. `relay start` runs at every install and every
240
+ * `baychat update`, so keeping one previous file here bounds the log without a
241
+ * rotation daemon (newsyslog needs root).
242
+ */
243
+ function prepareLaunchdLog() {
244
+ const log = launchdLogPath();
245
+ fs.mkdirSync(path.dirname(log), { recursive: true });
246
+ try {
247
+ if (fs.statSync(log).size > LAUNCHD_LOG_MAX_BYTES)
248
+ fs.renameSync(log, `${log}.1`);
249
+ }
250
+ catch (err) {
251
+ if (err.code !== "ENOENT")
252
+ throw err;
253
+ }
254
+ }
206
255
  /**
207
256
  * Windows — a Task Scheduler task, triggered at logon.
208
257
  *
@@ -370,10 +419,36 @@ function relayCommand(physicalPath = (value) => fs.realpathSync.native(value)) {
370
419
  // be absent from Task Scheduler's filesystem view. Resolve the actual file
371
420
  // before handing it to a process outside that package (observed on Windows).
372
421
  return {
373
- node: physicalPath(process.execPath),
422
+ node: stableNodePath(physicalPath(process.execPath), physicalPath),
374
423
  entry: physicalPath(path.resolve(process.argv[1] ?? "")),
375
424
  };
376
425
  }
426
+ /** `<prefix>/Cellar/<formula>/<version>/bin/node` — one Homebrew version's own folder. */
427
+ const HOMEBREW_CELLAR_NODE = /^(.*)\/Cellar\/([^/]+)\/[^/]+\/bin\/node$/;
428
+ /**
429
+ * The same node, by a path that survives `brew upgrade`.
430
+ *
431
+ * Homebrew installs every version into its own `Cellar/<formula>/<version>` folder and
432
+ * deletes the old one on upgrade; `process.execPath` names that folder. A unit written
433
+ * with it stops starting after the next upgrade, silently, at the next login. The
434
+ * formula's `opt/<formula>` link always points at the installed version, so it is used —
435
+ * but only when it resolves to this very node now, so nothing changes about which node
436
+ * runs today. Any other install (system, nvm, Windows) is returned unchanged.
437
+ */
438
+ function stableNodePath(node, physicalPath) {
439
+ const match = HOMEBREW_CELLAR_NODE.exec(node);
440
+ if (!match)
441
+ return node;
442
+ const [, prefix, formula] = match;
443
+ const opt = `${prefix}/opt/${formula}/bin/node`;
444
+ try {
445
+ return physicalPath(opt) === node ? opt : node;
446
+ }
447
+ catch {
448
+ // No opt link (an unusual Homebrew layout): the versioned path still works today.
449
+ return node;
450
+ }
451
+ }
377
452
  /** Exported for tests, which need to reach a platform other than the one they
378
453
  * run on. Production code goes through {@link autostartForPlatform}. */
379
454
  exports.AUTOSTART_IMPLEMENTATIONS = {
@@ -101,6 +101,12 @@ function request(sock, frame, timeoutMs = 5_000) {
101
101
  clearTimeout(timer);
102
102
  reject(err);
103
103
  });
104
+ // The relay went away without answering. Waiting out the timer would only delay
105
+ // the same answer; the caller decides whether gone was what it wanted.
106
+ sock.on("close", () => {
107
+ clearTimeout(timer);
108
+ reject(new Error("relay closed the connection without answering"));
109
+ });
104
110
  (0, socket_1.writeFrame)(sock, frame);
105
111
  });
106
112
  }
@@ -386,7 +392,16 @@ async function cmdRelayStop() {
386
392
  if (await (0, socket_1.probeSocket)((0, socket_1.socketPath)())) {
387
393
  // A windowless supervisor may stop without killing its child. Ask the
388
394
  // relay itself to shut down, and wait so the next start cannot reuse it.
389
- await request(await connectOrFail(), { type: "stop" });
395
+ try {
396
+ await request(await connectOrFail(), { type: "stop" });
397
+ }
398
+ catch (err) {
399
+ // launchd's bootout above already sent the relay SIGTERM; it can accept this
400
+ // connection and exit before answering. A relay that is gone is what was asked
401
+ // for — only a relay still listening is a failure to report.
402
+ if (await (0, socket_1.probeSocket)((0, socket_1.socketPath)()))
403
+ throw err;
404
+ }
390
405
  const deadline = Date.now() + 5_000;
391
406
  while (await (0, socket_1.probeSocket)((0, socket_1.socketPath)())) {
392
407
  if (Date.now() >= deadline)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.25.2",
3
+ "version": "0.25.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"