hop2 0.11.0 → 0.11.2

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 (82) hide show
  1. package/.github/workflows/ci.yml +25 -0
  2. package/.github/workflows/publish.yml +130 -0
  3. package/DESIGN-input-acks.md +349 -0
  4. package/MULTI-AGENT-CHECKLIST.md +5 -5
  5. package/README-MCP.md +84 -5
  6. package/README.md +55 -4
  7. package/demo/README.md +52 -0
  8. package/demo/capture/capture-env.mjs +108 -0
  9. package/demo/capture/capture.mjs +634 -0
  10. package/demo/capture/dispatch-task.mjs +44 -0
  11. package/demo/capture/mcp-client.mjs +61 -0
  12. package/demo/capture/native-css.mjs +49 -0
  13. package/demo/capture/setup-04.mjs +67 -0
  14. package/demo/capture/setup-sessions.mjs +40 -0
  15. package/demo/capture/spawn-aurora.mjs +53 -0
  16. package/demo/capture/tools/agent.sh +30 -0
  17. package/demo/capture/tools/build.sh +18 -0
  18. package/demo/capture/tools/metrics.sh +17 -0
  19. package/docs/hero-overview.svg +119 -0
  20. package/docs/hero.svg +118 -0
  21. package/docs/restore-reliability.md +139 -0
  22. package/docs/session-identity.md +76 -0
  23. package/docs/switcher-first-ux.md +103 -0
  24. package/hay/CHANGELOG.md +59 -1
  25. package/hay/apps/cli/package.json +3 -2
  26. package/hay/apps/cli/src/index.ts +705 -28
  27. package/hay/apps/server/src/pty.ts +36 -2
  28. package/hay/apps/server/src/rooms.ts +525 -34
  29. package/hay/apps/server/test/rooms.test.ts +411 -1
  30. package/hay/apps/web/index.html +2 -0
  31. package/hay/apps/web/package.json +14 -12
  32. package/hay/apps/web/scripts/run-e2e.mjs +42 -5
  33. package/hay/apps/web/src/App.tsx +2492 -281
  34. package/hay/apps/web/src/components/ContextMenu.tsx +109 -0
  35. package/hay/apps/web/src/components/MobileKeyboard.tsx +135 -14
  36. package/hay/apps/web/src/components/SecondaryPane.tsx +218 -0
  37. package/hay/apps/web/src/components/SessionSwitcher.tsx +2525 -0
  38. package/hay/apps/web/src/setupTests.ts +23 -0
  39. package/hay/apps/web/src/styles.css +2460 -180
  40. package/hay/apps/web/src/utils/fileDrop.ts +15 -0
  41. package/hay/apps/web/src/utils/keyboardProtocol.ts +22 -0
  42. package/hay/apps/web/src/utils/optimisticEcho.ts +83 -7
  43. package/hay/apps/web/src/utils/scrollFlywheel.ts +145 -0
  44. package/hay/apps/web/src/utils/switcherModel.ts +314 -0
  45. package/hay/apps/web/src/utils/terminalSearch.ts +54 -0
  46. package/hay/apps/web/src/vite-env.d.ts +3 -0
  47. package/hay/apps/web/test/SessionSwitcher.test.tsx +324 -0
  48. package/hay/apps/web/test/fileDrop.test.ts +22 -0
  49. package/hay/apps/web/test/optimisticEcho.test.ts +17 -0
  50. package/hay/apps/web/test/scrollFlywheel.test.ts +113 -0
  51. package/hay/apps/web/test/switcherModel.test.ts +377 -0
  52. package/hay/apps/web/test/terminalSearch.test.ts +37 -0
  53. package/hay/apps/web/vite.config.ts +20 -0
  54. package/hay/package-lock.json +1696 -1171
  55. package/hay/packages/shared/src/protocol.ts +25 -2
  56. package/hay-web/assets/index-BohAWpcL.css +1 -0
  57. package/hay-web/assets/index-DfGT3DuO.js +130 -0
  58. package/hay-web/assets/index-DfGT3DuO.js.map +1 -0
  59. package/hay-web/index.html +4 -2
  60. package/hop +4348 -302
  61. package/hopa +465 -0
  62. package/mcp/hop-mcp.js +2878 -122
  63. package/package.json +7 -4
  64. package/scripts/claude-session-hook.js +152 -0
  65. package/scripts/hay-host.js +227 -4
  66. package/scripts/hop-math.js +316 -0
  67. package/scripts/salvage-and-restart-host.js +217 -0
  68. package/skills/hop-manager/SKILL.md +62 -0
  69. package/skills/hop-session/SKILL.md +42 -0
  70. package/skills/hopa/SKILL.md +75 -0
  71. package/tests/agent-skill.unit.test.cjs +60 -0
  72. package/tests/composer-state.unit.test.cjs +102 -0
  73. package/tests/hop-mcp.integration.test.cjs +476 -94
  74. package/tests/hopa-parity.unit.test.cjs +188 -0
  75. package/tests/readable-echo.unit.test.cjs +94 -0
  76. package/tests/readable-flush.unit.test.cjs +93 -0
  77. package/tests/submit-verification.unit.test.cjs +331 -0
  78. package/tests/terminal-api.integration.test.cjs +301 -13
  79. package/hay-web/assets/index-BzJB9r_I.js +0 -64
  80. package/hay-web/assets/index-BzJB9r_I.js.map +0 -1
  81. package/hay-web/assets/index-DG8Ntznc.css +0 -32
  82. package/sessions.html +0 -726
