handmux 0.5.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/README.zh-CN.md +285 -0
  4. package/bin/handmux.js +417 -0
  5. package/hooks/handmux-notify.sh +20 -0
  6. package/hooks/handmux-write.cjs +92 -0
  7. package/package.json +52 -0
  8. package/public/assets/index-BN-IwtP6.css +32 -0
  9. package/public/assets/index-BUQ0R83h.js +157 -0
  10. package/public/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  11. package/public/fonts/TWUnifont.woff2 +0 -0
  12. package/public/icons/apple-touch-icon.png +0 -0
  13. package/public/icons/badge-96.png +0 -0
  14. package/public/icons/icon-192.png +0 -0
  15. package/public/icons/icon-512.png +0 -0
  16. package/public/icons/logo.svg +32 -0
  17. package/public/index.html +105 -0
  18. package/public/manifest.webmanifest +37 -0
  19. package/public/offline.html +50 -0
  20. package/public/sw.js +117 -0
  21. package/src/.gitkeep +0 -0
  22. package/src/appName.js +23 -0
  23. package/src/asr/iflyConfig.js +10 -0
  24. package/src/asr/iflySign.js +16 -0
  25. package/src/auth.js +30 -0
  26. package/src/claudeEvents.js +212 -0
  27. package/src/cli/cfNamed.js +5 -0
  28. package/src/cli/claudeHooks.js +116 -0
  29. package/src/cli/cloudflareUrl.js +9 -0
  30. package/src/cli/cloudflared.js +53 -0
  31. package/src/cli/drivers.js +59 -0
  32. package/src/cli/options.js +169 -0
  33. package/src/cli/probe.js +16 -0
  34. package/src/cli/qr.js +34 -0
  35. package/src/cli/service.js +98 -0
  36. package/src/cli/setupWizard.js +248 -0
  37. package/src/cli/sshTunnel.js +12 -0
  38. package/src/cli/state.js +42 -0
  39. package/src/cli/supervisor.js +172 -0
  40. package/src/cli/tmuxConf.js +90 -0
  41. package/src/cli/tmuxVersion.js +49 -0
  42. package/src/cli/tunlite.js +22 -0
  43. package/src/config.js +6 -0
  44. package/src/docPath.js +46 -0
  45. package/src/docs.js +222 -0
  46. package/src/git.js +185 -0
  47. package/src/httpApi.js +546 -0
  48. package/src/previewServer.js +182 -0
  49. package/src/previews.js +118 -0
  50. package/src/push.js +121 -0
  51. package/src/server.js +97 -0
  52. package/src/staticCache.js +8 -0
  53. package/src/tmux/commands.js +223 -0
  54. package/src/trimCapture.js +28 -0
  55. package/src/uploadTypes.js +28 -0
  56. package/tmux/README.md +77 -0
  57. package/tmux/claude-tab-seed.py +67 -0
  58. package/tmux/claude-tab-seen.sh +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yuanyuanzijin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # handmux
