npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tmux-next

v0.8.0

Published

A phone-friendly web client for tmux sessions, built on tmux control mode

Readme

tmux-next

CI npm license

在手机上盯着跑在 tmux 里的 Claude Code。

一个自托管的 tmux web 客户端。列出本机所有 session、看到每个的最后几行输出、点进去接着聊——锁屏、进地铁、换 Wi-Fi 回来,画面自动重建。

┌──────────────────────────┐      ┌──────────────────────────┐
│  ● PROJ-1042      2 分钟 │      │ ‹ 会话  PROJ-1042   已连接│
│    要哪个说一声。         │      ├──────────────────────────┤
│    ✻ Cogitated for 1m21s │      │ ✻ Cogitated for 1m 21s   │
│    ❯ rebase 到最新 master│      │                          │
│                      ⋯  │      │ ❯ ▊                      │
├──────────────────────────┤      │                          │
│    billing-ci     1 小时 │      ├──────────────────────────┤
│    ✓ 12 passed           │      │ Esc Tab ⇧Tab Ctrl ⌨ ↑↓←→ │
└──────────────────────────┘      └──────────────────────────┘
         会话列表                          终端

左边那个 是「它在等你回复」——Claude Code 打完一轮会输出 ✻ Cogitated for …,列表据此把等你的 session 排到最前面。


它能做什么

| | | |---|---| | 会话列表 | 最后几行输出预览、「在等你」状态点、还没发出去的输入、最后活跃时间 | | 新建会话 | 点选目录(可一路往下钻)、可选会话名、可选跳过权限确认 | | 终端 | 宽度自适应窗口、软键盘工具条(Esc / Tab / ⇧Tab / Ctrl / 方向键 / ^C / ⏎)、拖动滚动全屏程序 | | 断线重连 | 不缓冲、不重放——重连时从 tmux 重新抓一次完整画面 | | 锁屏通知 | 会话结束、一轮聊完在等你、Claude 要你确认时,推到手机锁屏(Web Push,需装 hook + 订阅) | | 中文输入 | 绕过了 xterm.js 5.5.0 吞掉中文标点的输入守卫 |

跑起来

前置条件

| | | |---|---| | tmux 3.2+ | 每次 resize 走 refresh-client -C <列>,<行>,逗号写法是 3.2 引入的。更早的版本不认这条命令,尺寸会静默不生效——所以启动时会检查版本并拒绝运行 | | Bun 1.0+ | 直接跑 TypeScript,没有构建步骤 | | macOS / Linux | 依赖 tmux 与 POSIX shell |

装与跑

Bun 直接跑(不支持 node —— 入口是未编译的 TypeScript,没有构建步骤):

bunx tmux-next

或者克隆下来改:

git clone https://github.com/niletry/tmux-next.git
cd tmux-next
bun install          # 两个包,都只是前端用的 xterm.js
bun run src/index.ts

两种方式都打开 http://127.0.0.1:7682/,会列出你本机所有 tmux session。

tmux-next [options]

  -p, --port <n>     监听端口(默认 7682)
      --host <addr>  绑定地址(默认 127.0.0.1)
  -h, --help         显示帮助
  -v, --version      显示版本

从手机访问:必须自己加一层

这个服务本身没有任何认证,所以默认只绑 loopback。它假设前面有一个反向代理负责 TLS 和身份验证。

直接 --host 0.0.0.0 暴露到网络,等于把一个无需密码的 shell 交给任何能连到这个端口的人——它可以附着到你所有 tmux session 并执行命令。程序启动时会为此打印警告,但不会阻止你。

反代需要处理两件事:

  1. 普通请求做认证(Basic Auth、OIDC,随你)
  2. WebSocket 单独处理 —— 浏览器发起 WS 握手时不会带 Basic Auth 头。可行的做法是普通请求认证成功时种一个 cookie,WS 路径改为校验该 cookie

docs/deploy.md 里有一份可用的 Caddy 配置,包含上面这个 cookie 方案。

开机自启

仓库里不带 service 文件——路径和用户都因机器而异。macOS 用 launchd plist,Linux 用 systemd unit,命令都是 bun run /path/to/tmux-next/src/index.ts。注意 launchd 给的环境极其精简,tmux 未必在 PATH 里,需要显式指定。

会话恢复(tmux 重启后找回 Claude 会话)

tmux server 一死(重启、崩溃、tmux kill-server),里面跑着的 Claude 会话就没了。装上配套的 SessionStart hook,tmux-next 就能在事后把它们恢复回来:

bunx tmux-next hook

它把一个 hook 脚本装进 ~/.claude/hooks/,并在 ~/.claude/settings.json 里注册(改动前备份、幂等、不覆盖你已有配置)。之后每个在 tmux 里新启动的 Claude,会把 {会话名, 会话 id, cwd} 记到 ~/.tmux-next/sessions/(磁盘上,扛得过 tmux 死——Claude 的对话记录本就在磁盘上)。

tmux 重启后打开列表,顶部会出现「N 个上次的会话可恢复」;点一下,tmux-next 用 tmux new-session -c <cwd> 重建会话并 claude --resume <id>,对话就回来了。需要机器上有 jq。只对之后新起的 Claude 生效;用 tmux-next「结束会话」主动杀掉的不会被提示恢复。