package/README-MCP.md CHANGED
@@ -43,7 +43,16 @@ Supported auto-detected clients:
43
43
 
44
44
  4. Restart your MCP client.
45
45
 
46
+ Install the bundled Hop workflow instructions for Claude Code and Codex:
47
+
48
+ ```bash
49
+ hop agent-skill install
50
+ ```
51
+
46
52
  The server auto-connects to the local Hop daemon via `~/.hop2/.tunnel-state`.
53
+ Long-lived MCP clients refresh that file automatically when the local daemon
54
+ restarts on a new port. Explicit `connect_server` and `HOP_API_URL` connections
55
+ remain pinned to the configured endpoint.
47
56
 
48
57
  ## Recommended Flow: One Agent Terminal
49
58
 
@@ -85,6 +94,50 @@ Safety notes:
85
94
  - Keep one subagent per terminal (no multiplexing multiple agent CLIs in one PTY).
86
95
  - For existing user sessions, verify `agentPermitted` (or set with `hop_set_agent_permission`) before writing input.
87
96
 
97
+ ## Manager Agents: Orchestrating a Fleet
98
+
99
+ A manager agent (any MCP client — Claude Code, Codex, ...) can run a whole
100
+ fleet of subagent terminals through hop. The loop:
101
+
102
+ 1. **Spawn** — one call per subagent:
103
+ `hopx_spawn_agent(name="worker-1", cwd="/repo", agent="claude", initial_task="<task>", async=true)`
104
+ → `{ terminal_id, sessionName, wait_id, completion_contract }`. Hop gives
105
+ each initial task a unique completion contract automatically.
106
+ 2. **Wait for anyone** — block on the whole fleet with one call:
107
+ `hopx_wait_any(wait_ids=[...], max_wait_ms=60000)` returns the first
108
+ completed turn(s) and the still-pending set. No round-robin polling.
109
+ A settled result is successful only when `ok=true` and its completion
110
+ contract matched; a mismatch is `contract_failed`. A watch whose window
111
+ merely expired is re-armed under a new wait_id and stays in `pending` (carry
112
+ those forward to the next call). Two distinct timers: each background job's own
113
+ wait window defaults to 15 minutes; `max_wait_ms` on the call is only how
114
+ long that one `hopx_wait_any` invocation blocks (default 30s).
115
+ 3. **Read the result** — `hop_read_trajectory(name=<sessionName>)` for the
116
+ full turn (digest mode is context-safe), or use the slimmed wait result
117
+ returned by `hopx_wait_any` directly. Follow-up turns should pass
118
+ `until_reply_regex`; only `reply_matched=true` counts as completion.
119
+ 4. **Redispatch** — next task via
120
+ `hopx_agent_turn(terminal_id=..., data="<task>", async=true)`; collect the
121
+ new `wait_id` and go back to step 2.
122
+ 5. **Check the room** — `hopx_agents_overview()` any time you lose track:
123
+ who is busy (a live wait job), who is idle, turn counts, and bell counters.
124
+ Diff `bellSeq`/`turnCount` against your previous call to spot a subagent
125
+ asking for attention or finishing turns you didn't drive.
126
+ 6. **Interrupt / tear down** — `hopx_agent_turn(wait_id=..., control="interrupt")`
127
+ for a stuck turn; `hop_close_terminal(terminal_id=..., killSession=true)`
128
+ when a worker is done. Never leave orphaned sessions.
129
+ 7. **Survive your own restart** — every async dispatch is recorded in the
130
+ durable task ledger. If the manager process dies, start fresh and call
131
+ `hopx_task_ledger`: turns that finished unwatched are reconciled (contract
132
+ verdicts included), still-pending ones can be re-armed with
133
+ `hopx_wait_any(terminal_ids=[...])`. Workers commit to their own fleet/
134
+ branch when using worktree isolation, so no work is ever trapped in a dead
135
+ manager's memory.
136
+
137
+ Guardrails (see MULTI-AGENT-CHECKLIST.md): one subagent per terminal, explicit
138
+ deadlines on every wait, never write into a terminal whose overview state is
139
+ `busy`, and gate destructive commands behind user confirmation.
140
+
88
141
  ## Shell Commands: `hopx_exec`
89
142
 
