JORAN POCKET · v0.2

手机指挥 Mac 终端的
Claude Code 控制器。

在 Mac 终端里跑 pocket attach,iMessage 把链接推到 iPhone, Safari 打开就能看自己的终端、给 Claude 发指令。 终端字节走 WebRTC P2P 直连——没有任何中间服务器看到你的内容。

零账号

终端用户不注册任何账号。下二进制,开终端,扫码。

On-demand 链接

默认无链接。pocket attach 才生效,pocket sleep 立即吊销。

端到端加密

WebRTC DataChannel 自带 DTLS 加密;信令服务器看不到任何终端字节。

VPN 兼容

纯 P2P,不占 iOS VPN 槽位。Astrill / 公司 VPN 可以并行运行。

三步上手 (终端用户)

1 一键安装并配置

在 Mac Terminal 里粘贴下面这条命令。脚本会引导你登录 你自己的 Cloudflare、部署到 你自己的 免费 Worker + Pages、选择 iMessage 接收方。
作者看不到你的会话;你的免费额度只服务你一人。

curl -fsSL https://raw.githubusercontent.com/cocohahaha/joran-pocket/main/scripts/setup.sh | bash

仅 macOS。Apple Silicon + Intel 都支持。需要 Cloudflare 免费账号 (无信用卡)。

2 用的时候

任意 Mac Terminal 窗口里:

pocket attach

helper 立即激活、注册新链接、推 iMessage 到你 iPhone、当前 Terminal 自动接管 tmux。屏幕在 attach 期间不熄屏。

3 不用了就 sleep

pocket sleep

链接立即失效。Mac 恢复正常熄屏。下次再 attach 会推送新链接。

整体技术架构

┌──────────────────────┐                            ┌──────────────────────┐
│   iPhone Safari      │                            │   Mac (你的电脑)     │
│   (PWA Guest)        │                            │                      │
│                      │                            │  ┌────────────────┐  │
│  React + xterm.js    │                            │  │ Terminal.app   │  │
│  + WebRTC client     │                            │  │  pocket attach │  │
│                      │                            │  └───────┬────────┘  │
│         ▲            │                            │          │           │
│         │            │                            │          ▼           │
│         │ DataChannel│                            │  ┌────────────────┐  │
│         │ (DTLS/SCTP)│   1) 信令握手 (短)         │  │ tmux server    │  │
│         │            │ ◄──────────────────────►   │  │ (-L pocket)    │  │
│         │            │                            │  │   pocket sess. │  │
│         │ 2) 直连 P2P│                            │  │   ↑↑ pane      │  │
│         │ (终端字节) │                            │  └────┬─┬─────────┘  │
│         └────────────┼────────────────────────────┼───────┘ │            │
│                      │                            │         ▼ pty        │
│                      │                            │  ┌────────────────┐  │
└──────────────────────┘                            │  │ pocket helper  │  │
                                  ▲                 │  │ (Go binary)    │  │
                                  │                 │  │  - WebRTC peer │  │
                                  │ /api/register   │  │  - tmux client │  │
                                  │ /api/pair/X/ws  │  │  - caffeinate  │  │
              ┌───────────────────┴─────────────┐   │  └────────┬───────┘  │
              │  Cloudflare Pages + Workers     │   │           │          │
              │  (joran-pocket.pages.dev)       │   │           ▼          │
              │  ┌──────────────────────────┐   │   │  ~/.pocket/active    │
              │  │ Pages Functions          │   │   │  ~/.pocket/url.txt   │
              │  │   POST /api/register     │   │   │  ~/.pocket/code.txt  │
              │  │   GET  /api/pair/X/ws    │   │   │  (state markers)     │
              │  └──────────┬───────────────┘   │   └──────────────────────┘
              │             ▼                    │
              │  ┌──────────────────────────┐   │
              │  │ PairingSession (DO)      │   │
              │  │  - host slot + guest slot │   │
              │  │  - relay offer/answer/ICE │   │
              │  └──────────────────────────┘   │
              └──────────────────────────────────┘
关键:终端字节 (PTY 数据) 全程走 P2P DTLS DataChannel经过 Cloudflare。 Cloudflare 只在握手前 ~10 秒中转 SDP/ICE (~2KB JSON 元数据),不存任何会话字节。

激活生命周期 (on-demand 链接)

  Mac 开机 → launchd → helper boot
                            │
                            │ 删 active / url.txt / code.txt
                            ▼
                       ┌─────────┐
                       │  IDLE   │  ✗ 无 DO 注册
                       │         │  ✗ 无链接 (任何旧 URL 都死)
                       │         │  ✗ 无 caffeinate
                       └────┬────┘
                            │
            user: pocket attach
                            │ (touch ~/.pocket/active)
                            │ helper 250ms 内检测到
                            ▼
                       ┌─────────┐
                       │ ACTIVE  │  ✓ 注册新 6 位码
                       │         │  ✓ 写 url.txt
                       │         │  ✓ 启 caffeinate (不熄屏)
                       │         │  ✓ iMessage 推链接
                       │         │  ✓ Pair loop 跑 WebRTC
                       └────┬────┘
                            │
            user: pocket sleep
                            │ (rm ~/.pocket/active)
                            │ helper 2s 内检测到
                            ▼
                       ┌─────────┐
                       │  IDLE   │  ← 链接立即失效
                       └─────────┘

