tmux-watch 2026.2.1
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/LICENSE +21 -0
- package/README.md +256 -0
- package/index.ts +29 -0
- package/openclaw.plugin.json +47 -0
- package/package.json +47 -0
- package/skills/SKILL.md +124 -0
- package/src/cli.ts +120 -0
- package/src/config.ts +129 -0
- package/src/manager.ts +837 -0
- package/src/service.ts +17 -0
- package/src/tmux-watch-tool.ts +189 -0
- package/types/openclaw-plugin-sdk.d.ts +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wangnov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# tmux-watch
|
|
2
|
+
|
|
3
|
+
[中文](#zh) | [English](#en)
|
|
4
|
+
|
|
5
|
+
<a id="zh"></a>
|
|
6
|
+
## 中文
|
|
7
|
+
|
|
8
|
+
基于 tmux 输出的稳定性监测插件:当某个 pane 的输出在连续 N 次捕获中保持不变时,触发告警并唤醒 Agent,总结并通知你。
|
|
9
|
+
|
|
10
|
+
### 安装
|
|
11
|
+
|
|
12
|
+
#### 从 npm 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
openclaw plugins install tmux-watch
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### 从本地目录安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
openclaw plugins install /path/to/tmux-watch
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
或使用软链接(便于开发调试):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
openclaw plugins install --link /path/to/tmux-watch
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### 从归档安装
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
openclaw plugins install ./tmux-watch.tgz
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> 安装或启用插件后需要重启 Gateway。
|
|
37
|
+
|
|
38
|
+
### 配置
|
|
39
|
+
|
|
40
|
+
在 `~/.openclaw/openclaw.json` 中启用并配置:
|
|
41
|
+
|
|
42
|
+
```json5
|
|
43
|
+
{
|
|
44
|
+
"plugins": {
|
|
45
|
+
"entries": {
|
|
46
|
+
"tmux-watch": {
|
|
47
|
+
"enabled": true,
|
|
48
|
+
"config": {
|
|
49
|
+
"socket": "/private/tmp/tmux-501/default",
|
|
50
|
+
"captureIntervalSeconds": 10,
|
|
51
|
+
"stableCount": 6,
|
|
52
|
+
"captureLines": 200,
|
|
53
|
+
"stripAnsi": true,
|
|
54
|
+
"maxOutputChars": 4000,
|
|
55
|
+
"notify": {
|
|
56
|
+
"mode": "targets",
|
|
57
|
+
"targets": [
|
|
58
|
+
{ "channel": "gewe-openclaw", "target": "wxid_xxx", "label": "gewe" }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### 配置项说明
|
|
69
|
+
|
|
70
|
+
- `enabled`:是否启用插件(默认 `true`)。
|
|
71
|
+
- `socket`:tmux socket 路径(必填)。
|
|
72
|
+
- `captureIntervalSeconds`:每次捕获间隔(秒),默认 `10`。
|
|
73
|
+
- `stableCount`:连续多少次捕获内容一致才触发告警,默认 `6`。总时长 = `captureIntervalSeconds × stableCount`(例如 `3 × 5 = 15s`)。
|
|
74
|
+
- `pollIntervalMs`:**兼容字段**,捕获间隔(毫秒)。仅在需要与旧配置兼容时使用。
|
|
75
|
+
- `stableSeconds`:**兼容字段**,稳定时长(秒)。会按当前捕获间隔换算成次数。
|
|
76
|
+
- `captureLines`:从 pane 末尾向上截取的行数(默认 `200`)。
|
|
77
|
+
- `stripAnsi`:是否剥离 ANSI 转义码(默认 `true`)。
|
|
78
|
+
- `maxOutputChars`:通知中最多包含的输出字符数(默认 `4000`,超出将从末尾截断)。
|
|
79
|
+
- `sessionKey`:覆盖默认 Agent 会话(通常不需要改)。
|
|
80
|
+
- `notify.mode`:通知方式(`last` / `targets` / `targets+last`)。
|
|
81
|
+
- `notify.targets`:通知目标数组(支持多个 channel,按数组顺序发送)。
|
|
82
|
+
|
|
83
|
+
### 快速配置(onboarding)
|
|
84
|
+
|
|
85
|
+
插件提供一个最小化向导,仅要求设置 `socket`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
openclaw tmux-watch setup
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
你也可以手动指定:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
openclaw tmux-watch setup --socket "/private/tmp/tmux-501/default"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### socket 如何获取
|
|
98
|
+
|
|
99
|
+
进入目标 tmux 会话后执行:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
echo $TMUX
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
输出形如:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
/private/tmp/tmux-501/default,3191,4
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
逗号前的路径就是 socket,配置到 `socket` 字段即可。
|
|
112
|
+
|
|
113
|
+
### 订阅(通过 Agent 工具)
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"action": "add",
|
|
118
|
+
"target": "session:0.0",
|
|
119
|
+
"label": "codex-tui",
|
|
120
|
+
"note": "本会话是AI编程TUI助手,卡住时总结最后输出并通知我",
|
|
121
|
+
"captureIntervalSeconds": 3,
|
|
122
|
+
"stableCount": 5
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 依赖
|
|
127
|
+
|
|
128
|
+
- 系统依赖:`tmux`
|
|
129
|
+
- peer 依赖:`openclaw >= 2026.1.29`
|
|
130
|
+
|
|
131
|
+
<a id="en"></a>
|
|
132
|
+
## English
|
|
133
|
+
|
|
134
|
+
tmux-watch monitors a tmux pane and triggers an alert when the output stays unchanged for N consecutive captures.
|
|
135
|
+
The agent is woken up to summarize the last output and notify you.
|
|
136
|
+
|
|
137
|
+
### Install
|
|
138
|
+
|
|
139
|
+
#### From npm
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
openclaw plugins install tmux-watch
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### From a local directory
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
openclaw plugins install /path/to/tmux-watch
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or use a symlink (for local development):
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
openclaw plugins install --link /path/to/tmux-watch
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### From an archive
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
openclaw plugins install ./tmux-watch.tgz
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> Restart the Gateway after installing or enabling the plugin.
|
|
164
|
+
|
|
165
|
+
### Configuration
|
|
166
|
+
|
|
167
|
+
Edit `~/.openclaw/openclaw.json`:
|
|
168
|
+
|
|
169
|
+
```json5
|
|
170
|
+
{
|
|
171
|
+
"plugins": {
|
|
172
|
+
"entries": {
|
|
173
|
+
"tmux-watch": {
|
|
174
|
+
"enabled": true,
|
|
175
|
+
"config": {
|
|
176
|
+
"socket": "/private/tmp/tmux-501/default",
|
|
177
|
+
"captureIntervalSeconds": 10,
|
|
178
|
+
"stableCount": 6,
|
|
179
|
+
"captureLines": 200,
|
|
180
|
+
"stripAnsi": true,
|
|
181
|
+
"maxOutputChars": 4000,
|
|
182
|
+
"notify": {
|
|
183
|
+
"mode": "targets",
|
|
184
|
+
"targets": [
|
|
185
|
+
{ "channel": "gewe-openclaw", "target": "wxid_xxx", "label": "gewe" }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Configuration reference
|
|
196
|
+
|
|
197
|
+
- `enabled`: Enable/disable the plugin (default `true`).
|
|
198
|
+
- `socket`: tmux socket path (required).
|
|
199
|
+
- `captureIntervalSeconds`: Capture interval in seconds (default `10`).
|
|
200
|
+
- `stableCount`: Number of consecutive identical captures before alert (default `6`). Total duration = `captureIntervalSeconds × stableCount` (for example `3 × 5 = 15s`).
|
|
201
|
+
- `pollIntervalMs`: **Legacy** capture interval in milliseconds. Use only for backward compatibility.
|
|
202
|
+
- `stableSeconds`: **Legacy** stable duration in seconds. Converted into counts using the current interval.
|
|
203
|
+
- `captureLines`: Lines captured from the bottom of the pane (default `200`).
|
|
204
|
+
- `stripAnsi`: Strip ANSI escape codes (default `true`).
|
|
205
|
+
- `maxOutputChars`: Max output chars in the notification (default `4000`, tail-truncated).
|
|
206
|
+
- `sessionKey`: Override the default agent session (rare).
|
|
207
|
+
- `notify.mode`: Notification mode (`last` / `targets` / `targets+last`).
|
|
208
|
+
- `notify.targets`: Notification targets (multiple channels supported, sent in order).
|
|
209
|
+
|
|
210
|
+
#### Find the socket
|
|
211
|
+
|
|
212
|
+
Inside the target tmux session:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
echo $TMUX
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Output looks like:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
/private/tmp/tmux-501/default,3191,4
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Use the path before the first comma as `socket`.
|
|
225
|
+
|
|
226
|
+
### Quick setup (onboarding)
|
|
227
|
+
|
|
228
|
+
The plugin ships a minimal setup wizard that only requires the `socket`:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
openclaw tmux-watch setup
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Or pass it explicitly:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
openclaw tmux-watch setup --socket "/private/tmp/tmux-501/default"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Add a subscription (via agent tool)
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"action": "add",
|
|
245
|
+
"target": "session:0.0",
|
|
246
|
+
"label": "codex-tui",
|
|
247
|
+
"note": "This is an AI coding TUI; summarize the last output and notify me if it stalls.",
|
|
248
|
+
"captureIntervalSeconds": 3,
|
|
249
|
+
"stableCount": 5
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Requirements
|
|
254
|
+
|
|
255
|
+
- System dependency: `tmux`
|
|
256
|
+
- Peer dependency: `openclaw >= 2026.1.29`
|
package/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { createTmuxWatchManager } from "./src/manager.js";
|
|
4
|
+
import { createTmuxWatchService } from "./src/service.js";
|
|
5
|
+
import { createTmuxWatchTool } from "./src/tmux-watch-tool.js";
|
|
6
|
+
import { registerTmuxWatchCli } from "./src/cli.js";
|
|
7
|
+
|
|
8
|
+
const plugin = {
|
|
9
|
+
id: "tmux-watch",
|
|
10
|
+
name: "tmux-watch",
|
|
11
|
+
description: "Watch tmux panes and notify the agent when output stays stable.",
|
|
12
|
+
register(api: OpenClawPluginApi) {
|
|
13
|
+
const manager = createTmuxWatchManager(api);
|
|
14
|
+
api.registerTool(createTmuxWatchTool(manager));
|
|
15
|
+
api.registerService(createTmuxWatchService(manager));
|
|
16
|
+
api.registerCli(
|
|
17
|
+
(ctx: { program: unknown }) => {
|
|
18
|
+
registerTmuxWatchCli({
|
|
19
|
+
program: ctx.program as Command,
|
|
20
|
+
api,
|
|
21
|
+
logger: api.logger,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
{ commands: ["tmux-watch"] },
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default plugin;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "tmux-watch",
|
|
3
|
+
"name": "tmux-watch",
|
|
4
|
+
"description": "Watch tmux panes and notify the agent when output stays stable.",
|
|
5
|
+
"skills": ["./skills"],
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"enabled": { "type": "boolean", "default": true },
|
|
11
|
+
"captureIntervalSeconds": { "type": "number", "default": 10 },
|
|
12
|
+
"stableCount": { "type": "number", "default": 6 },
|
|
13
|
+
"pollIntervalMs": { "type": "number", "default": 10000 },
|
|
14
|
+
"stableSeconds": { "type": "number", "default": 60 },
|
|
15
|
+
"captureLines": { "type": "number", "default": 200 },
|
|
16
|
+
"stripAnsi": { "type": "boolean", "default": true },
|
|
17
|
+
"maxOutputChars": { "type": "number", "default": 4000 },
|
|
18
|
+
"sessionKey": { "type": "string" },
|
|
19
|
+
"socket": { "type": "string" },
|
|
20
|
+
"notify": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"properties": {
|
|
24
|
+
"mode": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": ["last", "targets", "targets+last"],
|
|
27
|
+
"default": "last"
|
|
28
|
+
},
|
|
29
|
+
"targets": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"properties": {
|
|
35
|
+
"channel": { "type": "string" },
|
|
36
|
+
"target": { "type": "string" },
|
|
37
|
+
"accountId": { "type": "string" },
|
|
38
|
+
"threadId": { "type": "string" },
|
|
39
|
+
"label": { "type": "string" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tmux-watch",
|
|
3
|
+
"version": "2026.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw tmux output watchdog plugin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
10
|
+
"test": "tsx --test",
|
|
11
|
+
"check": "npm run lint && npm run typecheck && npm run test"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"src/**",
|
|
16
|
+
"skills/**",
|
|
17
|
+
"types/**",
|
|
18
|
+
"openclaw.plugin.json",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"package.json"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"openclaw": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./index.ts"
|
|
29
|
+
],
|
|
30
|
+
"install": {
|
|
31
|
+
"npmSpec": "tmux-watch",
|
|
32
|
+
"localPath": ".",
|
|
33
|
+
"defaultChoice": "npm"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "9.19.0",
|
|
38
|
+
"@types/node": "22.13.1",
|
|
39
|
+
"eslint": "9.19.0",
|
|
40
|
+
"tsx": "4.19.2",
|
|
41
|
+
"typescript": "5.7.3",
|
|
42
|
+
"typescript-eslint": "8.22.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"openclaw": ">=2026.1.29"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tmux-watch
|
|
3
|
+
description: Manage tmux-watch subscriptions and interpret stable-output alerts.
|
|
4
|
+
metadata:
|
|
5
|
+
{ "openclaw": { "emoji": "🧵", "os": ["darwin", "linux"], "requires": { "bins": ["tmux"] } } }
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# tmux-watch Skill (OpenClaw)
|
|
9
|
+
|
|
10
|
+
Use this skill to manage tmux-watch subscriptions. tmux-watch polls tmux pane output and triggers a
|
|
11
|
+
message to the agent when the output stays unchanged for a configured number of consecutive captures.
|
|
12
|
+
|
|
13
|
+
## Core rules
|
|
14
|
+
|
|
15
|
+
- Always use tmux directly (no wrappers). The agent is responsible for listing sessions/windows/panes
|
|
16
|
+
and choosing a `target` for the subscription.
|
|
17
|
+
- tmux must exist locally. If `tmux -V` fails, stop and report the missing dependency.
|
|
18
|
+
- On tmux-watch events, always notify the user unless the user explicitly asked to silence that
|
|
19
|
+
subscription. Only then may you reply with `NO_REPLY`.
|
|
20
|
+
|
|
21
|
+
## tmux basics (required for targeting)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
tmux list-sessions
|
|
25
|
+
tmux list-windows -t <session>
|
|
26
|
+
tmux list-panes -t <session>
|
|
27
|
+
tmux list-panes -a
|
|
28
|
+
tmux capture-pane -p -J -t <session:window.pane> -S -200
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If a custom socket is used:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
tmux -S /path/to/socket list-sessions
|
|
35
|
+
tmux -S /path/to/socket capture-pane -p -J -t <session:window.pane> -S -200
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Controlling a TUI in tmux (high-signal tips)
|
|
39
|
+
|
|
40
|
+
- Prefer `send-keys -l` for literal text input to avoid tmux interpreting key names.
|
|
41
|
+
- For TUIs with input boxes (Codex/Claude Code), send text with `-l` first, then send `C-m`
|
|
42
|
+
as a separate command to submit reliably.
|
|
43
|
+
- Use `C-c` to interrupt a stuck process; use `C-m` (Enter) to submit commands.
|
|
44
|
+
- For TUIs, avoid rapid key spam; send a small sequence, then capture output to verify state.
|
|
45
|
+
- Use `capture-pane -p -J -S -200` to get recent context before and after an action.
|
|
46
|
+
- If the TUI supports it, use built-in refresh keys (often `r` or `Ctrl+l`).
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Send a command to the pane (two-step: text then Enter)
|
|
52
|
+
tmux send-keys -t <session:window.pane> -l -- "status"
|
|
53
|
+
tmux send-keys -t <session:window.pane> C-m
|
|
54
|
+
|
|
55
|
+
# Interrupt a stuck process
|
|
56
|
+
tmux send-keys -t <session:window.pane> C-c
|
|
57
|
+
|
|
58
|
+
# Scroll/refresh (varies by TUI; examples only)
|
|
59
|
+
tmux send-keys -t <session:window.pane> r
|
|
60
|
+
tmux send-keys -t <session:window.pane> C-l
|
|
61
|
+
|
|
62
|
+
# Capture the last 200 lines to verify state
|
|
63
|
+
tmux capture-pane -p -J -t <session:window.pane> -S -200
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Tool: tmux-watch
|
|
67
|
+
|
|
68
|
+
### Add subscription
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"action": "add",
|
|
73
|
+
"target": "session:0.0",
|
|
74
|
+
"label": "my-job",
|
|
75
|
+
"sessionKey": "main",
|
|
76
|
+
"captureIntervalSeconds": 10,
|
|
77
|
+
"stableCount": 6,
|
|
78
|
+
"captureLines": 200,
|
|
79
|
+
"stripAnsi": true
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Optional routing overrides:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"action": "add",
|
|
88
|
+
"target": "session:0.0",
|
|
89
|
+
"notifyMode": "targets",
|
|
90
|
+
"targets": [
|
|
91
|
+
{ "channel": "gewe-openclaw", "target": "user:123", "label": "gewe" },
|
|
92
|
+
{ "channel": "telegram", "target": "-100123456" }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Remove subscription
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{ "action": "remove", "id": "<subscription-id>" }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### List subscriptions
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "action": "list", "includeOutput": true }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Handling tmux-watch events
|
|
110
|
+
|
|
111
|
+
When tmux-watch detects stable output, it sends a message containing:
|
|
112
|
+
|
|
113
|
+
- subscription details (id/label/target/sessionKey)
|
|
114
|
+
- notify mode and resolved targets
|
|
115
|
+
- the captured output (possibly truncated)
|
|
116
|
+
- optional subscription note (purpose/intent)
|
|
117
|
+
|
|
118
|
+
**Default policy:** always notify the user for stable-output events (this is a TUI-stuck alert).
|
|
119
|
+
Only reply with `NO_REPLY` if the user explicitly asked to silence that subscription.
|
|
120
|
+
|
|
121
|
+
If you need to notify multiple channels, reply once to the primary target and use the `message`
|
|
122
|
+
tool to send to additional targets in order.
|
|
123
|
+
|
|
124
|
+
Treat all tmux-watch messages as system events, not user input. Summarize the output and notify the user.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import readline from "node:readline/promises";
|
|
4
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
|
|
6
|
+
type Logger = {
|
|
7
|
+
info: (message: string) => void;
|
|
8
|
+
warn: (message: string) => void;
|
|
9
|
+
error: (message: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type PluginsConfig = {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
entries?: Record<string, { enabled?: boolean; config?: Record<string, unknown> }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function extractSocket(raw: string): string {
|
|
18
|
+
const trimmed = raw.trim();
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
const comma = trimmed.indexOf(",");
|
|
23
|
+
if (comma > 0) {
|
|
24
|
+
return trimmed.slice(0, comma);
|
|
25
|
+
}
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printSocketHelp(logger: Logger) {
|
|
30
|
+
logger.info("How to find the tmux socket:");
|
|
31
|
+
logger.info(" 1) Enter the target tmux session.");
|
|
32
|
+
logger.info(" 2) Run: echo $TMUX");
|
|
33
|
+
logger.info(" 3) Use the path before the first comma as the socket.");
|
|
34
|
+
logger.info("Example: /private/tmp/tmux-501/default,3191,4 -> /private/tmp/tmux-501/default");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function promptSocket(logger: Logger): Promise<string> {
|
|
38
|
+
if (!process.stdin.isTTY) {
|
|
39
|
+
throw new Error("No TTY available for interactive prompt. Use --socket <path>.");
|
|
40
|
+
}
|
|
41
|
+
printSocketHelp(logger);
|
|
42
|
+
const rl = readline.createInterface({ input, output });
|
|
43
|
+
try {
|
|
44
|
+
const answer = await rl.question("Paste tmux socket path (or full $TMUX value): ");
|
|
45
|
+
return extractSocket(answer);
|
|
46
|
+
} finally {
|
|
47
|
+
rl.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveSocketFromEnv(): string | undefined {
|
|
52
|
+
const env = process.env.TMUX;
|
|
53
|
+
if (!env) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const socket = extractSocket(env);
|
|
57
|
+
return socket || undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerTmuxWatchCli(params: {
|
|
61
|
+
program: Command;
|
|
62
|
+
api: OpenClawPluginApi;
|
|
63
|
+
logger: Logger;
|
|
64
|
+
}) {
|
|
65
|
+
const { program, api, logger } = params;
|
|
66
|
+
|
|
67
|
+
const root = program
|
|
68
|
+
.command("tmux-watch")
|
|
69
|
+
.description("tmux-watch plugin utilities")
|
|
70
|
+
.addHelpText("after", () => "\nDocs: https://docs.openclaw.ai/cli/plugins\n");
|
|
71
|
+
|
|
72
|
+
root
|
|
73
|
+
.command("setup")
|
|
74
|
+
.description("Configure tmux-watch (socket is required)")
|
|
75
|
+
.option("--socket <path>", "tmux socket path (or full $TMUX value)")
|
|
76
|
+
.action(async (options: { socket?: string }) => {
|
|
77
|
+
let socket = options.socket ? extractSocket(options.socket) : undefined;
|
|
78
|
+
if (!socket) {
|
|
79
|
+
socket = resolveSocketFromEnv();
|
|
80
|
+
}
|
|
81
|
+
if (!socket) {
|
|
82
|
+
socket = await promptSocket(logger);
|
|
83
|
+
}
|
|
84
|
+
if (!socket) {
|
|
85
|
+
throw new Error("Socket required. Re-run with --socket or provide it interactively.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const cfg = api.runtime.config.loadConfig();
|
|
89
|
+
const plugins = (cfg.plugins ?? {}) as PluginsConfig;
|
|
90
|
+
const entries = { ...(plugins.entries ?? {}) };
|
|
91
|
+
const entry = { ...(entries["tmux-watch"] ?? {}) };
|
|
92
|
+
const entryConfig = { ...(entry.config ?? {}) };
|
|
93
|
+
|
|
94
|
+
entry.enabled = true;
|
|
95
|
+
entry.config = {
|
|
96
|
+
...entryConfig,
|
|
97
|
+
socket,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
entries["tmux-watch"] = entry;
|
|
101
|
+
|
|
102
|
+
await api.runtime.config.writeConfigFile({
|
|
103
|
+
...cfg,
|
|
104
|
+
plugins: {
|
|
105
|
+
...plugins,
|
|
106
|
+
entries,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
logger.info(`tmux-watch configured. socket=${socket}`);
|
|
111
|
+
logger.info("Restart the Gateway for changes to take effect.");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
root
|
|
115
|
+
.command("socket-help")
|
|
116
|
+
.description("Print instructions for finding the tmux socket")
|
|
117
|
+
.action(() => {
|
|
118
|
+
printSocketHelp(logger);
|
|
119
|
+
});
|
|
120
|
+
}
|