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 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
+ }
@@ -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
+ }