2
+
3
+ **[handmux.com](https://handmux.com)** · *[中文文档 → README.zh-CN.md](README.zh-CN.md)*
4
+
5
+ [![npm](https://img.shields.io/npm/v/handmux?color=cb3837&logo=npm)](https://www.npmjs.com/package/handmux) [![CI](https://github.com/yuanyuanzijin/handmux/actions/workflows/test.yml/badge.svg)](https://github.com/yuanyuanzijin/handmux/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![node](https://img.shields.io/badge/node-%E2%89%A518-339933?logo=node.js&logoColor=white)](https://nodejs.org)
6
+
7
+ > **Vibe-code from your phone.** One command on your computer, scan a QR, and your live terminal —
8
+ > agent and all — is in your hand.
9
+
10
+ handmux puts the *same* live **tmux** session that's on your computer into your phone's browser —
11
+ **real panes, not a read-only mirror**. Spin up a brand-new session right from your phone, or pick up
12
+ one already running at your desk — then keep steering it from the couch, the train, a queue at the
13
+ coffee shop. The agent never stops; you just change screens.
14
+ **Nothing to install on the phone — open a link and you're in**, or add it to your home screen as a
15
+ PWA that runs full-screen, basically like a native app. It works with any shell or TUI, and
16
+ goes deepest with **Claude Code**: it pushes you the moment a pane needs a decision, and you approve
17
+ with your thumb.
18
+
19
+ <p align="center">
20
+ <img src="assets/handmux-approve.gif" alt="handmux: a Claude Code permission lands on your phone and you approve it with a tap" width="280">
21
+ &nbsp;&nbsp;
22
+ <img src="assets/handmux-git.gif" alt="handmux: open a doc as rendered Markdown, then read a git diff full-screen" width="280">
23
+ <br>
24
+ <em>Real phone browser, real panes — a push pings you to approve a Claude Code prompt (left); read a doc as Markdown (even aloud) &amp; review a git diff (right).</em>
25
+ </p>
26
+
27
+ ## Why handmux
28
+
29
+ - **🚀 One command to go live.** `handmux start`, scan the QR — done. No account, no App Store, no
30
+ native app to sideload. Just a link your phone opens in any browser.
31
+ - **🧶 Your real session, in your pocket.** Not a fresh shell and not a screenshot — the *exact* tmux
32
+ panes from your computer, agent still running. Desk → phone → desk, same session the whole way.
33
+ - **🤖 Made for vibe coding with agents.** Deepest with Claude Code: a push the instant it needs you,
34
+ an inbox of which pane is *working / waiting on you / done*, and plan & permission approvals you tap
35
+ through. Codex, aider, any shell/TUI work too.
36
+
37
+ ## Get started in 2 minutes
38
+
39
+ You need two things **on the computer** (the phone needs only a browser). If you already live in tmux,
40
+ you're basically there:
41
+
42
+ ```bash
43
+ node -v # need Node ≥ 18 — get it at https://nodejs.org
44
+ tmux -V # need tmux ≥ 3.0 — `brew install tmux` / `apt install tmux`
45
+ ```
46
+
47
+ Then install and run:
48
+
49
+ ```bash
50
+ npm i -g handmux # install once
51
+ handmux start # run it — local / same-wifi only, nothing exposed
52
+ ```
53
+
54
+ `start` prints a **QR code** (plus the URL and a token). **Scan the QR with your phone** — it carries
55
+ the token, so you're signed in on the first load. That's it: you'll see your real tmux sessions, tap
56
+ one, and you're driving it.
57
+
58
+ Want to reach it from **anywhere**, not just your wifi? One flag spins up a free public HTTPS link:
59
+
60
+ ```bash
61
+ handmux start --tunnel cloudflare # instant public URL (auto-installs cloudflared)
62
+ handmux setup # or configure tunnel + name + notifications once, saved
63
+ ```
64
+
65
+ ```
66
+ tunnel cloudflare · pid 21352
67
+ 🌐 open https://elementary-incidents.trycloudflare.com/
68
+ 💻 local http://localhost:19999/
69
+ 🔑 token aicbHOGW…
70
+ ```
71
+
72
+ The printed links are token-free — safe to screenshot or share. Only the **QR** carries the token, and
73
+ the `🔑 token` line is your password: paste it to sign in when you open a plain link instead of scanning.
74
+
75
+ ## Features
76
+
77
+ Not just a remote shell — a full **mobile cockpit** for your terminal and your coding agents.
78
+
79
+ **Built around Claude Code**
80
+
81
+ - **Pinged when it needs you** — a push the moment a pane hits a permission prompt, a plan approval, or finishes, even with the tab closed.
82
+ - **Agent inbox** — every Claude pane tagged *working / waiting on you / done*; jump straight to the one that's blocked.
83
+ - **Approve with your thumb** — answer permission prompts and plan approvals from the phone; it drives the real keys, so a tap is a real keystroke.
84
+ - **Voice input** — dictate the next prompt hands-free (optional; bring your own iFlytek keys).
85
+
86
+ **A real cockpit, on your phone**
87
+
88
+ - **Git viewer** — VS Code-style: changes, commit history, any branch, full-screen colored diffs, multi-repo tabs. Read-only, never touches your working tree.
89
+ - **Live preview** — preview a static site from a folder, or a running service by port, with routing/API/HMR intact; phone or desktop viewport.
90
+ - **Docs** — tap a path in the terminal to open it; Markdown rendered, font zoom, read-aloud with sentence-by-sentence highlight.
91
+ - **Files both ways** — multi-select upload from the chat box (paths auto-filled), download with confirm, share into the app, copy absolute paths.
92
+ - **Ideas & commands** — a per-window to-do list (voice in, one-tap insert) plus a command palette with frequent/recent and slash shortcuts (`/compact`, `/model`, `/loop`…).
93
+ - **Image viewer** — pinch-zoom, save/share, inline GIFs.
94
+
95
+ **Solid on a phone**
96
+
97
+ - Real tmux panes — any TUI, shell or agent — not a read-only mirror.
98
+ - Reconnect with backoff, a connection-lost banner, an offline fallback page, polling paused when hidden.
99
+ - Reflow-safe cursor, drag-to-select copy, auto-repeat key bar, keyboard auto-lift.
100
+ - Nothing to install — runs in the phone browser; optional add-to-home-screen PWA. Bilingual (English / 中文).
101
+
102
+ ## Once you're in
103
+
104
+ - You'll see your real tmux sessions — tap one to attach. Type in the terminal; use the on-screen
105
+ key bar for arrows / Ctrl / Tab / Esc, and switch sessions, windows and panes from the top bar.
106
+ - **Add to Home Screen** (Safari/Chrome share menu) to run it full-screen like an app.
107
+ - The screen survives flaky networks — it keeps the last good frame, shows a "connection lost" banner
108
+ after repeated failures, and pauses while the tab is hidden.
109
+
110
+ ## Commands
111
+
112
+ ```
113
+ handmux start [flags] start server (+ tunnel), in the background
114
+ handmux setup configure tunnel / name / notifications (writes config; re-run to change)
115
+ handmux stop stop everything
116
+ handmux restart
117
+ handmux status show state + current access URL
118
+ handmux logs [--follow] [--lines N] tail the supervisor log
119
+ handmux config show the effective config + where each value came from
120
+ handmux hooks install|uninstall enable/disable Claude Code notifications (inbox)
121
+ handmux service install [start-flags] start at login (launchd / systemd --user)
122
+ handmux service uninstall remove the autostart entry
123
+ handmux --version print the version
124
+ ```
125
+
126
+ **The whole config story is two doors:** `handmux start` just runs it (no config needed — defaults to
127
+ LAN-only, auto-generates a token, prints a QR), and `handmux setup` is the one place to configure
128
+ persistently. Re-run `setup` to change anything. That's it; everything below is detail.
129
+
130
+ ### Claude Code notifications (inbox)
131
+
132
+ The agent inbox and "pinged when it needs you" push are driven by Claude Code lifecycle hooks. They're
133
+ **opt-in** — `handmux hooks install` copies a tiny notify script into `~/.claude/hooks/` and registers six
134
+ hook events in `~/.claude/settings.json` (idempotent; leaves your own hooks alone). `handmux setup` offers
135
+ this too, and you can turn it on from the phone the first time you open the inbox. `handmux hooks uninstall`
136
+ removes it. If you don't use Claude Code, this is skipped — nothing touches `~/.claude`.
137
+
138
+ After installing the hooks it also offers (opt-in) to add a small block to `~/.tmux.conf` so each tmux
139
+ window tab shows a live Claude status dot (working / needs you / done) — the same signal as the inbox, on
140
+ your terminal. Decline it and nothing is changed; the block is marked and easy to remove later.
141
+
142
+ ### start flags
143
+
144
+ Flags override the config file for **one run** and never persist — handy for a quick try
145
+ (`handmux start --tunnel cloudflare`) without touching your saved setup. For anything permanent, use
146
+ `handmux setup`.
147
+
148
+ ```
149
+ --tunnel none|cloudflare|cloudflare-named|ssh how to expose it (default: none — local/LAN only)
150
+ --port N server port (default: 19999)
151
+ --host H bind host (default: 0.0.0.0)
152
+ --token S auth token (default: generated, printed on start)
153
+ --name "My Box" app name in the browser tab + home-screen icon label
154
+ --preview-domain D enable dynamic port previews (needs a wildcard subdomain)
155
+ --config PATH use this config file instead of ~/.handmux/config.json (dev / multiple configs)
156
+ --foreground, -f run in the foreground instead of daemonizing
157
+ --no-qr don't render the QR code
158
+
159
+ # ssh tunnel (--tunnel ssh):
160
+ --ssh-host user@host[:port] the server to reverse-forward to (tunlite)
161
+ --remote-port N port bound on that server (default: same as --port)
162
+ --ssh-jump user@host[,…] optional bastion/jump host(s)
163
+ --public-url URL the public URL to advertise (default: http://<host>:<remote-port>)
164
+ # cloudflare-named (--tunnel cloudflare-named):
165
+ --cf-hostname H your Cloudflare hostname (e.g. handmux.example.com)
166
+ --cf-tunnel-name N named-tunnel name (default: handmux)
167
+ ```
168
+
169
+ ### Configuration
170
+
171
+ There are **two layers**, and that's the whole model:
172
+
173
+ - **The config file** is your machine's persistent setup (tunnel, token, push/voice keys). There is one
174
+ location — `~/.handmux/config.json` — written by `handmux setup`. Pass `--config PATH` to use a
175
+ different file (e.g. keep `dev.json` / `prod.json` side by side and pick one). No merging or inheritance:
176
+ at most one file is read.
177
+ - **Flags** override individual settings **for that one run only** and are never written back.
178
+
179
+ Precedence for a setting: **flag > config file > built-in default.** `start` prints which file it loaded
180
+ (`config: …`), and `handmux config` shows the value each setting resolves to **and where it came from**
181
+ (flag / file / env / default), so flag-vs-file is never a mystery.
182
+
183
+ You normally never hand-edit the file — `handmux setup` writes it (and re-running edits it). If you do
184
+ want to, it's plain JSON; optional integrations live in the **same file** (no separate `.env`):
185
+
186
+ ```jsonc
187
+ {
188
+ "tunnel": "none", // none | cloudflare | cloudflare-named | ssh
189
+ "port": 19999,
190
+ "host": "0.0.0.0",
191
+ "name": "My Box", // browser-tab / home-screen label; omit → default
192
+ "token": "…", // omit/empty → auto-generated on first start
193
+ "previewDomain": "preview.example.com",
194
+ "vapid": { "public": "…", "private": "…", "subject": "mailto:you@example.com" }, // push
195
+ "xfyun": { "appId": "…", "apiKey": "…", "apiSecret": "…" } // voice
196
+ // ssh tunnel adds: "sshHost", "remotePort", "sshJump", "publicUrl"
197
+ // cloudflare-named adds: "cfHostname", "cfTunnelName"
198
+ }
199
+ ```
200
+
201
+ The file is written `0600` because it holds a token and push/voice secrets.
202
+
203
+ ## Networking: two paths
204
+
205
+ | mode | edge | TLS / hostname | best for |
206
+ |------|------|----------------|----------|
207
+ | **cloudflare** | Cloudflare's global edge (free quick tunnel) | automatic, random `*.trycloudflare.com` | quick start, zero config |
208
+ | **self-hosted (ssh)** | *your own VPS* | your domain + cert (Caddy auto-HTTPS recommended) | stable access, your own domain, regions where Cloudflare is unreliable |
209
+
210
+ > The `ssh` self-hosted tunnel (engine: [`tunlite run`](https://www.npmjs.com/package/tunlite), bundled) is
211
+ > available now — run `handmux setup` (or `--tunnel ssh --ssh-host user@host`). A `cloudflare-named` tunnel
212
+ > (stable HTTPS on your own Cloudflare domain) is available the same way.
213
+
214
+ ### Self-hosted ssh tunnel: server-side reverse proxy
215
+
216
+ `tunlite` reverse-forwards your local port to your own server (bound to `127.0.0.1:<remote-port>` by
217
+ default — not exposed to the public internet until you put a reverse proxy in front of it).
218
+
219
+ **nginx (existing install):**
220
+
221
+ ```nginx
222
+ server {
223
+ server_name handmux.example.com;
224
+ client_max_body_size 60m; # prevents "file too large" on mobile uploads
225
+ location / {
226
+ proxy_pass http://127.0.0.1:19999; # = handmux --remote-port
227
+ proxy_http_version 1.1;
228
+ proxy_set_header Upgrade $http_upgrade;
229
+ proxy_set_header Connection "upgrade";
230
+ proxy_read_timeout 90s; # tolerates long-polling
231
+ }
232
+ }
233
+ # Run certbot for TLS; add an A record pointing your domain at this server.
234
+ ```
235
+
236
+ **No nginx — Caddy (automatic Let's Encrypt, two lines):**
237
+
238
+ ```caddy
239
+ handmux.example.com {
240
+ reverse_proxy 127.0.0.1:19999
241
+ }
242
+ ```
243
+
244
+ No TLS needed? Bind tunlite to `0.0.0.0` and set `GatewayPorts yes` in sshd, then access via
245
+ `http://<host>:<remote-port>` directly (unencrypted).
246
+
247
+ ## Autostart
248
+
249
+ ```bash
250
+ handmux service install --tunnel cloudflare # comes back after reboot/login
251
+ ```
252
+
253
+ macOS uses a launchd LaunchAgent; Linux uses a `systemd --user` unit (for autostart
254
+ before you log in: `loginctl enable-linger "$USER"`). While the service is installed,
255
+ `handmux stop` is temporary (the OS restarts it) — use `service uninstall` to stop for good.
256
+
257
+ ## Security
258
+
259
+ The access URL is public when you use a tunnel, so a **token is always required** (one is
260
+ generated if you don't pass `--token`). The printed plain link is token-free and safe to share;
261
+ treat the token (and the token-bearing QR) like a password.
262
+
263
+ Found a security issue? Please report it privately — see [SECURITY.md](SECURITY.md), not a public issue.
264
+
265
+ ## Voice input (optional)
266
+
267
+ Tapping the mic dictates into the input box. It's powered by [iFlytek](https://www.xfyun.cn/) and is
268
+ **off until you add your own keys** — open a "语音听写 (IAT)" app at the iFlytek console, then add an
269
+ `"xfyun": { "appId": "…", "apiKey": "…", "apiSecret": "…" }` block to your config file (see *Configuration*).
270
+ The secret stays on the server; the phone only ever gets a short-lived signed URL. With no keys configured
271
+ the mic button simply doesn't show.
272
+
273
+ ## Push notifications (optional)
274
+
275
+ The "pinged when a pane needs you / finishes" push is **off until you add a VAPID key pair** (the standard
276
+ Web Push credential). Generate one with the bundled `web-push`:
277
+
278
+ ```bash
279
+ npx web-push generate-vapid-keys
280
+ ```
281
+
282
+ Add a `"vapid": { "public": "…", "private": "…", "subject": "mailto:you@example.com" }` block to your
283
+ config file (see *Configuration*). With both keys set, `/api/push/vapid` serves the public key and the
284
+ phone can subscribe; with none, the endpoint returns 503 and the bell stays hidden. Push also requires the
285
+ Claude Code hooks (so a pane has a state to push) — see *Claude Code notifications* above.
286
+
287
+ ## Dynamic port previews (advanced)
288
+
289
+ Set `--preview-domain` (or `"previewDomain"` in the config) to expose other local dev
290
+ servers (e.g. a Vite app on `:3000`) to your phone, each on its own subdomain. This needs a
291
+ **wildcard subdomain** (`*.your.domain`) pointed at the gateway, so it only works on the
292
+ self-hosted path, not a quick tunnel. `handmux setup` does not wire this for you — set it up
293
+ yourself and point `previewDomain` at it.
294
+
295
+ **TLS depth (Cloudflare):** a browser reaches the preview over HTTPS, so the wildcard needs a
296
+ cert. Cloudflare's free Universal SSL covers **one level** — `*.example.com` works (previews at
297
+ `<port>.example.com`), but a deeper `*.preview.example.com` needs **Advanced Certificate
298
+ Manager**. So either keep previews one level deep, or enable ACM. (On the ssh/own-edge path you
299
+ provide the wildcard cert yourself — e.g. a Let's Encrypt `*.preview.your.domain`.)
300
+
301
+ ## License
302
+
303
+ MIT
@@ -0,0 +1,285 @@
1
+ # handmux
2
+
3
+ **[handmux.com](https://handmux.com)** · *[English → README.md](README.md)*
4
+
5
+ [![npm](https://img.shields.io/npm/v/handmux?color=cb3837&logo=npm)](https://www.npmjs.com/package/handmux) [![CI](https://github.com/yuanyuanzijin/handmux/actions/workflows/test.yml/badge.svg)](https://github.com/yuanyuanzijin/handmux/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![node](https://img.shields.io/badge/node-%E2%89%A518-339933?logo=node.js&logoColor=white)](https://nodejs.org)
6
+
7
+ > **在手机上 vibe 编程。** 电脑上一条命令、扫个二维码,你那个活着的终端——连同正在跑的 agent
8
+ > ——就到手里了。
9
+
10
+ handmux 把你电脑上那个**活着的 tmux 会话**原样搬进手机浏览器——**同一个、真实的 pane,不是只读
11
+ 镜像**。直接在手机上新建一个会话,或接手工位上已经在跑的那个——然后窝在沙发上、挤地铁时、排队买咖啡时接着盯它跑。进程不停,你只是换了块屏。
12
+ **手机端零安装——点开链接就进去了**;还能"添加到主屏",作为 PWA 全屏运行,体验和原生 App 基本一致。任意 shell / TUI 都能开,跟 **Claude Code** 配合最深:哪个 pane
13
+ 要你拍板,第一时间推到手机上,拇指点一下就批。
14
+
15
+ <p align="center">
16
+ <img src="assets/handmux-approve.gif" alt="handmux:Claude Code 的授权请求推到手机上,拇指一点就批" width="280">
17
+ &nbsp;&nbsp;
18
+ <img src="assets/handmux-git.gif" alt="handmux:把文档当 Markdown 渲染打开,再全屏读一段 git diff" width="280">
19
+ <br>
20
+ <em>真实手机浏览器、真实 pane——左:推送提醒,拇指批 Claude Code 的授权;右:把文档当 Markdown 渲染(还能朗读)并看 git diff。</em>
21
+ </p>
22
+
23
+ ## 为什么是 handmux
24
+
25
+ - **🚀 一条命令就上线。** `handmux start`,扫码,完事。不用注册、不上应用商店、不用旁加载原生 App,
26
+ 就是一个手机浏览器能点开的链接。
27
+ - **🧶 你真实的会话,装进口袋。** 不是新开个 shell、也不是截图——是你电脑上**那一个** tmux pane,agent
28
+ 照跑。工位 → 手机 → 工位,全程同一个会话。
29
+ - **🤖 为「跟 agent 一起 vibe 编程」而生。** 跟 Claude Code 最深:要你时立刻推送、收件箱标好哪个 pane
30
+ 「进行中 / 等你 / 已完成」、批计划批授权点一下就过。Codex、aider、任意 shell/TUI 也都行。
31
+
32
+ ## 2 分钟零门槛上手
33
+
34
+ **电脑上**需要两样东西(手机只要个浏览器)。如果你本来就泡在 tmux 里,基本已经齐了:
35
+
36
+ ```bash
37
+ node -v # 需要 Node ≥ 18 —— 没有就去 https://nodejs.org
38
+ tmux -V # 需要 tmux ≥ 3.0 —— `brew install tmux` / `apt install tmux`
39
+ ```
40
+
41
+ 然后装上、跑起来:
42
+
43
+ ```bash
44
+ npm i -g handmux # 装一次
45
+ handmux start # 跑起来 —— 仅本机 / 同 wifi,不对外暴露
46
+ ```
47
+
48
+ `start` 会打印一个**二维码**(外加地址和 token)。**手机扫二维码**——token 在二维码里,首次打开即登录。
49
+ 就这样:你会看到自己真实的 tmux 会话,点一个,就开始操作了。
50
+
51
+ 想从**任何地方**(不只同一 wifi)都连得上?加一个参数就开一条免费公网 HTTPS 链接:
52
+
53
+ ```bash
54
+ handmux start --tunnel cloudflare # 即时公网地址(自动装 cloudflared)
55
+ handmux setup # 或一次性配好隧道 + 名称 + 通知,落盘
56
+ ```
57
+
58
+ ```
59
+ tunnel cloudflare · pid 21352
60
+ 🌐 open https://elementary-incidents.trycloudflare.com/
61
+ 💻 local http://localhost:19999/
62
+ 🔑 token aicbHOGW…
63
+ ```
64
+
65
+ 打印出来的明文链接不带 token,可安全截图/分享。只有**二维码**带 token;`🔑 token` 那行就是你的密码——
66
+ 开明文链接时把它粘进去就能登录。
67
+
68
+ ## 功能
69
+
70
+ 不止是个远程 shell——是给终端和你的编码 agent 配的一整个**手机驾驶舱**。
71
+
72
+ **围绕 Claude Code 打造**
73
+
74
+ - **需要你,就推给你**——某个 pane 弹了授权、要你批计划、或者干完了,那一刻就推一条到手机,标签页关着照样收得到。
75
+ - **Agent 收件箱**——每个 Claude pane 标好「进行中 / 等你 / 已完成」,哪个卡住点一下直接过去。
76
+ - **拇指点一下就批**——授权、批计划直接在手机上点;手机敲的就是真实按键,点一下等于在键盘上敲了一下。
77
+ - **语音输入**——下一条 prompt 张嘴就说,不用打字(可选,需自备讯飞 key)。
78
+
79
+ **真·驾驶舱,装进手机**
80
+
81
+ - **Git 查看器**——像 VS Code 那样:改动、提交记录、随便哪个分支、全屏彩色 diff、多仓库分页。只读,绝不动你的工作区。
82
+ - **站点预览**——挑个目录预览静态站,或填端口预览正跑着的服务,路由/接口/HMR 全保留;手机、电脑视图随你切。
83
+ - **文档**——终端里的路径点一下就打开;Markdown 排版、字号缩放、一句句高亮念出来。
84
+ - **文件随手传**——聊天框多选上传(自动填好路径)、下载要确认、系统分享进来、绝对路径随手复制。
85
+ - **想法和命令**——每个窗口一份待办(能语音记、一点填入)+ 命令面板(常用/最近 + 斜杠命令 `/compact`、`/model`、`/loop`…)。
86
+ - **图片查看**——捏合缩放、保存/分享、内嵌 GIF。
87
+
88
+ **在手机上够稳**
89
+
90
+ - 真实 tmux pane,任何 TUI、shell、agent 都行,不是只读镜像。
91
+ - 断线自动退避重连、掉线有横幅、离线有兜底页、切到后台就暂停轮询。
92
+ - 光标不乱跳、长按拖动选中复制、按键区长按连发、键盘自动抬起来。
93
+ - 零安装——手机浏览器直接跑;愿意的话"加到主屏"就成了 PWA。双语(English / 中文)。
94
+
95
+ ## 进去之后
96
+
97
+ - 你会看到自己真实的 tmux 会话——点一个进入。终端里直接打字;用屏幕上的按键条按方向键 /
98
+ Ctrl / Tab / Esc,顶栏切换会话、窗口、pane。
99
+ - **添加到主屏幕**(Safari/Chrome 分享菜单),即可像 App 一样全屏运行。
100
+ - 画面能扛弱网——失败时保留上一帧好画面,连续失败显示"连接丢失"横幅,标签页隐藏时自动暂停。
101
+
102
+ ## 命令
103
+
104
+ ```
105
+ handmux start [flags] 后台启动服务(+ 隧道)
106
+ handmux setup 配置隧道 / 名称 / 通知(写入配置;重跑即修改)
107
+ handmux stop 全部停止
108
+ handmux restart
109
+ handmux status 查看状态 + 当前访问地址
110
+ handmux logs [--follow] [--lines N] 查看 supervisor 日志
111
+ handmux config 打印生效配置 + 每个值的来源
112
+ handmux hooks install|uninstall 开启 / 关闭 Claude Code 通知(收件箱)
113
+ handmux service install [start-flags] 登录时自启(launchd / systemd --user)
114
+ handmux service uninstall 移除自启项
115
+ handmux --version 打印版本号
116
+ ```
117
+
118
+ **整个配置就两道门:** `handmux start` 直接跑(不需要配置——默认仅局域网、自动生成 token、出二维码),
119
+ `handmux setup` 是**唯一**的持久化配置入口。要改任何东西,重跑 `setup`。就这么简单;下面都是细节。
120
+
121
+ ### Claude Code 通知(收件箱)
122
+
123
+ Agent 收件箱和「需要你就推给你」靠的是 Claude Code 的生命周期 hook。它**默认不开、需你显式开启**——
124
+ `handmux hooks install` 会把一个很小的通知脚本拷进 `~/.claude/hooks/`,并往 `~/.claude/settings.json`
125
+ 注册六个 hook 事件(幂等;不动你自己的 hook)。`handmux setup` 也会问一句,首次在手机上打开收件箱时
126
+ 还能一键开启。`handmux hooks uninstall` 移除它。不用 Claude Code 的话直接跳过——绝不碰 `~/.claude`。
127
+
128
+ 装完 hook 后,它还会(opt-in)问一句要不要往 `~/.tmux.conf` 加一小段配置,让每个 tmux 窗页签也显示一个
129
+ 实时的 Claude 状态点(进行中 / 需要你 / 已完成)——和收件箱同一个信号,直接画在你的终端上。拒绝则什么都不改;
130
+ 这段配置带标记,以后想删很容易。
131
+
132
+ ### start 参数
133
+
134
+ 参数只对**这一次运行**覆盖配置文件、绝不落盘——适合不动已存配置就快速试一下
135
+ (`handmux start --tunnel cloudflare`)。要永久生效,用 `handmux setup`。
136
+
137
+ ```
138
+ --tunnel none|cloudflare|cloudflare-named|ssh 如何对外暴露(默认 none —— 仅本机/局域网)
139
+ --port N 服务端口(默认 19999)
140
+ --host H 绑定地址(默认 0.0.0.0)
141
+ --token S 鉴权 token(默认自动生成并打印)
142
+ --name "My Box" 浏览器标签页 + 主屏幕图标显示的应用名
143
+ --preview-domain D 开启端口动态预览(需要通配子域名)
144
+ --config PATH 改用指定配置文件,而非 ~/.handmux/config.json(开发 / 多套配置)
145
+ --foreground, -f 前台运行,不守护
146
+ --no-qr 不渲染二维码
147
+
148
+ # ssh 隧道(--tunnel ssh):
149
+ --ssh-host user@host[:port] 反向转发到的服务器(tunlite)
150
+ --remote-port N 在该服务器上绑定的端口(默认同 --port)
151
+ --ssh-jump user@host[,…] 可选的跳板/堡垒机
152
+ --public-url URL 对外公布的地址(默认 http://<host>:<remote-port>)
153
+ # cloudflare-named(--tunnel cloudflare-named):
154
+ --cf-hostname H 你的 Cloudflare 域名(如 handmux.example.com)
155
+ --cf-tunnel-name N 命名隧道的名字(默认 handmux)
156
+ ```
157
+
158
+ ### 配置
159
+
160
+ 就**两层**,这就是全部模型:
161
+
162
+ - **配置文件**是这台机器的持久设置(隧道、token、推送/语音密钥)。位置只有一个——
163
+ `~/.handmux/config.json`,由 `handmux setup` 写入。用 `--config PATH` 可指向别的文件(比如 `dev.json` /
164
+ `prod.json` 并存、按需选)。**不合并、不继承**:最多只读一个文件。
165
+ - **参数**只对**这一次运行**覆盖单项,绝不写回。
166
+
167
+ 单项优先级:**参数 > 配置文件 > 内置默认。** 启动时会打印加载了哪个文件(`config: …`);`handmux config`
168
+ 则逐项打印每个设置**解析成什么值、来自哪里**(参数 / 文件 / 环境 / 默认),flag 和文件谁赢一目了然。
169
+
170
+ 通常你不用手改文件——`handmux setup` 替你写(重跑即修改)。真要手改也行,就是普通 JSON;可选功能写进
171
+ **同一个文件**(不再有单独的 `.env`):
172
+
173
+ ```jsonc
174
+ {
175
+ "tunnel": "none", // none | cloudflare | cloudflare-named | ssh
176
+ "port": 19999, "host": "0.0.0.0",
177
+ "name": "My Box", // 浏览器标签 / 主屏图标名;省略 → 默认
178
+ "token": "…", // 留空 → 首次启动自动生成
179
+ "previewDomain": "preview.example.com",
180
+ "vapid": { "public": "…", "private": "…", "subject": "mailto:you@example.com" }, // 推送
181
+ "xfyun": { "appId": "…", "apiKey": "…", "apiSecret": "…" } // 语音
182
+ // ssh 隧道追加:"sshHost"、"remotePort"、"sshJump"、"publicUrl"
183
+ // cloudflare-named 追加:"cfHostname"、"cfTunnelName"
184
+ }
185
+ ```
186
+
187
+ 文件以 `0600` 写入(含 token 和推送/语音密钥)。
188
+
189
+ ## 联网:两条路
190
+
191
+ | 方式 | 中转/边缘 | TLS / 域名 | 适合 |
192
+ |------|----------|-----------|------|
193
+ | **cloudflare** | Cloudflare 全球边缘(免费快速隧道) | 自动,随机 `*.trycloudflare.com` | 快速尝鲜、零配置 |
194
+ | **自建(ssh)** | *你自己的 VPS* | 你的域名 + 证书(推荐 Caddy 自动 HTTPS) | 稳定访问、自有域名、Cloudflare 不稳的地区 |
195
+
196
+ > 自建(ssh)隧道(引擎:[`tunlite run`](https://www.npmjs.com/package/tunlite),已打包)现已可用——
197
+ > 运行 `handmux setup`(或 `--tunnel ssh --ssh-host user@host`)。`cloudflare-named` 隧道(在你自己的
198
+ > Cloudflare 域名上获得稳定 HTTPS)也用同样方式开启。
199
+
200
+ ### 自建 ssh 隧道:服务端反向代理
201
+
202
+ `tunlite` 把你的本地端口反向转发到你自己的服务器(默认绑在 `127.0.0.1:<remote-port>`——
203
+ 在你给它加反向代理之前,不会暴露到公网)。
204
+
205
+ **nginx(已有安装):**
206
+
207
+ ```nginx
208
+ server {
209
+ server_name handmux.example.com;
210
+ client_max_body_size 60m; # 防止手机上传时报「文件过大」
211
+ location / {
212
+ proxy_pass http://127.0.0.1:19999; # = handmux --remote-port
213
+ proxy_http_version 1.1;
214
+ proxy_set_header Upgrade $http_upgrade;
215
+ proxy_set_header Connection "upgrade";
216
+ proxy_read_timeout 90s; # 容忍长轮询
217
+ }
218
+ }
219
+ # 用 certbot 配 TLS;再加一条 A 记录把域名指向这台服务器。
220
+ ```
221
+
222
+ **没有 nginx——Caddy(自动 Let's Encrypt,两行):**
223
+
224
+ ```caddy
225
+ handmux.example.com {
226
+ reverse_proxy 127.0.0.1:19999
227
+ }
228
+ ```
229
+
230
+ 不需要 TLS?把 tunlite 绑到 `0.0.0.0`、在 sshd 里设 `GatewayPorts yes`,然后直接用
231
+ `http://<host>:<remote-port>` 访问(明文不加密)。
232
+
233
+ ## 开机自启
234
+
235
+ ```bash
236
+ handmux service install --tunnel cloudflare # 重启/登录后自动回来
237
+ ```
238
+
239
+ macOS 用 launchd LaunchAgent;Linux 用 `systemd --user` unit(未登录也要自启:
240
+ `loginctl enable-linger "$USER"`)。服务装着时,`handmux stop` 只是临时(系统会重新拉起)
241
+ ——要彻底停用 `service uninstall`。
242
+
243
+ ## 安全
244
+
245
+ 用隧道时访问地址是公开的,所以**始终要求 token**(不传 `--token` 会自动生成一个)。打印出来的
246
+ 明文链接不带 token、可安全分享;**把 token(以及带 token 的二维码)当密码对待。**
247
+
248
+ 发现安全问题?请私下报告——见 [SECURITY.md](SECURITY.md),别开公开 issue。
249
+
250
+ ## 语音输入(可选)
251
+
252
+ 点麦克风可把语音听写进输入框,底层用[讯飞](https://www.xfyun.cn/),**需要你自己填 key 才会开启**——
253
+ 在讯飞控制台开通「语音听写 (IAT)」应用,往配置文件里加一段
254
+ `"xfyun": { "appId": "…", "apiKey": "…", "apiSecret": "…" }`(见《配置》)。密钥只留在服务端,
255
+ 手机只拿到一个短时效的签名地址。没配 key 时,麦克风按钮直接不显示。
256
+
257
+ ## 推送通知(可选)
258
+
259
+ 「pane 需要你 / 跑完时推手机」这条**需要你自己配一对 VAPID 密钥**(标准 Web Push 凭证)才开启。
260
+ 用自带的 `web-push` 生成:
261
+
262
+ ```bash
263
+ npx web-push generate-vapid-keys
264
+ ```
265
+
266
+ 往配置文件里加一段 `"vapid": { "public": "…", "private": "…", "subject": "mailto:you@example.com" }`
267
+ (见《配置》)。两者齐全时 `/api/push/vapid` 才发公钥、手机才能订阅;缺则该接口返回 503、铃铛隐藏。
268
+ 推送还依赖 Claude Code hook(pane 得先有状态可推)——见上面《Claude Code 通知》。
269
+
270
+ ## 端口动态预览(进阶)
271
+
272
+ 设置 `--preview-domain`(或配置里的 `"previewDomain"`)可把本机其他开发服务(如 `:3000` 上的
273
+ Vite)透给手机,每个走自己的子域名。这需要一个指向网关的**通配子域名**(`*.your.domain`),所以
274
+ 只在自建路上可用,快速隧道用不了。`handmux setup` **不会**替你接好这一步——自己配好再把
275
+ `previewDomain` 指过去。
276
+
277
+ **证书层级(Cloudflare)**:浏览器是用 HTTPS 访问预览的,通配子域名得有证书。Cloudflare 免费的
278
+ Universal SSL 只覆盖**一级**——`*.example.com` 可用(预览走 `<端口>.example.com`),但更深的
279
+ `*.preview.example.com` 需要**高级证书(Advanced Certificate Manager)**。所以要么把预览保持在
280
+ 一级,要么开 ACM。(走 ssh / 自有边缘时由你自己提供通配证书,如 Let's Encrypt 的
281
+ `*.preview.your.domain`。)
282
+
283
+ ## 许可证
284
+
285
+ MIT