baychat 0.29.9 → 0.31.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.
Files changed (45) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +43 -10
  3. package/dist/approval-wait.js +143 -0
  4. package/dist/approve-hook.js +68 -133
  5. package/dist/auto-update/describe.js +66 -0
  6. package/dist/auto-update/notice.js +40 -0
  7. package/dist/auto-update/policy.js +110 -0
  8. package/dist/auto-update/relay-glue.js +143 -0
  9. package/dist/auto-update/store.js +140 -0
  10. package/dist/auto-update/updater.js +324 -0
  11. package/dist/channel/permission.js +6 -0
  12. package/dist/cli-help.js +30 -3
  13. package/dist/denied/after-hook.js +46 -0
  14. package/dist/denied/confirm.js +131 -0
  15. package/dist/denied/grants.js +292 -0
  16. package/dist/denied/hook-log.js +75 -0
  17. package/dist/denied/live-session.js +154 -0
  18. package/dist/denied/notice.js +29 -0
  19. package/dist/denied/opt-in.js +68 -0
  20. package/dist/denied/permission-rule.js +186 -0
  21. package/dist/denied/settings-rules.js +221 -0
  22. package/dist/denied/transcript.js +173 -0
  23. package/dist/denied-hook.js +431 -0
  24. package/dist/doctor-command.js +7 -1
  25. package/dist/doctor.js +28 -0
  26. package/dist/help-topics.js +12 -4
  27. package/dist/hook-stdin.js +35 -0
  28. package/dist/index.js +13 -1
  29. package/dist/main.js +20 -2
  30. package/dist/permissions-command.js +104 -0
  31. package/dist/protocol-content.js +1 -1
  32. package/dist/refresh-clients.js +36 -0
  33. package/dist/relay/acp/runner.js +4 -0
  34. package/dist/relay/commands.js +19 -3
  35. package/dist/relay/daemon.js +37 -1
  36. package/dist/relay/launcher/approval-settings.js +56 -2
  37. package/dist/relay/launcher/loop.js +4 -1
  38. package/dist/relay/self-restart.js +149 -0
  39. package/dist/relay/terminal-pane.js +74 -2
  40. package/dist/runtimes.js +2 -2
  41. package/dist/self-upgrade.js +92 -27
  42. package/dist/start-command.js +15 -1
  43. package/dist/update-command.js +51 -31
  44. package/dist/waiting-cards.js +107 -0
  45. package/package.json +4 -1
@@ -43,6 +43,9 @@ const multiplexer_launch_1 = require("./multiplexer-launch");
43
43
  const profiles_1 = require("./relay/profiles");
44
44
  const runtime_binary_1 = require("./runtime-binary");
45
45
  const update_command_1 = require("./update-command");
46
+ const policy_1 = require("./auto-update/policy");
47
+ const store_1 = require("./auto-update/store");
48
+ const commands_1 = require("./relay/commands");
46
49
  const launch_1 = require("./channel/launch");
