baychat 0.33.0 → 0.33.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
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": {
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onReleaseAnnounced = onReleaseAnnounced;
4
+ exports.announceRelease = announceRelease;
5
+ let listener;
6
+ let log = (line) => console.error(line);
7
+ function onReleaseAnnounced(fn, logLine) {
8
+ listener = fn;
9
+ if (logLine)
10
+ log = logLine;
11
+ }
12
+ function announceRelease(version) {
13
+ try {
14
+ listener?.(version);
15
+ }
16
+ catch (err) {
17
+ // A hint that cannot be taken is a hint missed; the daily check still comes.
18
+ log(`auto-update: could not take the server's release hint (${err instanceof Error ? err.message : String(err)})`);
19
+ }
20
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HELD_FRESH_MS = exports.QUIET_MS = exports.CARD_POLL_MS = exports.ASK_AGAIN_MS = exports.CHECK_INTERVAL_MS = exports.AUTO_UPDATE_ENV = void 0;
3
+ exports.HELD_FRESH_MS = exports.QUIET_MS = exports.CARD_POLL_MS = exports.ASK_AGAIN_MS = exports.HINT_COOLDOWN_MS = exports.HINT_SPREAD_MS = exports.HINT_MIN_MS = exports.CHECK_INTERVAL_MS = exports.AUTO_UPDATE_ENV = void 0;
4
4
  exports.planUpdate = planUpdate;
5
5
  exports.autoUpdateOffReason = autoUpdateOffReason;
6
6
  exports.nextCheckAt = nextCheckAt;
@@ -50,6 +50,13 @@ const CHECK_SPREAD_MS = HOUR;
50
50
  /** The first check after a start: soon, but not in the busy first minutes. */
51
51
  const FIRST_CHECK_MIN_MS = 5 * MINUTE;
52
52
  const FIRST_CHECK_SPREAD_MS = 10 * MINUTE;
53
+ /** The server named a newer release in the feed's `ready` frame (hint.ts): npm is asked within
54
+ * 1–5 minutes — spread so a fleet that hears the same frame never asks npm together. */
55
+ exports.HINT_MIN_MS = MINUTE;
56
+ exports.HINT_SPREAD_MS = 4 * MINUTE;
57
+ /** A hint right after a check is not taken: a server that names a release on every reconnect
58
+ * must not become an npm query on every reconnect. */
59
+ exports.HINT_COOLDOWN_MS = 15 * MINUTE;
53
60
  /** A check that failed (offline, npm down) is tried again sooner than a day, then backs off. */
54
61
  const CHECK_RETRY_MS = HOUR;
55
62
  /** A failed install of one version waits a day, then two, … up to a week. A newer version resets it. */
@@ -10,6 +10,7 @@ const refresh_clients_1 = require("../refresh-clients");
10
10
  const waiting_cards_1 = require("../waiting-cards");
11
11
  const policy_1 = require("./policy");
12
12
  const store_1 = require("./store");
13
+ const hint_1 = require("./hint");
13
14
  const updater_1 = require("./updater");
14
15
  /**
15
16
  * The automatic update wired to a real relay: npm, the disk, the device API for the card, and the
@@ -50,6 +51,8 @@ function startRelayUpdater(opts) {
50
51
  };
51
52
  const updater = new updater_1.AutoUpdater(deps);
52
53
  updater.start();
54
+ // The server's feed names the newest release in each `ready` frame: check npm soon, not tomorrow.
55
+ (0, hint_1.onReleaseAnnounced)((version) => updater.hint(version), opts.log);
53
56
  return updater;
54
57
  }
55
58
  /**
@@ -17,6 +17,10 @@ class AutoUpdater {
17
17
  running = false;
18
18
  stopped = false;
19
19
  lastCardPoll = 0;
20
+ /** A release the server named (hint.ts), and when npm is asked about it. In memory only: a
21
+ * restarted relay hears the next `ready` frame anyway. */
22
+ hinted;
23
+ hintCheckAt;
20
24
  constructor(deps) {
21
25
  this.deps = deps;
22
26
  this.startedAt = deps.now();
@@ -46,6 +50,35 @@ class AutoUpdater {
46
50
  failure: state.failure?.reason,
47
51
  };
48
52
  }
53
+ /**
54
+ * The server's feed named `version` as the newest BayChat (hint.ts). Newer than this copy, and
55
+ * newer than what npm last said: ask npm in 1–5 minutes instead of at the daily check. Only the
56
+ * moment moves — npm still names the version, and planUpdate still decides what happens.
57
+ *
58
+ * The moment never moves earlier than the backoff allows: not within HINT_COOLDOWN_MS of the
59
+ * last check, and not while a failed check waits out its retry (npm down stays npm down,
60
+ * however often the server reconnects).
61
+ */
62
+ hint(version) {
63
+ const { mode } = this.mode();
64
+ if (mode === "off" || mode === "none")
65
+ return;
66
+ const ahead = (0, self_upgrade_1.compareVersions)(version, this.deps.current);
67
+ if (ahead === undefined || ahead <= 0 || this.hinted === version)
68
+ return;
69
+ const state = this.deps.loadState();
70
+ const known = state.latest ? (0, self_upgrade_1.compareVersions)(state.latest, version) : undefined;
71
+ if (known !== undefined && known >= 0)
72
+ return; // npm already named it (or newer)
73
+ const now = this.deps.now();
74
+ if (state.lastCheckAt !== undefined && now - state.lastCheckAt < policy_1.HINT_COOLDOWN_MS)
75
+ return;
76
+ if ((state.checkFailures ?? 0) > 0 && now < (0, policy_1.nextCheckAt)(state, this.startedAt, this.spread))
77
+ return;
78
+ this.hinted = version;
79
+ this.hintCheckAt = now + policy_1.HINT_MIN_MS + this.deps.random() * policy_1.HINT_SPREAD_MS;
80
+ this.deps.log(`auto-update: the server names ${version}; asking npm within minutes`);
81
+ }
49
82
  /** One look at the state. Never throws; never runs twice at once. */
50
83
  async tick() {
51
84
  await this.runExclusive(async () => {
@@ -64,8 +97,11 @@ class AutoUpdater {
64
97
  if (mode === "off" || mode === "none")
65
98
  return;
66
99
  const now = this.deps.now();
67
- if (now >= (0, policy_1.nextCheckAt)(state, this.startedAt, this.spread))
100
+ const hinted = this.hintCheckAt !== undefined && now >= this.hintCheckAt;
101
+ if (hinted || now >= (0, policy_1.nextCheckAt)(state, this.startedAt, this.spread)) {
102
+ this.hintCheckAt = undefined;
68
103
  await this.check(state, mode);
104
+ }
69
105
  if (mode !== "auto")
70
106
  return;
71
107
  if (state.card?.requestId && now - this.lastCardPoll >= policy_1.CARD_POLL_MS)
package/dist/relay/ws.js CHANGED
@@ -27,6 +27,7 @@ const context_notice_1 = require("./context-notice");
27
27
  const project_notes_1 = require("./project-notes");
28
28
  const updates_1 = require("./updates");
29
29
  const server_registration_1 = require("./server-registration");
30
+ const hint_1 = require("../auto-update/hint");
30
31
  /** The published endpoint. Changing it breaks every deployed server — a wire constant. */
31
32
  exports.AGENT_WS_PATH = "/api/agent-api/ws";
32
33
  /** How long we wait for the server's `ready` after sending `hello`. */
@@ -132,6 +133,8 @@ function runWebSocketFeed(opts) {
132
133
  onSessions?.(frame.sessions, frame.registrations);
133
134
  if (frame.cursor)
134
135
  onCursor?.(frame.cursor);
136
+ if (frame.latestCli)
137
+ (0, hint_1.announceRelease)(frame.latestCli);
135
138
  onReady?.(frame.cursor ?? null);
136
139
  return;
137
140
  }
@@ -288,6 +291,8 @@ function parseFrame(raw) {
288
291
  cursor: str(frame.cursor),
289
292
  sessions: names(frame.sessions),
290
293
  registrations: (0, server_registration_1.parseRegistrations)(frame.registrations),
294
+ // Plain x.y.z only; anything else is dropped (a hint, never what installs: auto-update/hint.ts).
295
+ latestCli: typeof frame.latestCli === "string" && /^\d+\.\d+\.\d+$/.test(frame.latestCli) ? frame.latestCli : undefined,
291
296
  };
292
297
  case "cursor":
293
298
  return {
@@ -42,6 +42,7 @@ exports.upgradePackage = upgradePackage;
42
42
  exports.lookupLatest = lookupLatest;
43
43
  exports.installRelease = installRelease;
44
44
  exports.describeOutcome = describeOutcome;
45
+ exports.npmCommand = npmCommand;
45
46
  exports.realNpm = realNpm;
46
47
  exports.readPackageVersion = readPackageVersion;
47
48
  exports.packageEntry = packageEntry;
@@ -249,6 +250,30 @@ function npmFailure(r) {
249
250
  const lines = r.stderr.split("\n").map((l) => l.trim()).filter(Boolean);
250
251
  return lines.slice(-3).join(" ") || `npm exited with ${r.status}`;
251
252
  }
253
+ /**
254
+ * Which npm to run, and the PATH to run it with: the npm installed beside the Node running this
255
+ * code, when there is one — else plain `npm` from PATH.
256
+ *
257
+ * A relay under systemd or launchd does not read a shell profile, so with Node from nvm (or any
258
+ * version manager) its PATH holds no npm at all: every automatic update on such a computer failed
259
+ * with "npm was not found on PATH" (Karmen's PC, 2026-10-21: 11 failed checks, never one update).
260
+ * The Node that is running is the Node that installed this copy, and its npm sits next to it. That
261
+ * directory also goes first on the child's PATH, because npm itself starts with
262
+ * `#!/usr/bin/env node`, which must find a node too.
263
+ *
264
+ * Which global folder that npm calls its own (Volta, asdf, a custom `prefix`) does not matter:
265
+ * installRelease always passes `--prefix <the running copy's prefix>`, which npm puts above its
266
+ * own config (checked 2026-10-21 with npm_config_prefix pointing elsewhere: the install landed in
267
+ * `--prefix`), and then believes only the version on disk there.
268
+ */
269
+ function npmCommand(execPath, platform, pathEnv, exists) {
270
+ const dir = path.dirname(execPath);
271
+ const beside = path.join(dir, platform === "win32" ? "npm.cmd" : "npm");
272
+ if (!exists(beside))
273
+ return { command: "npm", path: pathEnv };
274
+ const sep = platform === "win32" ? ";" : ":";
275
+ return { command: beside, path: pathEnv ? `${dir}${sep}${pathEnv}` : dir };
276
+ }
252
277
  /** Run npm for real, without blocking the event loop (the relay keeps serving while npm runs).
253
278
  * Windows' npm is a `.cmd` shim, which Node will only start through a shell, so every argument
254
279
  * is first checked against what cmd.exe could misread and then quoted. */
@@ -257,8 +282,20 @@ function realNpm(args, timeoutMs) {
257
282
  if (windows && args.some((a) => commands_1.WINDOWS_UNSAFE.test(a))) {
258
283
  return Promise.resolve({ status: null, stdout: "", stderr: "", error: new Error("a path npm needs cannot be passed safely through cmd.exe") });
259
284
  }
285
+ const found = npmCommand(process.execPath, process.platform, process.env.PATH, fs.existsSync);
286
+ // A Node directory cmd.exe could misread is not run through it: PATH's npm then, as before.
287
+ const npm = windows && commands_1.WINDOWS_UNSAFE.test(found.command) ? { command: "npm", path: process.env.PATH } : found;
288
+ // Through cmd.exe the command is a string too: a path with spaces must arrive as one word.
289
+ const command = windows && npm.command !== "npm" ? (0, commands_1.quoteForCmd)(npm.command) : npm.command;
260
290
  return new Promise((resolve) => {
261
- (0, child_process_1.execFile)("npm", windows ? args.map(commands_1.quoteForCmd) : args, { encoding: "utf8", timeout: timeoutMs, shell: windows, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
291
+ (0, child_process_1.execFile)(command, windows ? args.map(commands_1.quoteForCmd) : args, {
292
+ encoding: "utf8",
293
+ timeout: timeoutMs,
294
+ shell: windows,
295
+ windowsHide: true,
296
+ maxBuffer: 16 * 1024 * 1024,
297
+ env: { ...process.env, ...(npm.path !== undefined ? { PATH: npm.path } : {}) },
298
+ }, (err, stdout, stderr) => {
262
299
  if (!err)
263
300
  return resolve({ status: 0, stdout, stderr });
264
301
  const code = err.code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
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"