botmux 3.18.2 → 3.18.4

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 (97) hide show
  1. package/dist/.runtime-build-id +1 -1
  2. package/dist/adapters/hook-installer.d.ts.map +1 -1
  3. package/dist/adapters/hook-installer.js +75 -2
  4. package/dist/adapters/hook-installer.js.map +1 -1
  5. package/dist/cli/current-actor.d.ts.map +1 -1
  6. package/dist/cli/current-actor.js +2 -1
  7. package/dist/cli/current-actor.js.map +1 -1
  8. package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
  9. package/dist/cli/dashboard-endpoint.js +5 -2
  10. package/dist/cli/dashboard-endpoint.js.map +1 -1
  11. package/dist/cli/exact-chat-grant-client.d.ts.map +1 -1
  12. package/dist/cli/exact-chat-grant-client.js +2 -1
  13. package/dist/cli/exact-chat-grant-client.js.map +1 -1
  14. package/dist/cli/vc-agent.d.ts.map +1 -1
  15. package/dist/cli/vc-agent.js +2 -1
  16. package/dist/cli/vc-agent.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +35 -9
  19. package/dist/cli.js.map +1 -1
  20. package/dist/core/daemon-ipc-auth.d.ts +9 -1
  21. package/dist/core/daemon-ipc-auth.d.ts.map +1 -1
  22. package/dist/core/daemon-ipc-auth.js +14 -3
  23. package/dist/core/daemon-ipc-auth.js.map +1 -1
  24. package/dist/core/fleet-state-store.d.ts.map +1 -1
  25. package/dist/core/fleet-state-store.js +6 -0
  26. package/dist/core/fleet-state-store.js.map +1 -1
  27. package/dist/core/fleet-supervisor-policy.d.ts +38 -6
  28. package/dist/core/fleet-supervisor-policy.d.ts.map +1 -1
  29. package/dist/core/fleet-supervisor-policy.js +32 -9
  30. package/dist/core/fleet-supervisor-policy.js.map +1 -1
  31. package/dist/core/fleet-supervisor.d.ts +39 -0
  32. package/dist/core/fleet-supervisor.d.ts.map +1 -1
  33. package/dist/core/fleet-supervisor.js +61 -9
  34. package/dist/core/fleet-supervisor.js.map +1 -1
  35. package/dist/core/loopback-fetch.d.ts +59 -0
  36. package/dist/core/loopback-fetch.d.ts.map +1 -0
  37. package/dist/core/loopback-fetch.js +347 -0
  38. package/dist/core/loopback-fetch.js.map +1 -0
  39. package/dist/core/managed-origin-attestation.d.ts.map +1 -1
  40. package/dist/core/managed-origin-attestation.js +2 -1
  41. package/dist/core/managed-origin-attestation.js.map +1 -1
  42. package/dist/core/reply-target.d.ts +27 -0
  43. package/dist/core/reply-target.d.ts.map +1 -1
  44. package/dist/core/reply-target.js +58 -0
  45. package/dist/core/reply-target.js.map +1 -1
  46. package/dist/daemon.d.ts +3 -0
  47. package/dist/daemon.d.ts.map +1 -1
  48. package/dist/daemon.js +21 -6
  49. package/dist/daemon.js.map +1 -1
  50. package/dist/dashboard/aggregator.d.ts.map +1 -1
  51. package/dist/dashboard/aggregator.js +2 -1
  52. package/dist/dashboard/aggregator.js.map +1 -1
  53. package/dist/dashboard/daemon-internal-client.d.ts.map +1 -1
  54. package/dist/dashboard/daemon-internal-client.js +6 -1
  55. package/dist/dashboard/daemon-internal-client.js.map +1 -1
  56. package/dist/dashboard/groups-action-helpers.d.ts +0 -10
  57. package/dist/dashboard/groups-action-helpers.d.ts.map +1 -1
  58. package/dist/dashboard/groups-action-helpers.js +3 -2
  59. package/dist/dashboard/groups-action-helpers.js.map +1 -1
  60. package/dist/dashboard.js +2 -1
  61. package/dist/dashboard.js.map +1 -1
  62. package/dist/im/lark/event-dispatcher.d.ts +13 -0
  63. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  64. package/dist/im/lark/event-dispatcher.js +22 -1
  65. package/dist/im/lark/event-dispatcher.js.map +1 -1
  66. package/dist/platform/tunnel-client.d.ts +27 -0
  67. package/dist/platform/tunnel-client.d.ts.map +1 -1
  68. package/dist/platform/tunnel-client.js +205 -22
  69. package/dist/platform/tunnel-client.js.map +1 -1
  70. package/dist/services/hook-runner.d.ts.map +1 -1
  71. package/dist/services/hook-runner.js +2 -1
  72. package/dist/services/hook-runner.js.map +1 -1
  73. package/dist/services/send-policy.d.ts +38 -0
  74. package/dist/services/send-policy.d.ts.map +1 -1
  75. package/dist/services/send-policy.js +33 -0
  76. package/dist/services/send-policy.js.map +1 -1
  77. package/dist/services/voice/openai.d.ts.map +1 -1
  78. package/dist/services/voice/openai.js +10 -1
  79. package/dist/services/voice/openai.js.map +1 -1
  80. package/dist/services/worktree-slug-ai.d.ts.map +1 -1
  81. package/dist/services/worktree-slug-ai.js +8 -1
  82. package/dist/services/worktree-slug-ai.js.map +1 -1
  83. package/dist/types.d.ts +16 -0
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/utils/child-env.d.ts +41 -0
  86. package/dist/utils/child-env.d.ts.map +1 -1
  87. package/dist/utils/child-env.js +53 -0
  88. package/dist/utils/child-env.js.map +1 -1
  89. package/dist/workflows/v3/daemon-ipc-client.d.ts.map +1 -1
  90. package/dist/workflows/v3/daemon-ipc-client.js +2 -1
  91. package/dist/workflows/v3/daemon-ipc-client.js.map +1 -1
  92. package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
  93. package/dist/workflows/v3/session-relay-client.js +2 -1
  94. package/dist/workflows/v3/session-relay-client.js.map +1 -1
  95. package/package.json +8 -7
  96. package/scripts/install-path-entry.mjs +263 -0
  97. package/scripts/postinstall-bin.mjs +41 -7
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Put `~/.botmux/bin` on the user's PATH by writing their shell's startup file.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * `npm i -g botmux` has no `bin` field (removed with the Node fallback — see
6
+ * postinstall-bin.mjs), so the ONLY `botmux` command is the launcher written to
7
+ * `~/.botmux/bin/botmux`. If that directory is not on PATH, a successful install
8
+ * still leaves the user with `botmux: command not found`.
9
+ *
10
+ * Both installers used to just PRINT a hint, and the hint was:
11
+ *
12
+ * echo 'export PATH="…"' >> ~/.profile
13
+ *
14
+ * which is wrong for a large share of users, because **zsh never reads
15
+ * `~/.profile`** (measured: `zsh -lic` on a home dir containing `.profile` reads
16
+ * `.zshenv` + `.zprofile` + `.zshrc` and the `.profile` echo never fires). A zsh
17
+ * user who followed the hint verbatim got a file that is silently ignored — the
18
+ * command stayed missing with no indication why. That was the reported bug.
19
+ *
20
+ * ── WHICH FILE PER SHELL (all measured, not recalled) ─────────────────────────
21
+ * Startup files actually sourced, by shell and invocation mode:
22
+ *
23
+ * zsh -c → .zshenv
24
+ * -i → .zshenv .zshrc
25
+ * -li → .zshenv .zprofile .zshrc
26
+ * bash -c → (none)
27
+ * -i → .bashrc
28
+ * -li → the FIRST of .bash_profile → .bash_login → .profile
29
+ * fish → ~/.config/fish/conf.d/*.fish in all three modes
30
+ *
31
+ * So the target per shell is the file that covers the most modes:
32
+ * • zsh → ~/.zshenv (the only file read in all 3)
33
+ * • bash → ~/.bashrc AND its login file (login vs interactive are disjoint)
34
+ * • fish → ~/.config/fish/conf.d/botmux.fish
35
+ *
36
+ * bash genuinely needs both: writing only `.bashrc` misses login shells, and
37
+ * writing only the login file misses ordinary interactive ones.
38
+ *
39
+ * ⚠️ bash's login file is "the first that EXISTS", not always `.bash_profile` —
40
+ * see bashLoginFile() below for why creating the wrong one destroys the user's
41
+ * existing login config.
42
+ *
43
+ * ── fish SYNTAX ──────────────────────────────────────────────────────────────
44
+ * fish is not POSIX; `export PATH="$INSTALL_DIR:$PATH"` is not its syntax. (fish
45
+ * 3.6 happens to tolerate `export` as a compatibility shim and even produces a
46
+ * correct PATH — measured — but `set -gx` is the real form and is what we write.)
47
+ */
48
+
49
+ import { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
50
+ import { join, dirname } from 'node:path';
51
+ import { homedir } from 'node:os';
52
+ import { basename } from 'node:path';
53
+
54
+ /** Marker so we can detect our own previous edit and never write twice. */
55
+ export const PATH_ENTRY_MARKER = '# added by botmux installer';
56
+
57
+ /**
58
+ * Which shell is the user actually on? `$SHELL` is the login shell recorded in
59
+ * passwd, which is what future terminals will start — the right question here,
60
+ * and better than `process.ppid` guessing (the parent of a postinstall is npm,
61
+ * not the user's shell).
62
+ */
63
+ export function detectShell(env = process.env) {
64
+ const shell = env.SHELL ?? '';
65
+ const name = basename(shell);
66
+ if (name.includes('zsh')) return 'zsh';
67
+ if (name.includes('fish')) return 'fish';
68
+ if (name.includes('bash')) return 'bash';
69
+ // Unknown or unset (containers, cron, exotic shells): POSIX `.profile` is the
70
+ // most portable thing we can offer, and sh/ksh/dash all read it at login.
71
+ return name ? 'other' : 'unknown';
72
+ }
73
+
74
+ /**
75
+ * bash's LOGIN startup file, chosen without shadowing anything.
76
+ *
77
+ * ⚠️ bash reads only the FIRST of `.bash_profile` → `.bash_login` → `.profile`.
78
+ * So creating `.bash_profile` on a machine whose login config lives in
79
+ * `.profile` (or `.bash_login`) silently stops that file from ever being read.
80
+ * Measured — a sentinel exported from `.profile` is visible to `bash -lic`, and
81
+ * becomes MISSING the moment an unrelated `.bash_profile` appears; same for
82
+ * `.bash_login`. That is destroying the user's environment to fix a PATH entry,
83
+ * which is far worse than the bug being fixed.
84
+ *
85
+ * So: append to whichever of the three already exists (that is the file bash is
86
+ * actually reading), and only fall back to CREATING `.bash_profile` when none of
87
+ * them exists — in which case there is nothing to shadow.
88
+ */
89
+ function bashLoginFile(home) {
90
+ for (const name of ['.bash_profile', '.bash_login', '.profile']) {
91
+ const file = join(home, name);
92
+ if (existsSync(file)) return file;
93
+ }
94
+ return join(home, '.bash_profile');
95
+ }
96
+
97
+ /**
98
+ * Quote a path for literal use inside a shell startup file.
99
+ *
100
+ * ⚠️ SECURITY, not cosmetics. The install dir is caller-controlled
101
+ * (`BOTMUX_INSTALL_DIR`, or just an unusual home), and the line we write is
102
+ * EXECUTED by the user's shell on every startup. Interpolating it into double
103
+ * quotes lets `$(…)`, backticks and `$VAR` run: measured — an install dir
104
+ * containing `$(touch /tmp/PWNED)` created that file the first time zsh started,
105
+ * and did so on every shell thereafter.
106
+ *
107
+ * Single quotes make the shell treat every byte literally; the only character
108
+ * needing care is `'` itself, closed and re-opened via `'\''`. This form is
109
+ * identical in POSIX shells and in fish.
110
+ */
111
+ function shellQuote(value) {
112
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
113
+ }
114
+
115
+ /**
116
+ * The startup files to write for a shell, plus the line to write into each.
117
+ * Returns [] when we have nothing safe to say.
118
+ *
119
+ * `env` is read for the two relocation variables below; both are honoured
120
+ * because writing the un-relocated path produces a file the shell never reads —
121
+ * the same class of bug as writing `.profile` for zsh.
122
+ */
123
+ export function pathEntryTargets(shell, installDir, home = homedir(), env = process.env) {
124
+ const q = shellQuote(installDir);
125
+ // ⚠️ The STATEMENT has to be idempotent too, not just the file write. An
126
+ // unconditional `export PATH=<dir>:$PATH` re-prepends on every shell startup, so
127
+ // a nested or re-sourced shell keeps growing PATH — measured: the same directory
128
+ // appeared twice at nesting depth 2, and `.bash_profile` files that source
129
+ // `.bashrc` hit it within a single login. `case` is POSIX and quote-safe (no
130
+ // subshell, no `grep`), and the `:` padding makes it an exact ELEMENT test, so a
131
+ // sibling like `<dir>-old` never satisfies it.
132
+ const posix = `case ":$PATH:" in *:${q}:*) ;; *) export PATH=${q}":$PATH" ;; esac ${PATH_ENTRY_MARKER}`;
133
+ switch (shell) {
134
+ case 'zsh': {
135
+ // ⚠️ zsh reads its dotfiles from $ZDOTDIR when that is set, falling back to
136
+ // $HOME. Writing $HOME/.zshenv on a machine with ZDOTDIR set produces a file
137
+ // zsh never reads — measured: `zsh -c botmux` → "command not found".
138
+ const zdotdir = env.ZDOTDIR?.trim();
139
+ const base = zdotdir ? zdotdir : home;
140
+ return [{ file: join(base, '.zshenv'), line: posix }];
141
+ }
142
+ case 'bash': {
143
+ // Disjoint coverage — see header. Both, or one of the two common ways of
144
+ // opening a terminal is left broken. The login half must not shadow (above).
145
+ const login = bashLoginFile(home);
146
+ const targets = [{ file: join(home, '.bashrc'), line: posix }];
147
+ // When bash's login file IS .bashrc-adjacent there is nothing more to add;
148
+ // dedupe so we never append the same line to the same file twice.
149
+ if (login !== join(home, '.bashrc')) targets.push({ file: login, line: posix });
150
+ return targets;
151
+ }
152
+ case 'fish': {
153
+ // ⚠️ fish's config dir is $XDG_CONFIG_HOME/fish, defaulting to ~/.config/fish.
154
+ // Same failure as ZDOTDIR above — measured: with XDG_CONFIG_HOME set, a file
155
+ // under ~/.config/fish is ignored, while the same line under
156
+ // $XDG_CONFIG_HOME/fish/conf.d works (positive control).
157
+ const xdg = env.XDG_CONFIG_HOME?.trim();
158
+ const configHome = xdg ? xdg : join(home, '.config');
159
+ return [{
160
+ file: join(configHome, 'fish', 'conf.d', 'botmux.fish'),
161
+ // fish: `contains` is its own exact list-element test — PATH is a real list
162
+ // here, so no delimiter padding is needed.
163
+ line: `contains ${q} $PATH; or set -gx PATH ${q} $PATH ${PATH_ENTRY_MARKER}`,
164
+ }];
165
+ }
166
+ case 'other':
167
+ case 'unknown':
168
+ default:
169
+ return [{ file: join(home, '.profile'), line: posix }];
170
+ }
171
+ }
172
+
173
+ /** Is `installDir` already handled by this file (ours or the user's own line)? */
174
+ export function fileAlreadyHasEntry(file, installDir) {
175
+ if (!existsSync(file)) return false;
176
+ let text;
177
+ try { text = readFileSync(file, 'utf8'); } catch { return false; }
178
+
179
+ // ⚠️ TWO SEPARATE PREDICATES, deliberately not one clever one.
180
+ //
181
+ // Attempt #1 searched for the raw directory as a substring: `<installDir>-old`,
182
+ // `/backup<installDir>` and `<installDir>/other` all counted as configured, so
183
+ // the REAL directory was never written (measured, written=0 for each).
184
+ // Attempt #2 split the line into tokens and compared them — but the line we
185
+ // WRITE for a directory containing a quote is
186
+ // export PATH='/x/q'\''bin'":$PATH"
187
+ // and ANY tokenizer that treats `'` as a separator shreds that into `/x/q`,
188
+ // `\`, `bin`, so the quoted spelling can never match a token (measured: marker
189
+ // count 1 → 2 on every re-install). Shell quoting cannot be undone by splitting
190
+ // on characters.
191
+ //
192
+ // So: recognise OUR line by its own known syntax (marker + the exact quoted
193
+ // form we would generate), and use conservative whole-token matching only for a
194
+ // line the USER wrote by hand.
195
+ const ourQuoted = shellQuote(installDir);
196
+ const lines = text.split('\n').filter(l => !/^\s*#/.test(l));
197
+
198
+ // (a) Our own line, byte-for-byte on the part that identifies the directory.
199
+ // `PATH=` + the exact quoted dir is enough; the suffix may legitimately
200
+ // differ (`":$PATH"` vs fish's ` $PATH`).
201
+ if (lines.some(l => l.includes(PATH_ENTRY_MARKER) && l.includes(ourQuoted))) return true;
202
+
203
+ // (b) A hand-written line. Compare whole PATH elements, and do NOT treat quotes
204
+ // as element separators — strip the wrapping quotes first, then split on the
205
+ // characters that actually delimit PATH elements.
206
+ const PATH_ASSIGNMENT = new RegExp(
207
+ [
208
+ 'export\\s+PATH\\s*=', // POSIX: export PATH="…"
209
+ '^\\s*PATH\\s*=', // POSIX: PATH=…
210
+ 'set\\s+(-\\w+\\s+)*PATH', // fish: set -gx PATH …
211
+ 'fish_add_path', // fish: fish_add_path …
212
+ '\\bpath\\+=', // zsh: path+=(…)
213
+ '\\bpath=\\(', // zsh: path=(…)
214
+ ].join('|'),
215
+ 'i',
216
+ );
217
+ const target = installDir.replace(/\/+$/, '');
218
+ return lines.some(l => {
219
+ if (!PATH_ASSIGNMENT.test(l)) return false;
220
+ // Quotes here are shell syntax around a plain path (the quote-containing case
221
+ // is handled by (a)); dropping them leaves the path bytes intact.
222
+ const bare = l.replace(/["']/g, '');
223
+ return bare
224
+ .split(/[:\s()=]+/)
225
+ .map(t => t.replace(/\/+$/, ''))
226
+ .some(t => t !== '' && t === target);
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Append the PATH line to every startup file the user's shell reads.
232
+ *
233
+ * Returns `{ written: string[], skipped: string[], failed: [{file, error}] }`.
234
+ * Never throws: a PATH edit failing must not fail the install (the launcher is
235
+ * already in place and the caller prints a manual fallback).
236
+ */
237
+ export function ensurePathEntry(opts) {
238
+ const installDir = opts.installDir;
239
+ const env = opts.env ?? process.env;
240
+ const home = opts.home ?? homedir();
241
+ const shell = opts.shell ?? detectShell(env);
242
+ const targets = pathEntryTargets(shell, installDir, home, env);
243
+
244
+ const written = [], skipped = [], failed = [];
245
+ for (const { file, line } of targets) {
246
+ if (fileAlreadyHasEntry(file, installDir)) { skipped.push(file); continue; }
247
+ try {
248
+ mkdirSync(dirname(file), { recursive: true });
249
+ if (existsSync(file)) {
250
+ // Keep the user's file intact; only append, and only with a leading
251
+ // newline so we never glue onto an unterminated last line.
252
+ const prev = readFileSync(file, 'utf8');
253
+ appendFileSync(file, `${prev.endsWith('\n') || prev === '' ? '' : '\n'}${line}\n`);
254
+ } else {
255
+ writeFileSync(file, `${line}\n`);
256
+ }
257
+ written.push(file);
258
+ } catch (err) {
259
+ failed.push({ file, error: err && err.message ? err.message : String(err) });
260
+ }
261
+ }
262
+ return { shell, written, skipped, failed };
263
+ }
@@ -219,11 +219,45 @@ try {
219
219
  );
220
220
  }
221
221
 
222
- // ── PATH hint ─────────────────────────────────────────────────────────────────
223
- // Mirrors install.sh. Without ~/.botmux/bin on PATH the launcher we just wrote is
224
- // never the `botmux` the user's shell resolves.
225
- const pathEntries = (process.env.PATH ?? '').split(':');
226
- if (!pathEntries.includes(binDir)) {
227
- console.log(`[botmux] add ${binDir} to your PATH so this launcher is the \`botmux\` your shell finds:`);
228
- console.log(`[botmux] echo 'export PATH="${binDir}:$PATH"' >> ~/.profile && . ~/.profile`);
222
+ // ── PATH: write it, don't just suggest it ─────────────────────────────────────
223
+ // This used to only PRINT `echo 'export PATH=…' >> ~/.profile`. Two problems:
224
+ // the user had to act on it, and for zsh users the suggested file is WRONG —
225
+ // zsh never reads ~/.profile (measured), so following the hint verbatim left
226
+ // `botmux` still not found. There is no `bin` field any more, so PATH is the
227
+ // only way this launcher becomes a command; we now write the right startup file
228
+ // for the user's actual shell (bash/zsh/fish/other) and tell them what we did.
229
+ //
230
+ // ⚠️ FAIL-SOFT, and never `fail()`: the launcher is already installed and working
231
+ // at this point, so a PATH edit that cannot happen must degrade to the printed
232
+ // hint — not abort a successful install. That includes the sibling module simply
233
+ // not being there: it ships via package.json `files`, and if a future edit drops
234
+ // it (or a mirror repacks the tarball without it) the import throws. Guarding it
235
+ // keeps `npm i -g botmux` succeeding either way.
236
+ if (!(process.env.PATH ?? '').split(':').includes(binDir)) {
237
+ let ensurePathEntry = null;
238
+ try {
239
+ ({ ensurePathEntry } = await import('./install-path-entry.mjs'));
240
+ } catch (err) {
241
+ console.error(`[botmux] PATH helper unavailable (${err && err.message ? err.message : String(err)})`);
242
+ }
243
+ let written = [], skipped = [];
244
+ if (ensurePathEntry) {
245
+ try {
246
+ const r = ensurePathEntry({ installDir: binDir });
247
+ ({ written, skipped } = r);
248
+ for (const f of written) console.log(`[botmux] added ${binDir} to PATH in ${f} (${r.shell})`);
249
+ for (const f of skipped) console.log(`[botmux] ${f} already puts ${binDir} on PATH`);
250
+ for (const { file, error } of r.failed) console.error(`[botmux] could not update ${file}: ${error}`);
251
+ } catch (err) {
252
+ console.error(`[botmux] could not update your shell startup file: ${err && err.message ? err.message : String(err)}`);
253
+ }
254
+ }
255
+ if (written.length > 0) {
256
+ console.log('[botmux] open a new terminal (or re-source that file) and `botmux` will be on PATH');
257
+ } else if (skipped.length === 0) {
258
+ // Nothing written and nothing already present — fall back to telling them.
259
+ // Deliberately not naming a specific file here: the correct one depends on
260
+ // the shell, and naming the wrong one is what caused the original bug.
261
+ console.log(`[botmux] add ${binDir} to your PATH so this launcher is the \`botmux\` your shell finds`);
262
+ }
229
263
  }