47
50
  /**
48
51
  * `baychat start [name]` — the whole product in one command.
@@ -105,7 +108,11 @@ async function cmdStart(args) {
105
108
  // is swallowed: the agent starts anyway. An update is an improvement to a session, never
106
109
  // a precondition for one, and a person who cannot start their agent because a background
107
110
  // refresh failed would rightly never trust this command again.
108
- if (!args.includes("--no-update") && (0, update_command_1.autoUpdateDue)()) {
111
+ //
112
+ // Since 0.31.0 the relay updates this computer by itself (docs/features/CLI_AUTO_UPDATE.md), in
113
+ // a quiet moment and without restarting under a running turn. When it says it does, `start`
114
+ // leaves updating to it; when the person switched automatic updates off, `start` honours that.
115
+ if (!args.includes("--no-update") && (0, update_command_1.autoUpdateDue)() && (await startShouldUpdate())) {
109
116
  try {
110
117
  await (0, update_command_1.cmdUpdate)({ quiet: true });
111
118
  }
@@ -140,6 +147,13 @@ async function cmdStart(args) {
140
147
  // A NAMED session: running `baychat start` again returns you to the agent already running.
141
148
  return (0, multiplexer_launch_1.launchAgent)({ binary: binary.path, args: agentArgs, env: channel?.env ?? {} }, { kind: "reattach", name: name || "baychat" });
142
149
  }
150
+ /** Is the daily update `start`'s job here, rather than the relay's or nobody's? */
151
+ async function startShouldUpdate() {
152
+ if ((0, policy_1.autoUpdateOffReason)(process.env, (0, store_1.loadAutoSetting)()))
153
+ return false;
154
+ const relay = await (0, commands_1.tryRelayStatus)();
155
+ return relay?.autoUpdate?.mode !== "auto";
156
+ }
143
157
  /**
144
158
  * Which agent to start.
145
159
  *
@@ -39,10 +39,10 @@ const child_process_1 = require("child_process");
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const config_1 = require("./config");
42
- const credential_refresh_1 = require("./credential-refresh");
43
- const commands_1 = require("./commands");
44
- const client_paths_1 = require("./client-paths");
45
- const commands_2 = require("./relay/commands");
42
+ const refresh_clients_1 = require("./refresh-clients");
43
+ const commands_1 = require("./relay/commands");
44
+ const policy_1 = require("./auto-update/policy");
45
+ const store_1 = require("./auto-update/store");
46
46
  const self_upgrade_1 = require("./self-upgrade");
47
47
  /**
48
48
  * `baychat update` — bring this whole machine onto the current version, in one word.
@@ -76,6 +76,8 @@ const self_upgrade_1 = require("./self-upgrade");
76
76
  const AUTO_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
77
77
  const STAMP_FILE = "last-update-check.json";
78
78
  async function cmdUpdate(opts = {}) {
79
+ if (opts.auto !== undefined)
80
+ return setAutoUpdate(opts.auto);
79
81
  const say = (line) => {
80
82
  if (!opts.quiet)
81
83
  console.log(line);
@@ -93,12 +95,24 @@ async function cmdUpdate(opts = {}) {
93
95
  done.push(`upgraded baychat ${upgradedFrom} → ${(0, self_upgrade_1.ownVersion)()}`);
94
96
  }
95
97
  else {
96
- const outcome = (0, self_upgrade_1.upgradePackage)({
97
- install: (0, self_upgrade_1.classifyInstall)((0, self_upgrade_1.currentEntry)()),
98
- current: (0, self_upgrade_1.ownVersion)(),
99
- npm: self_upgrade_1.realNpm,
100
- readVersion: self_upgrade_1.readPackageVersion,
101
- });
98
+ // One npm install over this baychat at a time: the relay may be installing one by itself.
99
+ const unlock = (0, store_1.acquireUpdateLock)();
100
+ if (!unlock) {
101
+ console.error("BayChat is updating itself right now (the relay is installing a new version). Try again in a minute.");
102
+ return 1;
103
+ }
104
+ let outcome;
105
+ try {
106
+ outcome = await (0, self_upgrade_1.upgradePackage)({
107
+ install: (0, self_upgrade_1.classifyInstall)((0, self_upgrade_1.currentEntry)()),
108
+ current: (0, self_upgrade_1.ownVersion)(),
109
+ npm: self_upgrade_1.realNpm,
110
+ readVersion: self_upgrade_1.readPackageVersion,
111
+ });
112
+ }
113
+ finally {
114
+ unlock();
115
+ }
102
116
  const told = (0, self_upgrade_1.describeOutcome)(outcome);
103
117
  if (!told.ok) {
104
118
  // Never "Updated this machine" over a package that did not move. Stop before the relay
@@ -122,33 +136,22 @@ async function cmdUpdate(opts = {}) {
122
136
  if (told.done)
123
137
  done.push(told.done);
124
138
  }
125
- // 2. EVERY CONNECTED CLIENT's copy of the credential and endpoint.
126
- try {
127
- const updated = (0, credential_refresh_1.refreshExistingClientConfigs)({ url: `${device.baseUrl}/api/mcp`, token: device.token }, commands_1.realConfigIo, (0, client_paths_1.currentPathEnv)());
128
- for (const file of updated)
129
- done.push(`refreshed ${file}`);
130
- }
131
- catch (err) {
132
- // Never fatal: the next steps are independent, and a machine that failed one of four
133
- // is better off doing the other three than stopping at the first.
134
- say(` Could not refresh client configs: ${errText(err)}`);
135
- }
136
- // 3. CLAUDE CODE, which is not an MCP-config client and registers through its own CLI.
137
- if (hasBinary("claude") && (0, commands_1.registerWithClaude)(device.baseUrl, device.token)) {
138
- done.push("re-registered BayChat with Claude Code");
139
- }
139
+ // 2 + 3. EVERY CONNECTED CLIENT, and Claude Code.
140
+ done.push(...(0, refresh_clients_1.refreshConnectedClients)(device, say));
140
141
  // 4. THE RELAY — the step people forget, and the one that silently keeps old code
141
142
  // running. Stopped and started rather than signalled: the daemon loads its code at
142
143
  // process start, so nothing short of a restart picks up a new version.
143
144
  try {
144
- await (0, commands_2.cmdRelayStop)();
145
+ await (0, commands_1.cmdRelayStop)();
145
146
  }
146
147
  catch {
147
148
  // Not running is the normal case on a fresh boot, and is not a failure to report.
148
149
  }
149
- const relay = await (0, commands_2.ensureRelayInstalled)();
150
+ const relay = await (0, commands_1.ensureRelayInstalled)();
150
151
  done.push(relay.startsWith("Relay:") ? relay.toLowerCase() : relay);
151
152
  writeStamp();
153
+ // Whatever the relay left for the terminal ("x is available", "updated itself") is answered now.
154
+ (0, store_1.takeUpdateNotice)();
152
155
  if (done.length === 0) {
153
156
  say("Everything was already up to date.");
154
157
  return 0;
@@ -164,6 +167,27 @@ async function cmdUpdate(opts = {}) {
164
167
  console.log(` ✓ ${line}`);
165
168
  return 0;
166
169
  }
170
+ /**
171
+ * `baychat update --auto on|off` — the relay's automatic updates for this computer (default on).
172
+ * Read by the relay on its next look, within a minute; nothing needs restarting.
173
+ */
174
+ function setAutoUpdate(value) {
175
+ if (value !== "on" && value !== "off") {
176
+ console.error("Usage: baychat update --auto on|off");
177
+ return 1;
178
+ }
179
+ (0, store_1.saveAutoSetting)(value);
180
+ const envOff = (0, policy_1.autoUpdateOffReason)(process.env, "on");
181
+ if (value === "off") {
182
+ console.log("Automatic updates are off on this computer. Update with `baychat update` when you choose; `baychat update --auto on` turns them back on.");
183
+ }
184
+ else {
185
+ console.log("Automatic updates are on: the relay installs new BayChat versions by itself in a quiet moment, and asks on your phone first when a version needs you.");
186
+ if (envOff)
187
+ console.log(`Note: ${envOff} is set here, which keeps them off for anything started from this shell.`);
188
+ }
189
+ return 0;
190
+ }
167
191
  /**
168
192
  * Has it been long enough for `start` to check again?
169
193
  *
@@ -217,10 +241,6 @@ function runOnNewCode(packageRoot, from, quiet) {
217
241
  }
218
242
  return child.status ?? 1;
219
243
  }
220
- function hasBinary(name) {
221
- const probe = (0, child_process_1.spawnSync)(name, ["--version"], { stdio: "ignore", timeout: 15_000 });
222
- return !probe.error && probe.status === 0;
223
- }
224
244
  function errText(err) {
225
245
  return err instanceof Error ? err.message : String(err);
226
246
  }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.markWaitingCard = markWaitingCard;
37
+ exports.countWaitingCards = countWaitingCards;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const config_1 = require("./config");
41
+ const parent_watch_1 = require("./relay/parent-watch");
42
+ /**
43
+ * Which processes on this computer are waiting for a tap on a decision card right now.
44
+ *
45
+ * The hooks (`approve-hook`, `denied-hook`) and the Claude Code channel each wait on their own
46
+ * card in their own process, so nothing else on the machine knows a question is open. The relay's
47
+ * automatic update must (docs/features/CLI_AUTO_UPDATE.md): it never restarts BayChat while
48
+ * somebody is about to answer one. So each waiter leaves a marker for as long as it waits —
49
+ * `~/.baychat/waiting-cards/<pid>-<card id>` — and removes it when done. A crashed waiter's marker
50
+ * is ignored (and cleared) because its pid is gone; an absurdly old one because pids are reused.
51
+ */
52
+ /** Longer than any card is waited on (the denied-command card's 3600 s, an approval's day). */
53
+ const MARKER_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
54
+ function markerDir() {
55
+ return path.join((0, config_1.configDir)(), "waiting-cards");
56
+ }
57
+ /** Mark `cardId` as waited on by this process. Returns the function that ends the mark. Never throws. */
58
+ function markWaitingCard(cardId, pid = process.pid) {
59
+ const safeId = cardId.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64) || "card";
60
+ let file;
61
+ try {
62
+ file = path.join(markerDir(), `${pid}-${safeId}`);
63
+ fs.mkdirSync(markerDir(), { recursive: true, mode: 0o700 });
64
+ fs.writeFileSync(file, "");
65
+ }
66
+ catch {
67
+ // Without the marker an update might restart the relay mid-question; the card itself still
68
+ // works, because its wait never goes through the relay. Not worth failing a hook over.
69
+ return () => undefined;
70
+ }
71
+ return () => {
72
+ try {
73
+ fs.rmSync(file, { force: true });
74
+ }
75
+ catch {
76
+ // A marker left behind is ignored once this process has exited (its pid is gone).
77
+ }
78
+ };
79
+ }
80
+ /** How many cards live processes are waiting on. Stale markers are removed on the way. */
81
+ function countWaitingCards(isAlive = parent_watch_1.processIsAlive, now = Date.now()) {
82
+ let names;
83
+ try {
84
+ names = fs.readdirSync(markerDir());
85
+ }
86
+ catch {
87
+ // No directory: nobody has ever waited on a card here.
88
+ return 0;
89
+ }
90
+ let live = 0;
91
+ for (const name of names) {
92
+ const file = path.join(markerDir(), name);
93
+ const pid = Number(name.split("-")[0]);
94
+ let fresh = false;
95
+ try {
96
+ fresh = now - fs.statSync(file).mtimeMs < MARKER_MAX_AGE_MS;
97
+ }
98
+ catch {
99
+ continue; // Removed between the listing and the look: its waiter finished.
100
+ }
101
+ if (Number.isInteger(pid) && pid > 1 && fresh && isAlive(pid))
102
+ live += 1;
103
+ else
104
+ fs.rmSync(file, { force: true });
105
+ }
106
+ return live;
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.29.9",
3
+ "version": "0.31.0",
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"
@@ -22,6 +22,9 @@
22
22
  "vitest": "^4.1.10"
23
23
  },
24
24
  "license": "MIT",
25
+ "baychat": {
26
+ "attention": {}
27
+ },
25
28
  "homepage": "https://baychat.io",
26
29
  "keywords": [
27
30
  "baychat",