@ai-zen/socket-pty
v0.2.1
Published
Cross-platform PTY transport exposed over a socket via a standard JSON-RPC 2.0 protocol — any client that speaks JSON-RPC can read/write a managed terminal process. Renders the real terminal screen via xterm-headless. MCP stdio management included as a se
Downloads
519
Maintainers
Readme
@ai-zen/socket-pty
通过 socket(JSON-RPC 2.0)暴露一个可托管的终端进程。启动一个真实终端进程(bash、vim、node REPL 等)后,它会成为网络端点;任何能建立 socket 连接并遵循 JSON-RPC 2.0 的客户端,均可对该终端进行读写。
目录
安装
npm install @ai-zen/socket-pty在仓库内本地使用时,先构建并经由 dist/ 引入:
npm install
npm run build
node dist/cli.js serve --cmd bash --socket /tmp/x.sock依赖
node-pty(必需):pty 后端。Linux 无预编译二进制,安装时从源码编译,需要 g++ 支持 C++20(gcc-10及以上)。Windows / macOS 提供预编译二进制,开箱即用。@xterm/headless(必需):屏幕渲染后端,纯 JS 无编译负担。
node-pty 是不可替代的硬依赖,不存在降级后端。
组成与入口
包提供两个入口:
| 入口 | 内容 | 用途 |
|------|------|------|
| @ai-zen/socket-pty(主入口 .) | PtyServer / PtySession / createSocketPty / 协议与类型 | 以程序化方式启动/持有终端服务 |
| @ai-zen/socket-pty/mcp(./mcp) | MCPManager / ConnectionPool / MCP 适配层 | 作为 MCP server 暴露终端操作工具 |
主入口只暴露能力本体,不含入口适配。MCP 适配层是独立子路径。
端点与地址
端点(endpoint)是终端服务监听的网络位置,有两种形态:
| 形态 | 描述 |
|------|------|
| Unix domain socket | { type: "unix", path: "/path/to.sock" },地址形式 unix:/path/to.sock |
| TCP loopback | { type: "tcp", port: 5174, host: "127.0.0.1" },地址形式 tcp:127.0.0.1:5174 |
端点地址由启动方显式指定(--socket <path> 或 --port <port> 二选一)。连接一个终端的前提是已知其地址;服务端不自动分配地址——它启动时在调用方指定的地址上监听。
启动服务
CLI
# Unix domain socket
socket-pty serve --cmd bash --socket /tmp/v.sock
# TCP loopback
socket-pty serve --cmd bash --port 5174
# Windows:使用 PowerShell / CMD
socket-pty serve --cmd powershell.exe --port 5174
socket-pty serve --cmd cmd.exe --port 5174serve 选项:
| 选项 | 说明 |
|------|------|
| --cmd <命令> | 要托管的命令(必填)。Windows 上需带 .exe 后缀,如 powershell.exe / cmd.exe(见命令与平台) |
| --socket <路径> | Unix domain socket 监听路径(与 --port 二选一,必给其一) |
| --port <端口> | TCP 监听端口,1-65535 的具体端口(与 --socket 二选一,必给其一) |
| --cols <n> | 终端列数(默认 100) |
| --rows <n> | 终端行数(默认 30) |
| --cwd <路径> | 工作目录 |
| -h / --help | 帮助 |
--socket 与 --port 二选一,不能同时给出;两者都不给、或 --port 不是 1-65535 的具体端口时,serve 报错退出。
启动成功后打印:
[socket-pty] 会话已启动,命令: bash
[socket-pty] 连接地址: 127.0.0.1:5174程序化
import { createSocketPty } from "@ai-zen/socket-pty";
const server = createSocketPty({
endpoint: { type: "unix", path: "/tmp/v.sock" }, // 或 { type: "tcp", port: 5174, host: "127.0.0.1" }
command: "bash",
cols: 100,
rows: 30,
});
const address = await server.listen();
// address: unix:/tmp/v.sock命令与平台
--cmd / command 指定被托管的命令,由调用方负责传入当前平台可用的命令:
| 平台 | 推荐命令 | 说明 |
|------|----------|------|
| Linux / macOS | bash | 也可传 zsh、sh、node -i 等 |
| Windows | powershell.exe / cmd.exe | 必须带 .exe 后缀 |
Windows 上 node-pty(ConPTY)对裸命令名不做 PATH 搜索:传 powershell、bash 这类不带 .exe 的名字会报 File not found。应传带后缀的可执行名,或完整路径(如 C:\Program Files\Git\bin\bash.exe)。
# Windows:两种可用写法
socket-pty serve --cmd powershell.exe --port 5174
socket-pty serve --cmd cmd.exe --port 5174连接与协议
端点地址形式
- Unix domain socket:
unix:/path/to.sock - TCP:
tcp:127.0.0.1:5174
帧格式
连接建立后,每行一条 JSON-RPC 2.0 报文,以 \n 分隔(换行分帧)。
- 请求必须包含
id(string或number);不支持通知(无id的请求被拒绝)。 - 每个请求对应一个响应,响应回显同一个
id。 params必须是对象。
请求: {"jsonrpc":"2.0","method":"pty/read","params":{},"id":7}
成功: {"jsonrpc":"2.0","result":{...},"id":7}
失败: {"jsonrpc":"2.0","error":{"code":-32601,"message":"..."},"id":7}method 一览
| method | params | result(成功) |
|--------|--------|----------------|
| pty/read | { top? \| bottom? \| start?+end? } | { screen, cursor, size } |
| pty/write | { action } | { written } |
| pty/wait | { match?, timeout? } | { matched, screen, cursor, size, waitedMs } |
| pty/status | {} | { running, exitCode, pid } |
| pty/resize | { cols, rows } | {} |
| pty/kill | {} | {} |
错误码
| code | 含义 |
|------|------|
| -32700 | 解析错误(非法 JSON) |
| -32600 | 无效请求(结构不合法 / 旧 {op} 协议被拒绝 / 缺 id) |
| -32601 | 方法不存在 |
| -32602 | 无效参数(如 text/key 互斥、top/bottom 互斥、未知键名) |
| -32603 | 内部错误 |
| -32000 | 服务端错误 |
数据包格式
以下为每个 method 的完整请求与响应报文。
pty/status — 查询会话状态
→ {"jsonrpc":"2.0","method":"pty/status","params":{},"id":1}
← {"jsonrpc":"2.0","result":{"running":true,"exitCode":null,"pid":1234},"id":1}running:boolean,会话是否运行中。exitCode:number | null,已退出时为退出码,否则为null。pid:number,被托管进程的 pid。
pty/read — 读取当前屏幕
→ {"jsonrpc":"2.0","method":"pty/read","params":{},"id":2}
← {"jsonrpc":"2.0","result":{"screen":"root@host:~$\n","cursor":{"row":1,"col":14},"size":{"cols":100,"rows":30}},"id":2}screen:string,当前终端屏幕的纯文本(已去除 ANSI 控制序列,每行尾部空格被去除,多行以\n分隔)。语义为终端当前真实画面,非历史日志。cursor:object,光标位置,row/col为 1-based(示例中root@host:~$占 13 字符,光标在其后,col为 14)。size:object,终端尺寸{ cols, rows }。
pty/read 支持截取当前屏的部分行。top/bottom 与 start+end 为互斥的两组:
→ {"jsonrpc":"2.0","method":"pty/read","params":{"top":3},"id":3}
→ {"jsonrpc":"2.0","method":"pty/read","params":{"bottom":3},"id":4}
→ {"jsonrpc":"2.0","method":"pty/read","params":{"start":2,"end":5},"id":5}top: N— 取顶部 N 行。bottom: N— 取底部 N 行。start/end— 取 1-based 闭区间[start, end],必须成对给出。- 校验:
top/bottom互斥;start/end必须成对;两组不能混用;越界自动 clamp。
pty/write — 写入数据
action 为单个动作对象或动作数组。
写文本:
→ {"jsonrpc":"2.0","method":"pty/write","params":{"action":{"text":"echo hello"}},"id":6}
← {"jsonrpc":"2.0","result":{"written":11},"id":6}写按键:
→ {"jsonrpc":"2.0","method":"pty/write","params":{"action":{"key":"Enter"}},"id":7}
← {"jsonrpc":"2.0","result":{"written":1},"id":7}动作数组(按序执行多个动作):
→ {"jsonrpc":"2.0","method":"pty/write","params":{"action":[{"text":"ls"},{"key":"Enter"}]},"id":8}
← {"jsonrpc":"2.0","result":{"written":5},"id":8}action内text与key二选一,不能同时给出;空对象、空数组被拒绝。written:number,实际写入的 UTF-8 字节数(数组动作返回合计)。
pty/wait — 等待某段输出出现
→ {"jsonrpc":"2.0","method":"pty/wait","params":{"match":"hello","timeout":6000},"id":9}
← {"jsonrpc":"2.0","result":{"matched":true,"screen":"hello\n","cursor":{"row":2,"col":1},"size":{"cols":100,"rows":30},"waitedMs":45},"id":9}- 在当前屏幕中按子串匹配
match;未给match时只按timeout等待一段固定时长。 timeout:最大等待毫秒,默认10000。matched:boolean,是否在超时前匹配到。screen:匹配时或超时时的当前屏幕。waitedMs:实际耗时(毫秒)。
超时未匹配:
→ {"jsonrpc":"2.0","method":"pty/wait","params":{"match":"absent","timeout":100},"id":10}
← {"jsonrpc":"2.0","result":{"matched":false,"screen":"...","cursor":{"row":1,"col":1},"size":{"cols":100,"rows":30},"waitedMs":100},"id":10}pty/resize — 调整终端尺寸
→ {"jsonrpc":"2.0","method":"pty/resize","params":{"cols":120,"rows":40},"id":11}
← {"jsonrpc":"2.0","result":{},"id":11}pty/kill — 终止会话并关闭端点
→ {"jsonrpc":"2.0","method":"pty/kill","params":{},"id":12}
← {"jsonrpc":"2.0","result":{},"id":12}发起 pty/kill 后,会话被终止、端点关闭、服务进程退出,当前连接被服务端关闭。
错误响应
所有错误响应为统一形状:
→ {"jsonrpc":"2.0","method":"pty/unknown","params":{},"id":13}
← {"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found: pty/unknown"},"id":13}
→ {"jsonrpc":"2.0","method":"pty/write","params":{"action":{"text":"x","key":"Enter"}},"id":14}
← {"jsonrpc":"2.0","error":{"code":-32602,"message":"write 动作内 text 与 key 只能二选一"},"id":14}- 错误对象形如
{ code, message, data? },code/message见上文。 - 正常请求的响应回显请求的
id;无法解析的报文(连id都取不到)返回id: null。
会话流转
以下时序描述一个会话从启动到结束的完整流转:
sequenceDiagram
participant S as 启动方
participant P as serve 进程
participant C as 客户端
S->>P: 指定地址并启动(--socket / --port)
activate P
Note over P: 在指定地址监听
C->>P: 建立 socket 连接
C->>P: {"jsonrpc":"2.0","method":"pty/status","id":1}
P-->>C: {"jsonrpc":"2.0","result":{...},"id":1}
C->>P: {"jsonrpc":"2.0","method":"pty/write","params":{"action":[{"text":"ls"},{"key":"Enter"}]},"id":2}
P-->>C: {"jsonrpc":"2.0","result":{"written":5},"id":2}
C->>P: {"jsonrpc":"2.0","method":"pty/wait","params":{"match":"file","timeout":5000},"id":3}
P-->>C: {"jsonrpc":"2.0","result":{"matched":true,"screen":"...","id":3}}
C->>P: {"jsonrpc":"2.0","method":"pty/read","params":{},"id":4}
P-->>C: {"jsonrpc":"2.0","result":{"screen":"...","cursor":{"row":2,"col":1},"size":{"cols":100,"rows":30}},"id":4}
C->>P: {"jsonrpc":"2.0","method":"pty/kill","params":{},"id":5}
P-->>C: {"jsonrpc":"2.0","result":{},"id":5}
Note over P: 会话终止,端点关闭,连接被关闭
deactivate P- 启动:启动方在指定地址监听,地址已知。
- 连接与请求:客户端连到该地址,发送 JSON-RPC 2.0 请求行,收到对应响应行。
- 终止:
pty/kill终止会话并关闭端点。
真屏幕与 read 截取
- 屏幕由服务端(端点侧)用 xterm-headless 渲染,是终端的当前真实画面(所见即所得)。
pty/read返回整个当前屏幕(rows行)。它不是历史日志——滚动出屏外的内容不会出现在该返回值中。- 可截取当前屏的部分行(
top/bottom/start+end,见数据包格式)。 - 连接断开后终端仍运行;重新连接后仍可读到该会话的当前画面。
write 支持的键名
pty/write 的 action.key 使用以下键名(区分大小写),对应写入的字节序列:
| 类别 | 键名 |
|------|------|
| 方向 | ArrowUp ArrowDown ArrowLeft ArrowRight |
| 编辑 | Enter Tab Backspace Delete Home End PageUp PageDown Insert Escape Space |
| 功能 | F1 … F12 |
| 组合 | Ctrl+字母(Ctrl+A…Ctrl+Z)、Shift+字母、Alt+字母、Alt+Enter、Ctrl+Enter、Ctrl+Space 等,可组合修饰符(如 Ctrl+Alt+Del) |
- 单字符文本可直接用
text字段。 key无法解析时返回-32602无效参数错误。
程序化 API(TypeScript)
createSocketPty
import { createSocketPty } from "@ai-zen/socket-pty";
const server = createSocketPty({
endpoint: { type: "unix", path: "/tmp/v.sock" }, // 或 { type:"tcp", port:5174, host:"127.0.0.1" }
command: "bash", // Windows 上请用 "powershell.exe" / "cmd.exe"(见「命令与平台」)
cols: 100,
rows: 30,
cwd: "/path/to/workdir", // 可选
});
const address = await server.listen();endpoint 必填,地址由调用方指定。
PtyServer 实例方法
| 方法 | 说明 |
|------|------|
| listen(): Promise<string> | 监听端点并创建会话,返回连接地址 |
| address(): string | 实际监听地址 |
| session(): IPtySession \| null | 当前会话 |
| close({ kill? }): Promise<void> | 关闭端点(kill: true 时终止会话),幂等 |
会话对象方法
回调 server.session 返回 IPtySession,提供同步读屏等能力:
const session = server.session;
await session?.readScreen(); // 整屏
await session?.readScreen({ bottom: 5 }); // 底部 5 行
await session?.readScreen({ start: 2, end: 6 }); // 第 2..6 行| 方法 / 属性 | 说明 |
|------------|------|
| readScreen(sel?) | 读当前屏幕,返回 { screen, cursor, size } |
| wait(opts) | 等待输出,返回 { matched, screen, cursor, size, waitedMs } |
| write(data): number | 写原始数据,返回 UTF-8 字节数 |
| resize(cols, rows) | 调整尺寸 |
| kill(signal?) | 终止 |
| pid / running / exitCode | 进程信息 |
MCP 适配层
@ai-zen/socket-pty/mcp 提供一套 MCP server(stdio 传输),将终端操作暴露为 MCP 工具。
在 MCP 宿主中注册
{
"mcpServers": {
"socket-pty": {
"command": "npx",
"args": ["@ai-zen/socket-pty", "mcp"]
}
}
}工具
| 工具 | 参数 | 返回 |
|------|------|------|
| spawn | command, cwd?, cols?, rows? | address(字符串) |
spawn 的 command 与 CLI / 程序化接口一致,遵循命令与平台:Windows 上应传 powershell.exe / cmd.exe(带 .exe 后缀),传 powershell / bash 会启动失败。
| read | address, top?/bottom?/start?+end? | 屏幕文本 |
| write | address, action | 写入成功(含字节数) |
| wait | address, match?, timeout? | 匹配结果 + 屏幕 |
| resize | address, cols, rows | 成功/失败 |
| status | address | { running, exitCode, pid } |
| kill | address | 成功/失败 |
spawn 的地址来源
spawn 由 MCP 管理器自分配本机临时端点,并在工具返回中给出该 address:
- Unix:
unix:/tmp/pty-mcp-<pid>-<时间戳>-<随机>.sock - Windows:
tcp:127.0.0.1:<port>(先探测分配一个空闲端口)
该 address 由管理器分配并返回给调用方,作为后续工具的 address 参数使用。会话为孤儿进程,常驻直到被 kill。
spawn 的自动分配仅存在于 MCP 内部——管理器既分配地址又持有使用,地址始终在其掌握中。对外提供端点(serve / createSocketPty)则必须显式定址。
编程式引入
import { MCPManager, ConnectionPool } from "@ai-zen/socket-pty/mcp";MCPManager 提供 spawn / read / write / wait / resize / status / kill 方法,经内部 ConnectionPool(按地址复用连接,JSON-RPC 2.0)分发到对应 serve 进程。
会话生命周期
- 常驻:
serve/spawn启动的进程独立常驻,客户端的断开重连不影响其存活与当前屏幕。 - 连接:一个端点可被多个客户端连接;每个请求独立带
id,按id对应响应。 - 终止:
pty/kill终止会话并关闭端点;之后端点不可再连接。 - 清理:Unix socket 端点关闭时,对应 socket 文件被移除。
已知限制
- 仅当前屏幕,无历史日志:
pty/read返回当前画面(rows行),不提供滚动出屏外的历史回读。 - 依赖 node-pty(必需):无降级后端。Linux 需能编译 node-pty(见依赖)。
- Windows 命令需带
.exe后缀:node-pty 在 Windows 上不对裸命令名做 PATH 解析,传powershell/bash会File not found;应传powershell.exe/cmd.exe或完整路径(见命令与平台)。 - 不支持通知:所有请求必须带
id。 - 对外端点必须显式定址:
serve/createSocketPty不接受自动分配;MCP 的spawn为特例,地址由其自分配并返回。
许可证
MIT
