handmux 0.9.1 → 0.11.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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ handmux is more than a terminal on your phone. It puts the *same* live **tmux**
16
16
  <em>Real phone browser, real panes — say what you need and Claude Code writes it, then tap a filename to preview (left); a push pings you when needed, and you review the git repo &amp; each agent's usage (right).</em>
17
17
  </p>
18
18
 
19
- **[📖 Docs](https://handmux.com/docs.html)** · **[📝 Changelog](CHANGELOG.md)** · **[📦 npm](https://www.npmjs.com/package/handmux)**
19
+ **[📖 Docs](https://handmux.com/docs)** · **[📝 Changelog](CHANGELOG.md)** · **[📦 npm](https://www.npmjs.com/package/handmux)**
20
20
 
21
21
  ## Quick start · about a minute
22
22
 
@@ -35,7 +35,7 @@ Want to reach it from **anywhere**? Add one flag for a free public HTTPS link:
35
35
  handmux start --tunnel cloudflare # instant public URL (cloudflared auto-installed)
36
36
  ```
37
37
 
38
- > Tunnel types, self-hosting, Windows/WSL2, and the full command & flag reference → see the **[docs](https://handmux.com/docs.html)**.
38
+ > Tunnel types, self-hosting, Windows/WSL2, and the full command & flag reference → see the **[docs](https://handmux.com/docs)**.
39
39
 
40
40
  ## Why handmux
41
41
 
@@ -59,19 +59,19 @@ handmux start --tunnel cloudflare # instant public URL (cloudflared auto-insta
59
59
 
60
60
  ## Networking: one decision
61
61
 
62
- LAN-only by default, nothing exposed. To reach it from outside, just ask: **does your machine already have a public address?**
62
+ No tunnel by default — the phone connects **straight to your own computer**, nothing exposed and no middleman. To reach it from outside, just ask: **does your machine already have a public address?**
63
63
 
64
64
  - **Yes** (cloud box / public IP / forwarded port) — no tunnel needed, connect directly; fastest and most private.
65
- - **No** — open a tunnel: `cloudflare` (zero-config, up in seconds, but its public nodes can be slow or unreliable in some regions) · `cloudflare-named` (your own domain, steadier) · `ssh` self-hosted (through your own server the pick where Cloudflare is unreliable, e.g. mainland China).
65
+ - **No** — open a tunnel. Each runs on **your own free third-party account** — handmux just wires it up and operates no relay of its own: `cloudflare` (zero-config, up in seconds, but its public edge can be slow or unreliable in some regions) · `cloudflare-named` (your own domain, steadier) · `natapp` / `cpolar` (domestic providers that stay reachable inside mainland China) · `ssh` self-hosted (through your own server).
66
66
 
67
- > Tunnel config, server-side reverse proxy, autostart, voice/push credentials, and port previews → see the **[docs](https://handmux.com/docs.html)**.
67
+ > Tunnel config, server-side reverse proxy, autostart, voice/push credentials, and port previews → see the **[docs](https://handmux.com/docs)**.
68
68
 
69
69
  ## Requirements
70
70
 
71
- Your computer needs **Node ≥ 18** and **tmux ≥ 3.0**; the phone just needs a browser. On **Windows**, run it inside **WSL2** (a real Linux kernel + real tmux) — see the [docs](https://handmux.com/docs.html#windows).
71
+ Your computer needs **Node ≥ 18** and **tmux ≥ 3.0**; the phone just needs a browser. On **Windows**, run it inside **WSL2** (a real Linux kernel + real tmux) — see the [docs](https://handmux.com/docs#windows).
72
72
 
73
73
  ## More
74
74
 
75
- **[📖 Docs](https://handmux.com/docs.html)** · **[📝 Changelog](CHANGELOG.md)** · **[🔒 Security](SECURITY.md)** · License **AGPL-3.0**
75
+ **[📖 Docs](https://handmux.com/docs)** · **[📝 Changelog](CHANGELOG.md)** · **[🔒 Security](SECURITY.md)** · License **AGPL-3.0**
76
76
 
77
77
  Found a security issue? Please report it privately (see [SECURITY.md](SECURITY.md)), not via a public issue.
package/README.zh-CN.md CHANGED
@@ -16,7 +16,7 @@ handmux 不只是把终端搬上手机。它把你电脑上**正跑着的 tmux
16
16
  <em>真实手机浏览器、真实 pane——左:说出需求,Claude Code 直接写好,点文件名即可预览;右:需要你时推送提醒,查看 git 仓库与各 agent 用量。</em>
17
17
  </p>
18
18
 
19
- **[📖 文档](https://handmux.com/docs.html)** · **[📝 更新日志](CHANGELOG.md)** · **[📦 npm](https://www.npmjs.com/package/handmux)**
19
+ **[📖 文档](https://handmux.com/docs)** · **[📝 更新日志](CHANGELOG.md)** · **[📦 npm](https://www.npmjs.com/package/handmux)**
20
20
 
21
21
  ## 快速上手 · 约一分钟
22
22
 
@@ -35,7 +35,7 @@ handmux start # 跑起来 —— 仅本机 / 同 wifi,不对外暴露
35
35
  handmux start --tunnel cloudflare # 即时公网地址(自动装 cloudflared)
36
36
  ```
37
37
 
38
- > 隧道类型、自建、Windows/WSL2、完整命令与参数 → 见 **[文档](https://handmux.com/docs.html)**。
38
+ > 隧道类型、自建、Windows/WSL2、完整命令与参数 → 见 **[文档](https://handmux.com/docs)**。
39
39
 
40
40
  ## 为什么是 handmux
41
41
 
@@ -59,19 +59,19 @@ handmux start --tunnel cloudflare # 即时公网地址(自动装 cloudflared)
59
59
 
60
60
  ## 联网:一句话决策
61
61
 
62
- 默认只在局域网、什么都不暴露。想从外面连,只问一句:**电脑有没有公网地址?**
62
+ 默认不开隧道——手机**直连你自己的电脑**,什么都不暴露、也没有中间人。想从外面连,只问一句:**电脑有没有公网地址?**
63
63
 
64
64
  - **有**(云主机 / 公网 IP / 已端口转发)—— 不用隧道,直接连,最快也最私密。
65
- - **没有** —— 开一条隧道:`cloudflare`(零配置秒通,但公共节点在国内常不稳定)· `cloudflare-named`(你的域名,更稳)· `ssh` 自建(接你自己的服务器,**国内网络首选**)。
65
+ - **没有** —— 开一条隧道。每条都跑在**你自己的免费第三方账号**上,handmux 只负责接通、自身不设中转:`cloudflare`(零配置秒通,但公共边缘在国内常不稳)· `cloudflare-named`(你的域名,更稳)· `natapp` / `cpolar`(国内厂商,大陆境内可达)· `ssh` 自建(接你自己的服务器)。
66
66
 
67
- > 隧道配置、服务端反向代理、开机自启、语音 / 推送凭证、端口预览等细节 → 见 **[文档](https://handmux.com/docs.html)**。
67
+ > 隧道配置、服务端反向代理、开机自启、语音 / 推送凭证、端口预览等细节 → 见 **[文档](https://handmux.com/docs)**。
68
68
 
69
69
  ## 环境要求
70
70
 
71
- 电脑需 **Node ≥ 18** 与 **tmux ≥ 3.0**;手机只要浏览器。**Windows** 请装进 **WSL2**(真 Linux 内核 + 真 tmux)——见 [文档](https://handmux.com/docs.html#windows)。
71
+ 电脑需 **Node ≥ 18** 与 **tmux ≥ 3.0**;手机只要浏览器。**Windows** 请装进 **WSL2**(真 Linux 内核 + 真 tmux)——见 [文档](https://handmux.com/docs#windows)。
72
72
 
73
73
  ## 更多
74
74
 
75
- **[📖 文档](https://handmux.com/docs.html)** · **[📝 更新日志](CHANGELOG.md)** · **[🔒 安全](SECURITY.md)** · 许可证 **AGPL-3.0**
75
+ **[📖 文档](https://handmux.com/docs)** · **[📝 更新日志](CHANGELOG.md)** · **[🔒 安全](SECURITY.md)** · 许可证 **AGPL-3.0**
76
76
 
77
77
  发现安全问题请私下报告(见 [SECURITY.md](SECURITY.md)),别开公开 issue。
package/bin/handmux.js CHANGED
@@ -7,8 +7,8 @@
7
7
  // let you try variations for one run (e.g. --tunnel cloudflare).
8
8
  // handmux setup — the one place to configure persistently. Interactive; writes ~/.handmux/config.json
9
9
  // (name, tunnel, push, voice). Re-run it to change anything.
10
- // `start` reads that file; with no file it uses defaults. Precedence: flag > file > default a flag
11
- // overrides one value for one run and never persists. Advanced: `--config PATH` (a different file, for
10
+ // `start` reads that file; with no file it uses defaults. Precedence: flag > file > env (HANDMUX_*) >
11
+ // default — a flag overrides one value for one run and never persists. Advanced: `--config PATH` (a different file, for
12
12
  // dev / multiple configs), `handmux config` (show what's in effect and where each value came from).
13
13
  //
14
14
  // Tunnels: `none` (LAN only, nothing exposed) · `cloudflare` (instant random https URL) ·
@@ -26,6 +26,7 @@ import { renderCompactQr } from '../src/cli/qr.js';
26
26
  import { supervise, bareUrl, publicUrlWithToken } from '../src/cli/supervisor.js';
27
27
  import { resolveCloudflared } from '../src/cli/cloudflared.js';
28
28
  import { resolveTunlite, checkSshAuth } from '../src/cli/tunlite.js';
29
+ import { resolveNatapp, resolveCpolar } from '../src/cli/tunnelClients.js';
29
30
  import { installService, uninstallService } from '../src/cli/service.js';
30
31
  import { checkTmux, MIN_TMUX, tmuxInstallHint } from '../src/cli/tmuxVersion.js';
31
32
  import { readState, clearState, isAlive, pocketHome, logPath, configPath, claudeStatePath } from '../src/cli/state.js';
@@ -115,9 +116,24 @@ async function preflightSsh(cfg) {
115
116
  throw new Error(t('ssh.notSetup', { bin: cfg.tunliteBin, host: cfg.sshHost }));
116
117
  }
117
118
 
119
+ // natapp/cpolar preflight: resolve the client binary (cpolar auto-downloads; natapp must be pre-installed),
120
+ // and for cpolar seed the authtoken into its own config so the detached `cpolar http` authenticates. Throws
121
+ // a friendly message the caller prints.
122
+ async function preflightNgrok(cfg) {
123
+ if (cfg.tunnel === 'natapp') {
124
+ cfg.natappBin = resolveNatapp(HOME);
125
+ } else {
126
+ cfg.cpolarBin = await resolveCpolar(HOME);
127
+ if (spawnSync(cfg.cpolarBin, ['authtoken', cfg.authtoken], { stdio: 'ignore' }).status !== 0) {
128
+ throw new Error(t('client.cpolarAuthFail'));
129
+ }
130
+ }
131
+ }
132
+
118
133
  async function main() {
119
134
  switch (command) {
120
135
  case 'start': return start();
136
+ case 'open': return openCmd();
121
137
  case 'stop': stop(); return;
122
138
  case 'restart': { stop(); await sleep(600); return start(); }
123
139
  case 'status': return status();
@@ -227,6 +243,10 @@ async function start() {
227
243
  try { await preflightSsh(cfg); }
228
244
  catch (e) { console.error(t('err.generic', { msg: e.message })); process.exit(1); }
229
245
  }
246
+ if (cfg.tunnel === 'natapp' || cfg.tunnel === 'cpolar') {
247
+ try { await preflightNgrok(cfg); }
248
+ catch (e) { console.error(t('err.generic', { msg: e.message })); process.exit(1); }
249
+ }
230
250
 
231
251
  if (cfg.foreground) {
232
252
  supervise(cfg, { home: HOME });
@@ -304,6 +324,10 @@ async function serviceInstall() {
304
324
  console.error(t('err.generic', { msg: t('ssh.notSetup', { bin: cfg.tunliteBin, host: cfg.sshHost }) })); process.exit(1);
305
325
  }
306
326
  }
327
+ if (cfg.tunnel === 'natapp' || cfg.tunnel === 'cpolar') {
328
+ try { await preflightNgrok(cfg); }
329
+ catch (e) { console.error(t('err.generic', { msg: e.message })); process.exit(1); }
330
+ }
307
331
  const payload = Buffer.from(JSON.stringify(cfg)).toString('base64');
308
332
  const args = [process.execPath, SELF, '__supervise', '--payload', payload];
309
333
  try { installService(args, { home: HOME }); }
@@ -313,8 +337,14 @@ async function serviceInstall() {
313
337
 
314
338
  async function setupCmd() {
315
339
  const target = flags.config ? path.resolve(flags.config) : configPath(HOME);
316
- const cfg = await runSetup({ home: HOME, target });
317
- if (!cfg) { process.exit(2); }
340
+ // Is an instance already up? Then the run-action reads "Save & restart" and applying means a real restart
341
+ // (a running supervisor won't pick up the new config on its own). Captured before the interactive setup so
342
+ // the label and the action agree; stop() below is a safe no-op if it died meanwhile.
343
+ const st = readState(HOME);
344
+ const running = !!(st && isAlive(st.supervisorPid));
345
+ const res = await runSetup({ home: HOME, target, running });
346
+ if (!res) { process.exit(2); }
347
+ const { cfg, start: doStart } = res; // hub's "save & start/restart" carries the intent — no separate confirm
318
348
  // Offer to enable the inbox hooks when an agent is present but not yet wired (Claude 'absent', or Codex
319
349
  // 'absent'). installAgentHooks() then wires every present agent (idempotent for any already installed).
320
350
  const offerHooks = hooksStatus(HOME) === 'absent' || codexHooksStatus(HOME) === 'absent';
@@ -322,8 +352,12 @@ async function setupCmd() {
322
352
  installAgentHooks();
323
353
  }
324
354
  await maybeOfferStatusLine();
325
- if (await confirm(t('setup.confirmStart'))) { Object.assign(flags, cfg); return start(); }
326
- console.log(t('setup.later'));
355
+ if (doStart) {
356
+ Object.assign(flags, cfg);
357
+ if (running) { stop(); await sleep(600); } // restart into the new config
358
+ return start();
359
+ }
360
+ console.log(t(running ? 'setup.laterRestart' : 'setup.later'));
327
361
  }
328
362
 
329
363
  // Offer to enable the Claude statusLine usage capturer — it feeds the phone Usage page's 5h/weekly bars
@@ -469,7 +503,25 @@ async function maybeQr(url, st) {
469
503
  } catch { /* no qrcode-terminal — URL alone is fine */ }
470
504
  }
471
505
 
506
+ // `open` is deliberately DECOUPLED from the server lifecycle: it never starts/stops anything. It's the
507
+ // desk-side quick attach — `handmux open foo` attaches session foo (creating it if missing), including
508
+ // sessions that were created from the phone. Inside tmux it refuses (don't nest tmux in tmux).
509
+ function openCmd() {
510
+ const name = process.argv[3];
511
+ if (!name || name.startsWith('-')) { console.error(t('open.usage')); process.exit(2); }
512
+ if (process.env.TMUX) { console.error(t('open.insideTmux')); process.exit(1); }
513
+ const tmux = checkTmux();
514
+ if (!tmux.present) {
515
+ console.error(t('tmux.notFound'));
516
+ console.error(t('tmux.install', { hint: tmuxInstallHint() }));
517
+ process.exit(1);
518
+ }
519
+ const r = spawnSync('tmux', ['new-session', '-A', '-s', name], { stdio: 'inherit' });
520
+ process.exit(r.status ?? 0);
521
+ }
522
+
472
523
  function help() {
524
+ if (process.argv[3] === 'flags') { console.log(t('help.flags')); return; }
473
525
  console.log(t('help.body'));
474
526
  }
475
527
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.9.1",
4
- "description": "A mobile vibe-coding cockpit — built on tmux: drive your live session, Claude Code & Codex from your phone.",
3
+ "version": "0.11.0",
4
+ "description": "A mobile vibe-coding cockpit — built on tmux: drive your live session, Claude Code / Codex — anything a terminal can run — from your phone.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
7
7
  "author": "yuanyuanzijin",
@@ -13,7 +13,14 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/handmux/handmux/issues"
15
15
  },
16
- "keywords": ["tmux", "mobile", "terminal", "claude-code", "remote", "pwa"],
16
+ "keywords": [
17
+ "tmux",
18
+ "mobile",
19
+ "terminal",
20
+ "claude-code",
21
+ "remote",
22
+ "pwa"
23
+ ],
17
24
  "engines": {
18
25
  "node": ">=18"
19
26
  },
@@ -36,6 +43,7 @@
36
43
  "test": "vitest run"
37
44
  },
38
45
  "dependencies": {
46
+ "@clack/prompts": "^1.7.0",
39
47
  "busboy": "^1.6.0",
40
48
  "express": "^4.19.2",
41
49
  "tunlite": "^0.10.0",
@@ -47,5 +55,15 @@
47
55
  "devDependencies": {
48
56
  "supertest": "^7.0.0",
49
57
  "vitest": "^2.0.0"
50
- }
58
+ },
59
+ "whatsNew": [
60
+ {"version":"0.11.0","date":"2026-07-08","zh":"国内可用隧道 natapp/cpolar · 配置向导重做","en":"China-usable tunnels · setup redesigned"},
61
+ {"version":"0.10.0","date":"2026-07-06","zh":"升级前预览新功能 · 电脑 handmux open 接管","en":"Preview an update before installing · handmux open"},
62
+ {"version":"0.9.1","date":"2026-07-06","zh":"「添加到主屏」引导","en":"\"Add to Home Screen\" coach"},
63
+ {"version":"0.9.0","date":"2026-07-05","zh":"聊天输入框多行重做","en":"Multi-line chat composer redone"},
64
+ {"version":"0.8.0","date":"2026-07-05","zh":"快捷栏可自定义 · 上传体验重做","en":"Customizable quick-bars · upload redone"},
65
+ {"version":"0.7.0","date":"2026-07-03","zh":"新增「用量」页","en":"New \"Usage\" page"},
66
+ {"version":"0.6.0","date":"2026-07-02","zh":"新增 Codex 支持","en":"Codex support added"},
67
+ {"version":"0.5.0","date":"2026-06-28","zh":"首个公开版 · 多语言界面","en":"First public release · multi-language UI"}
68
+ ]
51
69
  }