锁屏通知(会话结束/在等你/需要确认时提醒)

把任务丢给 tmux 里的 Claude 然后去忙别的,回来才发现它早停了——通知补的就是这段。走标准 Web Push,手机锁屏、app 关着也能收到系统通知,点通知直接跳到那个会话。

装通知 hook 用的是同一条命令(它一次装好恢复用的和通知用的全部 hook):

bunx tmux-next hook       # 或从克隆的仓库:bun run src/index.ts hook

它把 hook 脚本装进 ~/.claude/hooks/,在 ~/.claude/settings.json 注册四个事件(SessionStart 用于恢复;Stop/SessionEnd/Notification 用于通知),改动前备份、幂等、不覆盖你已有配置。之后新起的 tmux 里的 Claude 会话,在这三个时机会把事件 POST 给本机的 tmux-next。需要 jqcurl

然后在 web 界面订阅一次:列表页右上角点铃铛,允许通知即可(铃铛变亮=已订阅)。几个前提:

  • 必须 HTTPS(或 localhost)——浏览器只在安全上下文里给通知权限。经反向代理访问天然满足;局域网明文 http://… 不行。
  • iPhone 需先把页面「添加到主屏幕」当 PWA,从那个图标进去再订阅(iOS 只对已安装 PWA 推送,需 iOS 16.4+)。
  • VAPID 密钥首次自动生成存 ~/.tmux-next/vapid.json,订阅存 ~/.tmux-next/push-subscriptions/,无需任何第三方账号。

同一会话 30 秒内最多推一条(会话结束不受限)。触发通知的 /api/notify 只接受本机来源,别人无法伪造。

开发

bun test    # 201 个测试,会真的起 tmux session,自动清理

运行时只有 xterm.js,没有前端框架、没有构建步骤——public/ 里的文件浏览器直接加载。

它是怎么工作的

flowchart LR
    B["浏览器<br/>xterm.js"] <-->|WebSocket| S["Bun 服务<br/>127.0.0.1:7682"]
    S <-->|"tmux -C attach<br/>control mode"| T["tmux server"]
    T --- P1["你的 session<br/>PROJ-1042"]
    T --- P2["你的 session<br/>billing-ci"]
    S -.->|"建 web-uuid<br/>grouped session"| T

核心选择是用 control mode,而不是起一个 PTY 跑 tmux attach

control mode 把 tmux 变成一个讲结构化协议的进程:输出按 pane 分发(%output %3 …),命令有请求/响应语义。所以这个 app 能只渲染一个 pane,不带 tmux 自己的分屏边框和 status bar——浏览器里看到的就是程序本身的画面。

每个浏览器连接建一个 web-<uuid> grouped session 当作可销毁的挂载点,断开即销毁。这样它是以一个正常 tmux 客户端的身份参与的,尺寸协商、window 归属这些都交给 tmux 自己仲裁,不必另造一套。

几个值得一提的设计

尺寸是共享的,这是 tmux 的事实而非缺陷。 window 尺寸是 window 的属性,同一个 window 无法对两个客户端渲染成两种宽度。浏览器连上时该 window 会跟随浏览器;断开时调 resize-window -A 把尺寸还给剩下的客户端。

宽度自适应,一条规则覆盖手机和桌面。 先算「让 80 列铺满窗口需要多大字号」;没超过上限就用它——手机因此永远是 80 列,只是字号跟着屏幕缩。超过上限(约 576px 宽以上)字号就钉在上限,多出来的宽度换成列数。没有设备嗅探,切换点是算出来的。

拖动滚动的是程序,不是滚动缓冲区。 tmux 原地重绘整个屏幕,从不让行滚出顶部,所以 xterm 的 scrollback 永远是空的。手势被翻译成合成的 WheelEvent 交给 xterm,由它按程序协商好的鼠标协议编码——程序不理会鼠标时不会有任何字节被塞进去,而这个「零输出」正好被用作信号,退化成发 PgUp/PgDn。

命令是常量。 启动 Claude Code 的字符串会经过 sh -c,所以任何来自请求的东西都不允许拼进去——目录作为独立 argv 通过 -c 传递,「跳过权限确认」是在两个固定字符串之间二选一,而不是拼接。

浏览目录没有白名单。 早期版本把浏览限制在家目录和一个卷内,但那不是真正的边界——能访问这个接口的人本来就能附着到 session 里敲 ls。限制浏览却放开 shell 只是自欺,代价却是实打实的:机器特定的路径被写死在源码里。

测试

201 个测试,大部分真的跟 tmux 打交道——起真实 session、发真实按键、读回 #{pane_start_command} 之类的格式串确认参数落到了位。纯逻辑(路径处理、尺寸计算、手势换算、控制协议解析)拆成不含 DOM 的模块单独测。

public/*.js 只有浏览器会加载,语法错误不会被任何测试发现——所以有一个测试专门把 public/ 下每个模块都打包一遍。这个守卫是在真出过两次「测试全绿但页面一打开就炸」之后才加的。

文档

  • SECURITY.md先读这个:这个服务没有内置认证,暴露它等于暴露一个 shell
  • docs/deploy.md — 反向代理、TLS、launchd 服务
  • docs/Caddyfile.reference — Caddy 配置结构参考(凭据已替换为占位符)
  • docs/superpowers/ — 设计文档与实现计划

许可

MIT