pi-terminal-mux 0.2.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 ADDED
@@ -0,0 +1,115 @@
1
+ # pi-terminal-mux
2
+
3
+ Terminal multiplexer abstraction for pi extensions — one unified surface API across **muxy, cmux, tmux, zellij, wezterm, herdr and otty**, with automatic **headless fallback** (background child process + log file) when no multiplexer is detected.
4
+
5
+ Any pi extension that needs terminal interaction (splitting panes, sending commands, reading screens, closing panes, waiting for process exit) should depend on this package instead of re-implementing backend detection and command assembly.
6
+
7
+ [中文文档](./README.zh-CN.md)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install pi-terminal-mux
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import {
19
+ isMuxAvailable,
20
+ muxSetupHint,
21
+ createSurface,
22
+ createSurfaceSplit,
23
+ sendCommand,
24
+ sendLongCommand,
25
+ sendEscape,
26
+ readScreen,
27
+ closeSurface,
28
+ pollForExit,
29
+ } from "pi-terminal-mux";
30
+
31
+ if (!isMuxAvailable()) {
32
+ console.warn(muxSetupHint()); // localized setup hint via pi-extensions-i18n
33
+ }
34
+
35
+ // Smart placement: split / stack / new tab depending on the backend strategy
36
+ // (returns a headless surface when no multiplexer is available)
37
+ const surface = createSurface("my-agent");
38
+
39
+ // Long commands are written to a script file first to avoid terminal line wrapping
40
+ const scriptPath = sendLongCommand(surface, "pi --session abc", {
41
+ scriptPreamble: "export MY_FLAG=1",
42
+ });
43
+
44
+ const tail = readScreen(surface, 50);
45
+ sendEscape(surface);
46
+ closeSurface(surface);
47
+ ```
48
+
49
+ ## Backend detection
50
+
51
+ | Backend | Detection |
52
+ |---------|-----------|
53
+ | muxy | `MUXY_SOCKET_PATH` + `muxy` command |
54
+ | cmux | `CMUX_SOCKET_PATH` + `cmux` command |
55
+ | tmux | `TMUX` + `tmux` command |
56
+ | zellij | `ZELLIJ` / `ZELLIJ_SESSION_NAME` + `zellij` command |
57
+ | wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` command |
58
+ | herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` command |
59
+ | otty | `TERM_PROGRAM=otty` + `otty` command |
60
+
61
+ Default priority follows the table order (muxy first). Force a backend with:
62
+
63
+ - `PI_TERMINAL_MUX` (preferred): `muxy | cmux | tmux | zellij | wezterm | herdr | otty`
64
+ - `PI_SUBAGENT_MUX`: backward-compatible alias
65
+
66
+ If the forced backend's runtime is unavailable, `getMuxBackend()` returns `null` — it never silently falls back to another backend.
67
+
68
+ ## API overview
69
+
70
+ ### Unified surface API (same semantics across backends)
71
+
72
+ | Function | Description |
73
+ |----------|-------------|
74
+ | `createSurface(name)` | Smart placement (cmux: first right-split then tabs; zellij: tab-aware tiled/stacked; muxy/otty: breadth-first splits), returns a surface handle |
75
+ | `createSurfaceSplit(name, direction, fromSurface?)` | Split in an explicit direction (left/right/up/down) |
76
+ | `sendCommand(surface, command)` | Send a command and press Enter |
77
+ | `sendLongCommand(surface, command, opts?)` | Write long commands to a script file first; `opts.scriptPreamble` injects env exports; returns the script path |
78
+ | `sendEscape(surface)` | Send one ESC keypress |
79
+ | `readScreen(surface, lines?)` / `readScreenAsync` | Read the last N screen lines |
80
+ | `closeSurface(surface)` | Close the surface |
81
+ | `renameSurface(surface, name)` / `renameCurrentTab(title)` / `renameAgent(surface, name)` / `renameWorkspace(title)` | Naming, degrading per backend capability |
82
+ | `pollForExit(surface, signal, opts)` | Wait for the process in a surface to exit: `.exit` sidecar file first, then a screen sentinel (`__SUBAGENT_DONE_<code>__`); headless uses child process exit |
83
+ | `getLastSplitSource()` / `clearLastSplitSource()` | Source pane of the most recent split (for UI display) |
84
+
85
+ ### Detection and utilities
86
+
87
+ `getMuxBackend()`, `isMuxAvailable()`, `isHeadlessMode()`, `muxSetupHint()`, `getAgentPaneId(backend?)`, `backendAgentPaneEnvVar(backend)`, `shellEscape()`, `isFishShell()`, `exitStatusVar()`, plus zellij placement planning (`selectZellijPlacement` etc.) and cmux/otty JSON parsing helpers — all pure and unit-testable.
88
+
89
+ ### Backend-native APIs
90
+
91
+ Backend-native functions are also re-exported (e.g. `createHerdrSurface`, `splitHerdrPane`, `readHerdrScreen`, `sendOttyCommand`, `renameOttyTab`, ...). Subpath imports are available too: `pi-terminal-mux/mux`, `pi-terminal-mux/herdr`, `pi-terminal-mux/otty`.
92
+
93
+ ## Headless mode
94
+
95
+ When no backend is detected, `createSurface` returns a `headless:`-prefixed surface, `sendLongCommand` spawns a background child process writing to a log file, and `readScreen` / `pollForExit` / `closeSurface` keep the same semantics — callers need no special-casing.
96
+
97
+ ## Environment variables
98
+
99
+ | Variable | Description |
100
+ |----------|-------------|
101
+ | `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` | Force a backend |
102
+ | `PI_SUBAGENT_ZELLIJ_MIN_COLUMNS` / `PI_SUBAGENT_ZELLIJ_MIN_ROWS` | Minimum usable size for zellij splits (default 50x10; stacks instead when smaller) |
103
+ | `PI_SUBAGENT_RENAME_TMUX_WINDOW` / `PI_SUBAGENT_RENAME_TMUX_SESSION` | Allow renameCurrentTab / renameWorkspace on tmux (user naming untouched by default) |
104
+ | `PI_SUBAGENT_RENAME_HERDR_WORKSPACE` | Allow renameWorkspace on herdr |
105
+ | `PI_EXTENSIONS_LOCALE` | Hint language (`zh-CN` / `en-US` / `auto`), provided by pi-extensions-i18n |
106
+
107
+ ## Design constraints
108
+
109
+ - **No machine coupling**: every backend is selected via runtime detection (env vars + command availability); no hardcoded local paths; missing CLIs degrade backend-by-backend down to headless.
110
+ - **Localized user-facing text**: setup hints go through the [pi-extensions-i18n](https://www.npmjs.com/package/pi-extensions-i18n) catalog with complete `zh-CN` and `en-US` entries.
111
+ - **Agent pane anchoring**: the agent's own pane ID on muxy/herdr/otty is captured at module load (`AGENT_MUXY_PANE_ID` etc.), immune to later focus switches.
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,114 @@
1
+ # pi-terminal-mux
2
+
3
+ 终端多路复用器统一抽象层,供 pi 扩展复用。任何涉及终端交互(分屏、发命令、读屏、关屏、等待退出)的插件都应依赖本包,而不是各自重新实现 backend 探测与命令拼装。
4
+
5
+ 一套统一的 surface API 跨 **muxy、cmux、tmux、zellij、wezterm、herdr、otty** 七个后端,探测不到任何后端时自动降级为 **headless**(后台子进程 + 日志文件)。
6
+
7
+ [English README](./README.md)
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ npm install pi-terminal-mux
13
+ ```
14
+
15
+ ## 快速上手
16
+
17
+ ```ts
18
+ import {
19
+ isMuxAvailable,
20
+ muxSetupHint,
21
+ createSurface,
22
+ createSurfaceSplit,
23
+ sendCommand,
24
+ sendLongCommand,
25
+ sendEscape,
26
+ readScreen,
27
+ closeSurface,
28
+ pollForExit,
29
+ } from "pi-terminal-mux";
30
+
31
+ if (!isMuxAvailable()) {
32
+ console.warn(muxSetupHint()); // 中英文安装提示,由 pi-extensions-i18n 决定语言
33
+ }
34
+
35
+ // 智能放置:按后端策略分屏 / 堆叠 / 开 tab(headless 时返回 headless surface)
36
+ const surface = createSurface("my-agent");
37
+
38
+ // 长命令自动落脚本文件,避免终端宽度截断
39
+ const scriptPath = sendLongCommand(surface, "pi --session abc", {
40
+ scriptPreamble: "export MY_FLAG=1",
41
+ });
42
+
43
+ const tail = readScreen(surface, 50);
44
+ sendEscape(surface);
45
+ closeSurface(surface);
46
+ ```
47
+
48
+ ## 后端探测
49
+
50
+ | 后端 | 探测条件 |
51
+ |------|----------|
52
+ | muxy | `MUXY_SOCKET_PATH` + `muxy` 命令 |
53
+ | cmux | `CMUX_SOCKET_PATH` + `cmux` 命令 |
54
+ | tmux | `TMUX` + `tmux` 命令 |
55
+ | zellij | `ZELLIJ` / `ZELLIJ_SESSION_NAME` + `zellij` 命令 |
56
+ | wezterm | `WEZTERM_UNIX_SOCKET` + `wezterm` 命令 |
57
+ | herdr | `HERDR_ENV=1` + `HERDR_PANE_ID` + `herdr` 命令 |
58
+ | otty | `TERM_PROGRAM=otty` + `otty` 命令 |
59
+
60
+ 默认优先级即上表顺序(muxy 优先)。可用环境变量强制指定后端:
61
+
62
+ - `PI_TERMINAL_MUX`(推荐):`muxy | cmux | tmux | zellij | wezterm | herdr | otty`
63
+ - `PI_SUBAGENT_MUX`:同上的向后兼容别名
64
+
65
+ 指定的后端运行环境不满足时 `getMuxBackend()` 返回 `null`,不会悄悄降级到其他后端。
66
+
67
+ ## API 概览
68
+
69
+ ### 统一 surface API(跨后端语义一致)
70
+
71
+ | 函数 | 说明 |
72
+ |------|------|
73
+ | `createSurface(name)` | 智能放置新 surface(cmux 首次右分屏后续开 tab、zellij tab 感知平铺/堆叠、muxy/otty 广度优先分屏),返回 surface 标识 |
74
+ | `createSurfaceSplit(name, direction, fromSurface?)` | 指定方向(left/right/up/down)分屏 |
75
+ | `sendCommand(surface, command)` | 发送命令并回车执行 |
76
+ | `sendLongCommand(surface, command, opts?)` | 长命令先写脚本文件再执行;`opts.scriptPreamble` 可注入 env export;返回脚本路径 |
77
+ | `sendEscape(surface)` | 发送一次 ESC |
78
+ | `readScreen(surface, lines?)` / `readScreenAsync` | 读取屏幕尾部 N 行 |
79
+ | `closeSurface(surface)` | 关闭 surface |
80
+ | `renameSurface(surface, name)` / `renameCurrentTab(title)` / `renameAgent(surface, name)` / `renameWorkspace(title)` | 命名(按后端能力降级或跳过) |
81
+ | `pollForExit(surface, signal, opts)` | 等待 surface 内进程退出:优先 `.exit` sidecar 文件,其次屏幕 sentinel(`__SUBAGENT_DONE_<code>__`),headless 走子进程 exit |
82
+ | `getLastSplitSource()` / `clearLastSplitSource()` | 最近一次分屏的来源 pane(用于 UI 展示) |
83
+
84
+ ### 探测与工具
85
+
86
+ `getMuxBackend()`、`isMuxAvailable()`、`isHeadlessMode()`、`muxSetupHint()`、`getAgentPaneId(backend?)`、`backendAgentPaneEnvVar(backend)`、`shellEscape()`、`isFishShell()`、`exitStatusVar()`,以及 zellij 放置规划(`selectZellijPlacement` 等)与 cmux/otty JSON 解析等纯函数,均可直接引用做单元测试。
87
+
88
+ ### 后端原生 API
89
+
90
+ 各后端原生函数也从包入口透出(如 `createHerdrSurface`、`splitHerdrPane`、`readHerdrScreen`、`sendOttyCommand`、`renameOttyTab`……),子路径导入亦可:`pi-terminal-mux/mux`、`pi-terminal-mux/herdr`、`pi-terminal-mux/otty`。
91
+
92
+ ## Headless 模式
93
+
94
+ 探测不到任何后端时,`createSurface` 返回 `headless:` 前缀的 surface,`sendLongCommand` 直接 spawn 后台子进程并把输出写入日志文件,`readScreen`/`pollForExit`/`closeSurface` 语义保持不变,调用方无需特判。
95
+
96
+ ## 环境变量
97
+
98
+ | 变量 | 说明 |
99
+ |------|------|
100
+ | `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` | 强制指定后端 |
101
+ | `PI_SUBAGENT_ZELLIJ_MIN_COLUMNS` / `PI_SUBAGENT_ZELLIJ_MIN_ROWS` | zellij 分屏最小可用尺寸(默认 50×10,不满足时改堆叠) |
102
+ | `PI_SUBAGENT_RENAME_TMUX_WINDOW` / `PI_SUBAGENT_RENAME_TMUX_SESSION` | tmux 下允许 renameCurrentTab / renameWorkspace(默认不动用户命名) |
103
+ | `PI_SUBAGENT_RENAME_HERDR_WORKSPACE` | herdr 下允许 renameWorkspace |
104
+ | `PI_EXTENSIONS_LOCALE` | 提示文案语言(`zh-CN` / `en-US` / `auto`),由 pi-extensions-i18n 提供 |
105
+
106
+ ## 设计约束
107
+
108
+ - **不绑定具体机器**:全部后端通过运行时探测(环境变量 + 命令存在性)选择,零硬编码本机路径;外部 CLI 缺失时按后端逐个降级,最终落到 headless。
109
+ - **用户文案国际化**:面向用户的提示走 [pi-extensions-i18n](https://www.npmjs.com/package/pi-extensions-i18n) catalog,中英文齐全。
110
+ - **agent pane 锚定**:muxy/herdr/otty 的 agent 自身 pane ID 在模块加载时捕获(`AGENT_MUXY_PANE_ID` 等),不受用户后续焦点切换影响。
111
+
112
+ ## License
113
+
114
+ MIT
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index.ts";
@@ -0,0 +1,46 @@
1
+ {
2
+ "setupHint.none": {
3
+ "zh-CN": "未检测到可用的终端多路复用器。",
4
+ "en-US": "No supported terminal multiplexer found."
5
+ },
6
+ "setupHint.cmux": {
7
+ "zh-CN": "请在 cmux 中启动 pi(`cmux pi`)。",
8
+ "en-US": "Start pi inside cmux (`cmux pi`)."
9
+ },
10
+ "setupHint.muxy": {
11
+ "zh-CN": "请在 Muxy 终端中启动 pi。",
12
+ "en-US": "Start pi inside Muxy terminal."
13
+ },
14
+ "setupHint.tmux": {
15
+ "zh-CN": "请在 tmux 中启动 pi(`tmux new -A -s pi 'pi'`)。",
16
+ "en-US": "Start pi inside tmux (`tmux new -A -s pi 'pi'`)."
17
+ },
18
+ "setupHint.zellij": {
19
+ "zh-CN": "请在 zellij 中启动 pi(`zellij --session pi`,然后运行 `pi`)。",
20
+ "en-US": "Start pi inside zellij (`zellij --session pi`, then run `pi`)."
21
+ },
22
+ "setupHint.wezterm": {
23
+ "zh-CN": "请在 WezTerm 中启动 pi。",
24
+ "en-US": "Start pi inside WezTerm."
25
+ },
26
+ "setupHint.herdr": {
27
+ "zh-CN": "请在终端中运行 herdr(`herdr`),拆分一个 pane(prefix+v 或 prefix+-),然后在该 pane 中运行 `pi`。herdr 会自动注入 HERDR_ENV=1 和 HERDR_PANE_ID 供 pi 检测。",
28
+ "en-US": "Start herdr in your terminal (`herdr`), split a pane (prefix+v or prefix+-), then run `pi` in that pane. herdr auto-injects HERDR_ENV=1 + HERDR_PANE_ID for pi to detect."
29
+ },
30
+ "setupHint.otty": {
31
+ "zh-CN": "请在 Otty 中运行 pi(Otty 会自动设置 TERM_PROGRAM=otty)。",
32
+ "en-US": "Run pi inside Otty (Otty sets TERM_PROGRAM=otty automatically)."
33
+ },
34
+ "setupHint.generic": {
35
+ "zh-CN": "请在 Muxy、cmux(`cmux pi`)、tmux(`tmux new -A -s pi 'pi'`)、zellij(`zellij --session pi`,然后运行 `pi`)、WezTerm、herdr(运行 `herdr`,拆分 pane 后在其中运行 `pi`)或 Otty(Otty 会自动设置 TERM_PROGRAM=otty)中启动 pi。",
36
+ "en-US": "Start pi inside Muxy, cmux (`cmux pi`), tmux (`tmux new -A -s pi 'pi'`), zellij (`zellij --session pi`, then run `pi`), WezTerm, herdr (run `herdr`, split a pane, then run `pi` in it), or Otty (Otty sets TERM_PROGRAM=otty automatically)."
37
+ },
38
+ "setupHint.herdrPreferred": {
39
+ "zh-CN": "请在 herdr 中启动 pi(必须设置 HERDR_ENV=1;先运行 herdr,再在 pane 中启动 pi)。",
40
+ "en-US": "Start pi inside herdr (HERDR_ENV=1 must be set; run herdr, then start pi in a pane)."
41
+ },
42
+ "setupHint.ottySendKeys": {
43
+ "zh-CN": "Otty 的 `ipc-allow-send-keys` 未启用。如需让 pi 驱动子 agent 分屏,请在 ~/.config/otty/config.toml 中添加 `ipc-allow-send-keys = true` 并重新加载 Otty。",
44
+ "en-US": "Otty's `ipc-allow-send-keys` is disabled. To let pi drive subagent panes, add `ipc-allow-send-keys = true` to ~/.config/otty/config.toml and reload Otty."
45
+ }
46
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "pi-terminal-mux",
3
+ "version": "0.2.0",
4
+ "description": "Terminal multiplexer abstraction for pi extensions — unified surface API across muxy, cmux, tmux, zellij, wezterm, herdr and otty, with headless fallback",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./mux": "./src/mux.ts",
10
+ "./herdr": "./src/herdr.ts",
11
+ "./otty": "./src/otty.ts"
12
+ },
13
+ "files": [
14
+ "index.ts",
15
+ "src",
16
+ "locales",
17
+ "README.md",
18
+ "README.zh-CN.md",
19
+ "tsconfig.json"
20
+ ],
21
+ "scripts": {
22
+ "test": "tsx --test tests/*.test.ts",
23
+ "typecheck": "tsc --noEmit --pretty false",
24
+ "build": "npm run typecheck",
25
+ "check": "npm run typecheck && npm test && npm pack --dry-run --json > /dev/null"
26
+ },
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "license": "MIT",
31
+ "author": "maplezzk",
32
+ "homepage": "https://github.com/maplezzk/pi-extensions/tree/main/packages/pi-terminal-mux",
33
+ "bugs": "https://github.com/maplezzk/pi-extensions/issues",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/maplezzk/pi-extensions.git",
37
+ "directory": "packages/pi-terminal-mux"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org"
42
+ },
43
+ "keywords": [
44
+ "pi-package",
45
+ "pi",
46
+ "pi-extension",
47
+ "terminal",
48
+ "multiplexer",
49
+ "tmux",
50
+ "zellij",
51
+ "wezterm",
52
+ "coding-agent"
53
+ ],
54
+ "peerDependencies": {
55
+ "pi-extensions-i18n": ">=0.2.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "24.12.4",
59
+ "pi-extensions-i18n": "^0.3.0",
60
+ "tsx": "4.23.1",
61
+ "typescript": "5.9.3"
62
+ }
63
+ }