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/src/service.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type {
2
+ OpenClawPluginService,
3
+ OpenClawPluginServiceContext,
4
+ } from "openclaw/plugin-sdk";
5
+ import type { TmuxWatchManager } from "./manager.js";
6
+
7
+ export function createTmuxWatchService(manager: TmuxWatchManager): OpenClawPluginService {
8
+ return {
9
+ id: "tmux-watch",
10
+ async start(ctx: OpenClawPluginServiceContext) {
11
+ await manager.start(ctx);
12
+ },
13
+ async stop() {
14
+ await manager.stop();
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,189 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { stringEnum } from "openclaw/plugin-sdk";
3
+ import type { NotifyMode, NotifyTarget } from "./config.js";
4
+ import type { TmuxWatchManager, TmuxWatchSubscription } from "./manager.js";
5
+
6
+ const ACTIONS = ["add", "remove", "list"] as const;
7
+ const NOTIFY_MODES = ["last", "targets", "targets+last"] as const;
8
+
9
+ type ToolParams = {
10
+ action: (typeof ACTIONS)[number];
11
+ id?: string;
12
+ target?: string;
13
+ label?: string;
14
+ note?: string;
15
+ sessionKey?: string;
16
+ socket?: string;
17
+ captureIntervalSeconds?: number;
18
+ intervalMs?: number;
19
+ stableCount?: number;
20
+ stableSeconds?: number;
21
+ captureLines?: number;
22
+ stripAnsi?: boolean;
23
+ enabled?: boolean;
24
+ notifyMode?: NotifyMode;
25
+ targets?: NotifyTarget[];
26
+ includeOutput?: boolean;
27
+ };
28
+
29
+ type ToolResult = {
30
+ content: Array<{ type: string; text: string }>;
31
+ details: unknown;
32
+ };
33
+
34
+ function jsonResult(payload: unknown): ToolResult {
35
+ return {
36
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
37
+ details: payload,
38
+ };
39
+ }
40
+
41
+ function readString(value: unknown): string | undefined {
42
+ if (typeof value !== "string") {
43
+ return undefined;
44
+ }
45
+ const trimmed = value.trim();
46
+ return trimmed ? trimmed : undefined;
47
+ }
48
+
49
+ function normalizeTargets(raw: NotifyTarget[] | undefined): NotifyTarget[] | undefined {
50
+ if (!Array.isArray(raw) || raw.length === 0) {
51
+ return undefined;
52
+ }
53
+ const targets: NotifyTarget[] = [];
54
+ for (const entry of raw) {
55
+ const channel = readString(entry.channel);
56
+ const target = readString(entry.target);
57
+ if (!channel || !target) {
58
+ continue;
59
+ }
60
+ targets.push({
61
+ channel,
62
+ target,
63
+ accountId: readString(entry.accountId),
64
+ threadId: readString(entry.threadId),
65
+ label: readString(entry.label),
66
+ });
67
+ }
68
+ return targets.length > 0 ? targets : undefined;
69
+ }
70
+
71
+ export function createTmuxWatchTool(manager: TmuxWatchManager) {
72
+ return {
73
+ name: "tmux-watch",
74
+ description:
75
+ "Manage tmux-watch subscriptions (add/remove/list) that monitor tmux pane output.",
76
+ parameters: Type.Object(
77
+ {
78
+ action: stringEnum(ACTIONS, { description: `Action to perform: ${ACTIONS.join(", ")}` }),
79
+ id: Type.Optional(Type.String({ description: "Subscription id." })),
80
+ target: Type.Optional(Type.String({ description: "tmux target, e.g. session:0.0" })),
81
+ label: Type.Optional(Type.String({ description: "Human-friendly label." })),
82
+ note: Type.Optional(
83
+ Type.String({ description: "Purpose/intent note shown to the agent on alert." }),
84
+ ),
85
+ sessionKey: Type.Optional(Type.String({ description: "Session key override." })),
86
+ socket: Type.Optional(Type.String({ description: "tmux socket path (for -S)." })),
87
+ captureIntervalSeconds: Type.Optional(
88
+ Type.Number({ description: "Capture interval in seconds." }),
89
+ ),
90
+ intervalMs: Type.Optional(
91
+ Type.Number({ description: "Legacy: capture interval in ms." }),
92
+ ),
93
+ stableCount: Type.Optional(
94
+ Type.Number({ description: "Consecutive identical captures before alert." }),
95
+ ),
96
+ stableSeconds: Type.Optional(
97
+ Type.Number({ description: "Legacy: stable duration in seconds." }),
98
+ ),
99
+ captureLines: Type.Optional(Type.Number({ description: "Lines to capture." })),
100
+ stripAnsi: Type.Optional(Type.Boolean({ description: "Strip ANSI escape codes." })),
101
+ enabled: Type.Optional(Type.Boolean({ description: "Enable or disable subscription." })),
102
+ notifyMode: Type.Optional(
103
+ stringEnum(NOTIFY_MODES, { description: "Notify mode override." }),
104
+ ),
105
+ targets: Type.Optional(
106
+ Type.Array(
107
+ Type.Object(
108
+ {
109
+ channel: Type.String({ description: "Channel id (e.g. telegram, gewe)." }),
110
+ target: Type.String({ description: "Channel target id." }),
111
+ accountId: Type.Optional(Type.String({ description: "Provider account id." })),
112
+ threadId: Type.Optional(Type.String({ description: "Thread id." })),
113
+ label: Type.Optional(Type.String({ description: "Label for this target." })),
114
+ },
115
+ { additionalProperties: false },
116
+ ),
117
+ ),
118
+ ),
119
+ includeOutput: Type.Optional(
120
+ Type.Boolean({ description: "Include last captured output in list." }),
121
+ ),
122
+ },
123
+ { additionalProperties: false },
124
+ ),
125
+ async execute(_id: string, params: ToolParams): Promise<ToolResult> {
126
+ try {
127
+ switch (params.action) {
128
+ case "add": {
129
+ const target = readString(params.target);
130
+ if (!target) {
131
+ throw new Error("target required for add action");
132
+ }
133
+ const subscription: Partial<TmuxWatchSubscription> & { target: string } = {
134
+ id: readString(params.id),
135
+ target,
136
+ label: readString(params.label),
137
+ note: readString(params.note),
138
+ sessionKey: readString(params.sessionKey),
139
+ socket: readString(params.socket),
140
+ captureIntervalSeconds:
141
+ typeof params.captureIntervalSeconds === "number"
142
+ ? params.captureIntervalSeconds
143
+ : undefined,
144
+ intervalMs:
145
+ typeof params.intervalMs === "number" ? params.intervalMs : undefined,
146
+ stableCount:
147
+ typeof params.stableCount === "number" ? params.stableCount : undefined,
148
+ stableSeconds:
149
+ typeof params.stableSeconds === "number" ? params.stableSeconds : undefined,
150
+ captureLines:
151
+ typeof params.captureLines === "number" ? params.captureLines : undefined,
152
+ stripAnsi: typeof params.stripAnsi === "boolean" ? params.stripAnsi : undefined,
153
+ enabled: typeof params.enabled === "boolean" ? params.enabled : undefined,
154
+ notify:
155
+ params.notifyMode || params.targets
156
+ ? {
157
+ mode: params.notifyMode,
158
+ targets: normalizeTargets(params.targets),
159
+ }
160
+ : undefined,
161
+ };
162
+ const created = await manager.addSubscription(subscription);
163
+ return jsonResult({ ok: true, subscription: created });
164
+ }
165
+ case "remove": {
166
+ const id = readString(params.id);
167
+ if (!id) {
168
+ throw new Error("id required for remove action");
169
+ }
170
+ const removed = await manager.removeSubscription(id);
171
+ return jsonResult({ ok: removed });
172
+ }
173
+ case "list": {
174
+ const items = await manager.listSubscriptions({
175
+ includeOutput: params.includeOutput !== false,
176
+ });
177
+ return jsonResult({ ok: true, subscriptions: items });
178
+ }
179
+ default: {
180
+ params.action satisfies never;
181
+ throw new Error(`Unknown action: ${String(params.action)}`);
182
+ }
183
+ }
184
+ } catch (err) {
185
+ return jsonResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
186
+ }
187
+ },
188
+ };
189
+ }
@@ -0,0 +1,71 @@
1
+ declare module "openclaw/plugin-sdk" {
2
+ type TSchema = import("@sinclair/typebox").TSchema;
3
+ export type OpenClawConfig = {
4
+ session?: {
5
+ store?: unknown;
6
+ scope?: string;
7
+ mainKey?: string;
8
+ };
9
+ agents?: {
10
+ list?: Array<{ id?: string; default?: boolean }>;
11
+ };
12
+ } & Record<string, unknown>;
13
+
14
+ export type OpenClawPluginServiceContext = {
15
+ stateDir?: string;
16
+ };
17
+
18
+ export type OpenClawPluginService = {
19
+ id: string;
20
+ start?: (ctx: OpenClawPluginServiceContext) => Promise<void> | void;
21
+ stop?: () => Promise<void> | void;
22
+ };
23
+
24
+ export type OpenClawPluginApi = {
25
+ pluginConfig: unknown;
26
+ config: OpenClawConfig;
27
+ logger: {
28
+ info: (message: string) => void;
29
+ warn: (message: string) => void;
30
+ error: (message: string) => void;
31
+ };
32
+ registerTool: (tool: unknown) => void;
33
+ registerService: (service: OpenClawPluginService) => void;
34
+ registerCli: (handler: (ctx: { program: unknown }) => void, opts?: { commands?: string[] }) => void;
35
+ runtime: {
36
+ state: {
37
+ resolveStateDir: () => string;
38
+ };
39
+ system: {
40
+ runCommandWithTimeout: (
41
+ argv: string[],
42
+ opts: { timeoutMs: number },
43
+ ) => Promise<{ code: number; stdout?: string; stderr?: string }>;
44
+ };
45
+ channel: {
46
+ reply: {
47
+ dispatchReplyWithBufferedBlockDispatcher: (args: {
48
+ ctx: unknown;
49
+ cfg: OpenClawConfig;
50
+ dispatcherOptions: {
51
+ deliver: () => Promise<void> | void;
52
+ onError: (err: unknown) => void;
53
+ };
54
+ }) => Promise<void>;
55
+ };
56
+ session: {
57
+ resolveStorePath: (store: unknown, opts: { agentId: string }) => string;
58
+ };
59
+ };
60
+ config: {
61
+ loadConfig: () => Record<string, unknown>;
62
+ writeConfigFile: (cfg: Record<string, unknown>) => Promise<void>;
63
+ };
64
+ };
65
+ };
66
+
67
+ export function stringEnum<T extends readonly string[]>(
68
+ values: T,
69
+ opts?: { description?: string },
70
+ ): TSchema;
71
+ }