核心组件

实现职责
Helper (Mac) Go + pion/webrtc + creack/pty tmux 客户端 + PTY 桥;IDLE/ACTIVE 状态机;caffeinate;iMessage 推送 (osascript)
信令 Cloudflare Pages Functions + Durable Object SDP/ICE 中转; per-code DO; 60s GC alarm; 不持有任何终端字节
PWA (手机) React + Vite + xterm.js WebRTC peer (guest); DOM renderer (iOS Safari 兼容); unicode-11 (CJK 列宽); 三步强制 paint
持久化 ~/.pocket/* (本地文件) active / url.txt / code.txt / imessage-to.txt / tmux.conf
开机自启 launchd LaunchAgent com.joranpocket.helper.plist; KeepAlive=true; UTF-8 locale 注入

一次手机连接的完整数据流

phone Safari            Cloudflare DO              Mac helper
────────────            ─────────────              ──────────

打开 /p/CODE
       │
       │  WS /api/pair/CODE/ws?role=guest
       ├──────────────────────────────►│
       │ ◄────hello role=guest──────────┤◄──peer-joined role=host───────┤  (host slot 已被 helper 占)
       │                               ├──peer-joined role=guest──►│
       │                               │                            │
       │                               │ ◄──────offer SDP───────────│
       │ ◄────offer SDP─────────────────┤                            │
       │ acceptOffer + createAnswer    │                            │
       ├──── answer SDP ──────────────►│ ────answer SDP───────────►├──ICE trickle────────────────►│ ──ICE trickle────────────►│
       │ ◄──ICE trickle─────────────────┤ ◄──ICE trickle────────────│
       │
       ╔════════════════════════════════════════════════════════╗
       ║   P2P DTLS 直连 — Cloudflare 不再参与                  ║
       ╠════════════════════════════════════════════════════════╣
       ║   pty DC: 终端字节双向 (Mac 渲染 + phone 输入)         ║
       ║   sidechannel DC: pane_size / windows / claude_state   ║
       ╚════════════════════════════════════════════════════════╝

关键技术决策

渲染端 (PWA)

决策原因
DOM renderer (非 canvas/WebGL)iOS Safari 在非整数 DPR 下 canvas 字宽算错
@xterm/addon-unicode11默认 v6 把中文当 1 列, 跟 tmux (2 列) 错位
.xterm-rows white-space:prenowrap 会合并连续空格 → 终端缩进消失
.xterm-viewport overflow:visible让外层 div 接管 scroll, iOS touch 不被吃掉
will-change + contain 提示iOS Safari 单独 compositor 层, 避免 lazy-paint
三步强制 paint (refresh + RAF×2 + transform)resize 后立即可见, 不需要划屏

连接端 (helper)

决策原因
默认 IDLE 不注册链接 on-demand, 泄露窗口窄
LANG=en_US.UTF-8 注入LaunchAgent 默认 C locale → tmux 把 \t 替换成 _
sticky code 失败 5 次自动旋转Cloudflare DO host 槽偶尔 ghost 占用
refresh-client per TTYresize 后强制每个 client 重绘 → 投屏即时
pane_size 200ms 采样比 500ms 响应快 2.5x, 几乎察觉不到延迟

信令服务 (Cloudflare)

决策原因
Pages Functions 而非独立 WorkerPWA 静态 + 信令 API 同源, 省一个 deploy + CORS 麻烦
Durable Object per code天然 host/guest slot 状态隔离
不存终端字节仅中转 SDP/ICE (~2KB), 隐私零保留

子命令清单

命令作用
pocket install装 LaunchAgent (开机自启 helper, 但默认 IDLE)
pocket uninstall卸 LaunchAgent
pocket attach激活 helper + 注册 URL + iMessage + tmux 接管
pocket sleep立即吊销链接 (helper 转回 IDLE)
pocket url输出当前 URL (IDLE 时无)
pocket status查 LaunchAgent / 进程 / 状态 / URL

自己部署 (开发者)

git clone https://github.com/cocohahaha/joran-pocket
cd joran-pocket

# 一键引导部署 (Cloudflare Worker + Pages + iMessage 配置)
bash scripts/setup.sh

# 或手动:
# 1) 信令 Worker
cd signaling && npm i && npx wrangler deploy
# 2) PWA 静态站
cd ../pwa && npm i && npm run build && npx wrangler pages deploy dist
# 3) helper 二进制
cd ../helper && go build -o pocket .
sudo ln -s "$(pwd)/pocket" /opt/homebrew/bin/pocket
pocket install

技术栈

Go 1.21+ pion/webrtc v4 creack/pty tmux React 18 Vite @xterm/xterm Cloudflare Pages Cloudflare Workers Durable Objects launchd caffeinate osascript (iMessage) MIT