90
143
  For plain shell work (not interactive TUIs), `hopx_exec` behaves like a Bash
@@ -130,8 +183,15 @@ connect_server(base_url="https://hop2.example.com", token="<token>", verify=true
130
183
 
131
184
  ## Tools
132
185
 
133
- Core tools (`hop_`): stable atomic operations.
186
+ Naming convention: `hop_` tools are atomic operations — one stable primitive
187
+ each. `hopx_` tools are the convenience/specialized layer composed from those
188
+ atomic operations; anything that orchestrates, joins, or wraps multiple
189
+ primitives lives under `hopx_`.
190
+
191
+ Connection (the one un-prefixed tool):
134
192
  - `connect_server` — connect to a Hop API base_url (optional token); use for remote hop instances
193
+
194
+ Core tools (`hop_`): stable atomic operations.
135
195
  - `hop_server_info` — hop-mcp runtime diagnostics (version, script path, read-mode capabilities)
136
196
  - `hop_list_sessions` — list Hop sessions and metadata
137
197
  - `hop_list_terminals` — list terminal API sessions (created via `hop_create_terminal` / `hop_attach_terminal`)
@@ -139,11 +199,11 @@ Core tools (`hop_`): stable atomic operations.
139
199
  - `hop_attach_terminal` — attach to an existing terminal session by name or internalName
140
200
  - `hop_write_terminal` — write raw input to a terminal session
141
201
  - `hop_send_key` — send a named keypress (enter, esc, tab, shift_tab, arrows, f1-f12, ctrl+[a-z], ...)
142
- - `hop_wait_terminal` — wait for output conditions (regex, prompt, idle, agent_done); `async=true` returns a `wait_id`
143
- - `hop_wait_start` — deprecated legacy alias for `hop_wait_terminal(async=true)`
202
+ - `hop_wait_terminal` — wait for output conditions (regex, prompt, idle, agent_done); `async=true` returns a `wait_id` (`hop_wait_start` was removed; use `hop_wait_terminal(async=true)`)
144
203
  - `hop_wait_poll` — poll or await completion of a background wait job
145
204
  - `hop_resize_terminal` — resize the terminal PTY
146
205
  - `hop_read_terminal` — read terminal output events (default `mode="readable_raw"`); returns a cursor for incremental reads
206
+ - `hop_read_trajectory` — read a Claude Code session's real conversation history from its on-disk transcript (resolved from the hop session name via the SessionStart hook record); context-safe reduced `digest` mode by default, plus `summary`/`list`/`get`/`tail` modes; gated by the per-session agent permission (`hop_set_agent_permission`) and requires the transcript on the same host as the MCP
147
207
  - `hop_close_terminal` — detach the terminal API session; optionally kill the underlying hop session
148
208
  - `hop_set_agent_permission` — allow or block agent access for a session
149
209
  - `hop_list_workspaces` — list available workspaces
@@ -156,7 +216,21 @@ Core tools (`hop_`): stable atomic operations.
156
216
  Helper tools (`hopx_`): convenience wrappers built on top of core tools.
157
217
  - `hopx_send_and_wait` — single-call send + wait wrapper
158
218
  - `hopx_exec` — Bash-tool-style shell execution: command in, clean stdout + `exit_code` out (see section above)
159
- - `hopx_agent_turn` — single-turn send + wait + mode-aware output, default `mode="auto"`
219
+ - `hopx_agent_turn` — single-turn send + wait + mode-aware output, default `mode="auto"`; optional `until_reply_regex` is a completion contract, and a mismatch returns `contract_failed`
220
+ - `hopx_capture_scrollback` — capture an alternate-screen TUI's scrollback the way a user would: scroll up page-by-page, snapshot each frame, and stitch newly-revealed rows together (live view restored afterward by default); best-effort and lossy for wrapped/redrawn content; requires agent permission on the session
221
+ - `hopx_agents_overview` — fleet status in one call: every agent-created/agent-permitted session with state (running/busy/idle), cwd, foreground program, Claude turn count, bell counters, last activity, and pending wait jobs; `include_user_sessions=false` narrows to agent-created only, `include_ports=true` adds proxy sessions
222
+ - `hopx_wait_any` — race several background waits (wait_ids and/or terminal_ids, auto-starting `until_agent_done` waits); returns the first real completion(s) plus the still-pending set; expired watches are re-armed automatically (new wait_id in `pending`) rather than reported as completions
223
+ - `hopx_spawn_agent` — one-call subagent bring-up: create terminal + launch agent CLI (claude/codex/gemini/custom) + wait until ready + optionally run a first task with an automatic completion contract; `isolation="worktree"` gives the worker its own git worktree + fleet/ branch for overlapping-file fleets
224
+ - the Claude preset uses the `sonnet` model alias and `bypassPermissions` mode by default, and removes inherited Claude Code process credentials before launch, so a delegated worker uses the host's durable `claude auth login` without parking on approval prompts; pass `permission_mode="manual"` for a restricted worker, `args="--model ..."` to choose another model, or an explicit `command` when inherited credentials are intentional
225
+ - the Codex preset runs autonomously and bypasses workspace-trust, approval, and self-update startup gates; Hop rejects detected login, trust, or update screens instead of dispatching a task into them
226
+ - MCP and `hopa spawn --task` wait by default and return the transcript-backed `assistant_reply`; `async=true` is for fleet dispatch through a long-lived MCP connection, while the short-lived `hopa` CLI rejects async waits
227
+ - `hopx_task_ledger` — the durable orchestration ledger (~/.hop2/orchestration): async dispatches with contracts survive manager/MCP restarts; pending entries reconcile lazily from turn counters + transcripts; acknowledge to consume
228
+
229
+ ### Reading terminal history
230
+
231
+ - `hop_read_terminal` — recent and incremental output; the day-to-day read primitive (cursor-based deltas).
232
+ - `hop_read_trajectory` — a Claude Code session's full conversation history from its transcript; prefer it when a transcript is available (the default `digest` mode is context-safe and shows far more than the terminal frame).
233
+ - `hopx_capture_scrollback` — scrollback of non-Claude TUIs, or when no transcript file is reachable; best-effort and lossy for wrapped/redrawn content.
160
234
 
161
235
  ## Resources
162
236
 
@@ -227,11 +301,15 @@ Helper tools (`hopx_`): convenience wrappers built on top of core tools.
227
301
 
228
302
  - For long-running conditions, start a background wait with `hop_wait_terminal(terminal_id=<id>, async=true, ...)` (returns `wait_id` immediately) and poll with `hop_wait_poll(wait_id=..., wait=true)`
229
303
  - use `consume=true` on poll to remove completed jobs
230
- - `hop_wait_start` is a deprecated legacy alias for `hop_wait_terminal(async=true)`; prefer the latter
304
+ - use `cancel=true` on poll to abort a still-running wait and reclaim it
305
+ - async jobs default to a 15-minute wait window when `max_wait_ms` is not given (synchronous waits default to 30s)
306
+ - `hop_wait_start` was removed; use `hop_wait_terminal(async=true)`
231
307
 
232
308
  ### `hopx_send_and_wait`
233
309
 
234
310
  - `hopx_send_and_wait` combines input send + optional enter/key + wait, and defaults to cursor-based delta capture from just before send.
311
+ - gotcha: `press_enter` defaults to **false** here but **true** on `hopx_agent_turn` (when data is provided) — sending text without `press_enter=true` types it but never submits it
312
+ - `clean_text=true` strips ANSI escape codes from captured output (also available on `hopx_agent_turn`)
235
313
  - helper defaults are token-thrifty for interactive loops: `capture_max_events=60`, `control_level=none`, `noise_filter=balanced`, `coalesce_ms=350` (unless overridden)
236
314
  - for `capture="readable_raw"`, `text_only` defaults to `true`; this condenses wait payloads by returning joined `wait.text`, setting `wait.events=[]`, `wait.eventCount=0`, and preserving prior count in `wait.originalEventCount` (set `text_only=false` to keep full events)
237
315
  - `hop_read_terminal` supports deterministic delta reads:
@@ -245,6 +323,7 @@ Helper tools (`hopx_`): convenience wrappers built on top of core tools.
245
323
  - in `mode="auto"`, if alternate-screen starts during the same turn, it auto-promotes to `ui` for that response
246
324
  - wait-only continuation is supported: omit `data`/`message` and use `control="wait"` to keep waiting on a terminal without sending new input
247
325
  - `async=true` starts the turn wait in the background and returns `wait_id` immediately; continue polling or controlling that turn with `hopx_agent_turn(wait_id=..., wait=true|false, control=...)`
326
+ - `until_reply_regex` is a completion contract; a finished turn that does not match returns `status="contract_failed"`, `ok=false`, and a tool error
248
327
  - `control="interrupt"` / `control="terminate"` send an explicit interrupt key (default `esc`); `terminate_message` can send a final follow-up after the interrupt
249
328
  - `mode="ui"` returns a UI snapshot payload after wait
250
329
  - for readable modes, `text_only` defaults to `true` and condenses waits the same way as `hopx_send_and_wait` (set `text_only=false` to keep full wait events)
package/README.md CHANGED
@@ -12,6 +12,10 @@ Hop gives you secure browser access to local terminals and a built-in MCP server
12
12
  o_(")(")
13
13
  ```
14
14
 
15
+ <p align="center">
16
+ <img src="docs/hero-overview.svg" width="980" alt="One hop runtime, three surfaces: a browser window, an MCP agent panel, and a CLI window all attached to the same live terminal session. Web sharing with presence and take/release control; MCP agents that create, drive, and read sessions; and a CLI that attaches from any terminal." />
17
+ </p>
18
+
15
19
  ## Why Hop
16
20
 
17
21
  - **Browser terminal from anywhere** — Tunnel through Cloudflare with no port forwarding
@@ -31,6 +35,14 @@ Most users only need the npm install. Source install is mainly for development o
31
35
  npm install -g hop2
32
36
  ```
33
37
 
38
+ That installs the **nightly** — a build from the latest green `main`, which
39
+ is usually what you want here since almost every commit is a fix. To hold
40
+ still on a version that was deliberately cut and used:
41
+
42
+ ```bash
43
+ npm install -g hop2@stable
44
+ ```
45
+
34
46
  This installs:
35
47
  - `hop` — main CLI / daemon / browser terminal entrypoint
36
48
  - `hop-mcp` — MCP server exposing Hop terminals and sessions
@@ -80,6 +92,10 @@ source ~/.zshrc
80
92
  hop
81
93
  ```
82
94
 
95
+ `hop` is safe to put at the end of your shell rc (`~/.zshrc`): it only launches
96
+ for interactive terminals — non-interactive shells (`ssh host <cmd>`, scp/rsync,
97
+ scripts, cron) and shells already inside a hop session are silently unaffected.
98
+
83
99
  First run:
84
100
  1. Optionally set a password with `hop password set`
85
101
  2. Scan the authenticator QR code
@@ -93,9 +109,10 @@ Then open the URL from another device, sign in, and pick or create a session.
93
109
  ```bash
94
110
  hop
95
111
  hop-mcp-setup
112
+ hop agent-skill install
96
113
  ```
97
114
 
98
- Then restart your MCP client. `hop-mcp-setup` auto-detects and configures supported clients such as Claude Code, Claude Desktop, Cursor, Gemini CLI, Codex CLI, VS Code / Copilot, and Antigravity.
115
+ Then restart your MCP client. `hop-mcp-setup` auto-detects and configures supported clients such as Claude Code, Claude Desktop, Cursor, Gemini CLI, Codex CLI, VS Code / Copilot, and Antigravity. `hop agent-skill install` installs Hop's bundled orchestration instructions for Claude Code and Codex.
99
116
 
100
117
  Typical use cases:
101
118
  - Launch a dedicated Claude Code / Codex / Gemini terminal and drive it over multiple turns
@@ -140,7 +157,7 @@ Use one dedicated terminal per subagent.
140
157
  2. Start the agent CLI with `hop_write_terminal`.
141
158
  3. Wait for readiness with `hop_wait_terminal(until_prompt=true)`.
142
159
  4. Prefer `hopx_agent_turn(mode="auto")` or `hopx_send_and_wait(...)` for one turn at a time.
143
- 5. For long waits, use `hopx_agent_turn(async=true, ...)` or the lower-level `hop_wait_terminal(async=true, ...)` + `hop_wait_poll(...)` (`hop_wait_start` remains only as a legacy alias).
160
+ 5. For long waits, use `hopx_agent_turn(async=true, ...)` or the lower-level `hop_wait_terminal(async=true, ...)` + `hop_wait_poll(...)` (`hop_wait_start` was removed; use `hop_wait_terminal(async=true)`).
144
161
  6. Interrupt with `hop_send_key(key="ctrl_c")` and close with `hop_close_terminal`.
145
162
 
146
163
  Safety tips:
@@ -172,7 +189,13 @@ hop attach <session>
172
189
  ```
173
190
 
174
191
  This is useful when you want a browser, an MCP client, and a local shell to converge on the same named session.
175
- Local terminal sessions are persistent by default: they keep running in the Hay host if the local CLI detaches or the Hop daemon restarts. The local CLI status line shows `persists`; use `Ctrl+Q` inside the local Hop view to kill the session.
192
+ Local terminal sessions are persistent by default: they keep running in the Hay host if the local CLI detaches or the Hop daemon restarts. Press `Ctrl+Q` twice inside the local Hop view to kill a session for everyone; `Ctrl+G` detaches and leaves it running.
193
+
194
+ The attached-terminal UI adapts light/dark themes to your terminal and puts the session state at a glance:
195
+
196
+ <p align="center">
197
+ <img src="docs/hero.svg" width="980" alt="The hop terminal UI, annotated: a session chip pinned flush-left, a live state dot (green connected, amber reconnecting, red dropped, purple locked), a scrollback position indicator, keycap-style hints, adaptive light/dark themes, and scrollback search with toast notices." />
198
+ </p>
176
199
 
177
200
  ### Use Hop on Mobile
178
201
 
@@ -180,9 +203,37 @@ Hop’s mobile UI includes:
180
203
 
181
204
  - a terminal-oriented accessory row with `Esc`, `Tab`, `Ctrl`, `Alt`, and arrows
182
205
  - a native keyboard button for dictation, spellcheck, and autocomplete
183
- - a floating menu for keyboard toggle and session switching
206
+ - a floating menu for keyboard toggle and session switching; the switcher starts with user sessions and can toggle to agent sessions or all sessions
184
207
  - draggable controls designed for one-handed use
185
208
 
209
+ ### Restore Sessions (incl. Claude Code)
210
+
211
+ A restart kills the underlying processes, but hop can rebuild your sessions and — for Claude Code — resume each conversation where it left off.
212
+
213
+ ```bash
214
+ hop claude-hook install # one-time: lets hop track each session's Claude conversation
215
+ hop restore # recreate saved sessions; claude ones resume their own conversation
216
+ hop restore --dry-run # preview what would be restored
217
+ ```
218
+
219
+ How it works: every hop terminal gets a `HOP_SESSION` env var; a Claude Code `SessionStart` hop hook records which conversation is running in each session. On `hop restore`, each session is reopened in its original directory and claude sessions relaunch with `claude --resume <id>` — so even multiple Claude sessions in the *same* directory each come back to their own conversation. Plain-shell sessions reopen a shell in their last directory and replay their last screen: on a graceful shutdown hop saves each session's recent output and, on restore, paints it back above a dim `──── session restored ────` separator before the fresh prompt — so you keep the context even though the old process is gone. `hop status` shows how many sessions can be restored.
220
+
221
+ `hop claude-hook install` adds a `SessionStart` entry to `~/.claude/settings.json` (backed up first); `hop claude-hook remove` reverts it. The hook is a no-op outside hop terminals.
222
+
223
+ ### Render LaTeX math (`hop math`)
224
+
225
+ Glance at a formula without leaving the terminal:
226
+
227
+ ```bash
228
+ hop math 'e^{i\pi} + 1 = 0'
229
+ hop math '\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}'
230
+ echo '\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}' | hop math
231
+ ```
232
+
233
+ Agents working inside hop terminals can render formulas for the humans watching by invoking the command themselves — install the bundled skills with `hop agent-skill install` so Claude and Codex sessions know to do this (plus ring the bell when blocked, and keep output phone-friendly).
234
+
235
+ `hop math` renders a **2D Unicode layout** — fractions, roots, sums/limits, sub/superscripts, Greek + operators, and accents — with no dependencies. Inline-image rendering (Kitty/iTerm) is deferred until graphics support lands in the web client (xterm.js): the real payoff is math that renders *inside* a shared hop session, which the re-rendering clients can't show yet, so until then it's Unicode everywhere.
236
+
186
237
  ## Operations
187
238
 
188
239
  ### Logging and History
package/demo/README.md CHANGED
@@ -10,6 +10,7 @@ Small reusable pieces for recording real Hop browser footage and driving real Ho
10
10
  - `static-preview-server.mjs` serves the sanitized demo workspace for preview clips
11
11
  - `stitch-demo-video.mjs` concatenates recorded WebM clips into one rough MP4
12
12
  - `hop-demo-lib.mjs` holds the shared state, auth, browser, and API helpers
13
+ - `capture/` holds the sanitized product-video capture rig (see [Capture Rig](#capture-rig-democapture) below)
13
14
 
14
15
  The harness uses the local Hop daemon port from `~/.hop2/.tunnel-state` and injects the Hop session cookie directly into Playwright. That avoids interactive login during capture while still exercising the real Hop UI.
15
16
 
@@ -97,6 +98,57 @@ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/stitch-demo-video.mjs \
97
98
  5. Record a mobile view of the same session
98
99
  6. Stitch the clips with `ffmpeg`
99
100
 
101
+ ## Capture Rig (`demo/capture/`)
102
+
103
+ The capture rig records the sanitized, dark-theme live-footage clips used in the Hop product video. It drives the real Hop UI in headless Chrome with two sanitization layers installed before first paint: `/api/sessions` responses are filtered down to the demo session cast, and a document-start MutationObserver rewrites the internal session names (`Aurora2` → `Aurora`, `Lyra2` → `Lyra`, …) in every text node and attribute. Before the kept portion of each clip it asserts a genuinely dark rendered frame (ffmpeg signalstats) and that no identity strings (account email banner, home directory, username) are visible; it aborts rather than save a bad take. Recording starts at page open and the setup head is trimmed off with ffmpeg on save.
104
+
105
+ ### Files
106
+
107
+ - `capture/capture.mjs` — the clip driver. `node demo/capture/capture.mjs <clip>` with `<clip>` one of `01-sessions`, `02-agent-live`, `02b-desktop-terminal`, `03-phone-live`, `04-phone-switcher`, `05-presence`, `06-math`, `07-theme`
108
+ - `capture/setup-sessions.mjs` — creates the sanitized shell cast (`Lyra2`, `Nebula2`, `Polaris2`) with a clean `env -i` bash, wiped scrollback, and live ticker output, and seeds the demo workspace tree
109
+ - `capture/setup-04.mjs` — recreates all four demo sessions phone-fit (46x40) for the `04-phone-switcher` clip, with a scripted agent-lookalike ticker in `Aurora2` instead of a real agent
110
+ - `capture/spawn-aurora.mjs` — spawns a real coding agent (default `claude`, override with `HOP_CAPTURE_AGENT`) in session `Aurora2` via the Hop MCP server
111
+ - `capture/dispatch-task.mjs` — sends a narration task to the `Aurora2` agent and waits until the identity banner has scrolled off the visible screen
112
+ - `capture/capture-env.mjs` — shared configuration: paths, env overrides, the demo cast, forbidden identity strings, and `terminals.json` helpers
113
+ - `capture/mcp-client.mjs` — minimal stdio MCP client for `mcp/hop-mcp.js`
114
+ - `capture/native-css.mjs` — document-start CSS that hides all web chrome for the `02b-desktop-terminal` "native terminal" clip
115
+ - `capture/tools/*.sh` — endless sanitized ticker scripts (build, metrics, agent-lookalike) that keep switcher preview cards alive
116
+
117
+ ### Prerequisites
118
+
119
+ Same as above (running Hop daemon, Google Chrome, vendored Playwright, Homebrew Node), plus `ffmpeg` and — for the live-agent clips — a `claude`-style agent CLI on `PATH`.
120
+
121
+ ### Configuration
122
+
123
+ Everything machine-specific is an env override with a macOS default (see `capture/capture-env.mjs`):
124
+
125
+ - `HOP_HOME` — hop state directory (default `~/.hop2`)
126
+ - `HOP_CAPTURE_ROOT` — sanitized demo tree: workspace, ticker scripts, fake `$HOME`, raw recordings, `terminals.json` (default `/tmp/hop-demo`)
127
+ - `HOP_CAPTURE_OUT` — where finished clips land (default `demo-output/footage/live` in the repo)
128
+ - `HOP_CAPTURE_CHROME`, `HOP_CAPTURE_FFMPEG` — binary paths
129
+ - `HOP_CAPTURE_AGENT` — command launched in `Aurora2` by `spawn-aurora.mjs` (default `claude`)
130
+ - `HOP_CAPTURE_FORBIDDEN` — extra comma-separated strings that must never appear in a kept frame, on top of the built-in home-directory/username/welcome-banner checks
131
+
132
+ ### Typical run
133
+
134
+ ```bash
135
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/setup-sessions.mjs
136
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/spawn-aurora.mjs
137
+
138
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 01-sessions
139
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 02-agent-live
140
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 03-phone-live
141
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 05-presence
142
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 06-math
143
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 07-theme
144
+
145
+ # Clip 04 uses its own phone-fit session setup:
146
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/setup-04.mjs
147
+ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 04-phone-switcher
148
+ ```
149
+
150
+ Notes on ordering: `02b-desktop-terminal` must run against an agent session whose welcome banner has already scrolled off — dispatch a first narration turn with `dispatch-task.mjs` (or run `02-agent-live` first). A clip that would show a leak, a light frame, or a finished/static screen aborts with `ABORT`/`RESHOOT` instead of saving; rerun it. Rerunning `setup-sessions.mjs` after `setup-04.mjs` restores the desktop-width cast, but `Aurora2` needs `spawn-aurora.mjs` again since `setup-04.mjs` replaces the real agent with a ticker.
151
+
100
152
  ## Notes
101
153
 
102
154
  - The current scripts are intentionally small. The new `director.mjs` is still a thin orchestrator, not a large demo framework.
@@ -0,0 +1,108 @@
1
+ // Shared configuration for the capture rig (demo/capture/*). Every
2
+ // machine-specific value lives here as an env override with a macOS default,
3
+ // so the individual scripts stay free of hardcoded paths and identities.
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { prepareSanitizedWorkspace } from "../hop-demo-lib.mjs";
8
+
9
+ const HERE = path.dirname(new URL(import.meta.url).pathname);
10
+ export const REPO_ROOT = path.resolve(HERE, "../..");
11
+
12
+ export const CHROME =
13
+ process.env.HOP_CAPTURE_CHROME ||
14
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
15
+ export const FFMPEG = process.env.HOP_CAPTURE_FFMPEG || "/opt/homebrew/bin/ffmpeg";
16
+
17
+ // Root of the sanitized demo tree: workspace/ (session cwd), tools/ (ticker
18
+ // scripts), home/ (fake $HOME for the sanitized shells), video/ (raw
19
+ // Playwright recordings before trim), terminals.json (session registry).
20
+ export const DEMO_ROOT = process.env.HOP_CAPTURE_ROOT || "/tmp/hop-demo";
21
+ export const WORKSPACE = path.join(DEMO_ROOT, "workspace");
22
+ export const TOOLS_DIR = path.join(DEMO_ROOT, "tools");
23
+ export const DEMO_HOME = path.join(DEMO_ROOT, "home");
24
+ export const VID_TMP = path.join(DEMO_ROOT, "video");
25
+ export const TERMINALS_PATH = path.join(DEMO_ROOT, "terminals.json");
26
+
27
+ // Where finished clips land.
28
+ export const OUT =
29
+ process.env.HOP_CAPTURE_OUT || path.join(REPO_ROOT, "demo-output", "footage", "live");
30
+
31
+ // Command launched inside Aurora2 by spawn-aurora.mjs.
32
+ export const AGENT_CMD = process.env.HOP_CAPTURE_AGENT || "claude";
33
+
34
+ // Demo cast, by INTERNAL session name. The capture driver's DOM rewriter
35
+ // shows the display names (Aurora/Lyra/Nebula/Polaris); Vega appears via the
36
+ // 01-sessions rename clip.
37
+ export const ALLOWED = ["Aurora2", "Lyra2", "Nebula2", "Polaris2", "Vega"];
38
+
39
+ // Strings that must NEVER be visible in a kept frame (identity leaks). The
40
+ // defaults cover the account-email welcome banner plus the real home
41
+ // directory and username; add more with HOP_CAPTURE_FORBIDDEN="a,b,c".
42
+ export const SCREEN_FORBIDDEN = [
43
+ "Welcome back",
44
+ os.homedir(),
45
+ os.userInfo().username,
46
+ ...(process.env.HOP_CAPTURE_FORBIDDEN || "")
47
+ .split(",")
48
+ .map((s) => s.trim())
49
+ .filter(Boolean)
50
+ ];
51
+
52
+ // Deterministic clean shell for demo sessions: env -i wipes the real
53
+ // environment, --norc/--noprofile skip dotfiles, PS1 is the demo prompt.
54
+ export const sanitizedShell = (prompt = "demo@hop workspace $ ") =>
55
+ `exec env -i HOME=${DEMO_HOME} USER=demo LOGNAME=demo TERM=xterm-256color ` +
56
+ "LANG=en_US.UTF-8 PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin " +
57
+ `PS1='${prompt}' /bin/bash --norc --noprofile`;
58
+
59
+ // Wipe screen AND scrollback (removes the real zsh prompt from history).
60
+ export const WIPE = "printf '\\033[2J\\033[3J\\033[H'";
61
+
62
+ export function loadTerminals({ required = true } = {}) {
63
+ if (!fs.existsSync(TERMINALS_PATH)) {
64
+ if (!required) return {};
65
+ throw new Error(
66
+ `${TERMINALS_PATH} not found — run setup-sessions.mjs (and spawn-aurora.mjs) first`
67
+ );
68
+ }
69
+ return JSON.parse(fs.readFileSync(TERMINALS_PATH, "utf8"));
70
+ }
71
+
72
+ export function saveTerminals(map) {
73
+ fs.mkdirSync(DEMO_ROOT, { recursive: true });
74
+ fs.writeFileSync(TERMINALS_PATH, JSON.stringify(map, null, 2));
75
+ }
76
+
77
+ // Seed the sanitized demo tree: the workspace files the agent tours, the
78
+ // ticker scripts the shell sessions run, the fake HOME, and the `hop math`
79
+ // wrapper used by clip 06. Idempotent; never overwrites existing workspace
80
+ // content except the rig-owned tools and wrapper.
81
+ export function seedDemoTree() {
82
+ fs.mkdirSync(DEMO_HOME, { recursive: true });
83
+ fs.mkdirSync(TOOLS_DIR, { recursive: true });
84
+ prepareSanitizedWorkspace(WORKSPACE, { reset: false });
85
+ for (const f of fs.readdirSync(path.join(HERE, "tools"))) {
86
+ fs.copyFileSync(path.join(HERE, "tools", f), path.join(TOOLS_DIR, f));
87
+ }
88
+ const wsScripts = path.join(WORKSPACE, "scripts");
89
+ fs.mkdirSync(wsScripts, { recursive: true });
90
+ fs.copyFileSync(
91
+ path.join(REPO_ROOT, "scripts", "hop-math.js"),
92
+ path.join(wsScripts, "hop-math.js")
93
+ );
94
+ const hopWrapper = path.join(WORKSPACE, "hop");
95
+ fs.writeFileSync(
96
+ hopWrapper,
97
+ `#!/usr/bin/env node
98
+ // hop (demo workspace copy) — \`hop math\` entry point.
99
+ if (process.argv[2] === 'math') {
100
+ const { runMathCli } = require('./scripts/hop-math.js');
101
+ process.exit(runMathCli(process.argv.slice(3)));
102
+ }
103
+ console.error('hop (demo): only \`hop math\` is available in this workspace');
104
+ process.exit(1);
105
+ `
106
+ );
107
+ fs.chmodSync(hopWrapper, 0o755);
108
+ }