tmux-agent-monitor 0.0.1 → 0.0.4

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.
@@ -0,0 +1,357 @@
1
+ import path from "node:path";
2
+ import { z } from "zod";
3
+
4
+ //#region packages/shared/src/constants.ts
5
+ const allowedKeys = [
6
+ "Enter",
7
+ "Escape",
8
+ "Tab",
9
+ "BTab",
10
+ "C-Tab",
11
+ "C-BTab",
12
+ "Space",
13
+ "BSpace",
14
+ "Up",
15
+ "Down",
16
+ "Left",
17
+ "Right",
18
+ "C-Up",
19
+ "C-Down",
20
+ "C-Left",
21
+ "C-Right",
22
+ "C-Enter",
23
+ "C-Escape",
24
+ "Home",
25
+ "End",
26
+ "PageUp",
27
+ "PageDown",
28
+ "C-c",
29
+ "C-d",
30
+ "C-z",
31
+ "C-\\",
32
+ "C-u",
33
+ "C-k",
34
+ "F1",
35
+ "F2",
36
+ "F3",
37
+ "F4",
38
+ "F5",
39
+ "F6",
40
+ "F7",
41
+ "F8",
42
+ "F9",
43
+ "F10",
44
+ "F11",
45
+ "F12"
46
+ ];
47
+ const defaultDangerKeys = [
48
+ "C-c",
49
+ "C-d",
50
+ "C-z",
51
+ "C-\\",
52
+ "F12"
53
+ ];
54
+ const defaultDangerCommandPatterns = [
55
+ "rm\\s+(-rf?|--recursive)",
56
+ "sudo\\s+rm",
57
+ "mkfs",
58
+ "dd\\s+if=",
59
+ ">\\s*/dev/",
60
+ "chmod\\s+777",
61
+ "curl.*\\|\\s*(ba)?sh",
62
+ "wget.*\\|\\s*(ba)?sh"
63
+ ];
64
+ const defaultConfig = {
65
+ bind: "127.0.0.1",
66
+ port: 11080,
67
+ token: "",
68
+ readOnly: false,
69
+ attachOnServe: true,
70
+ staticAuth: false,
71
+ allowedOrigins: [],
72
+ rateLimit: {
73
+ send: {
74
+ windowMs: 1e3,
75
+ max: 10
76
+ },
77
+ screen: {
78
+ windowMs: 1e3,
79
+ max: 10
80
+ }
81
+ },
82
+ dangerKeys: [...defaultDangerKeys],
83
+ dangerCommandPatterns: [...defaultDangerCommandPatterns],
84
+ activity: {
85
+ pollIntervalMs: 1e3,
86
+ runningThresholdMs: 15e3,
87
+ inactiveThresholdMs: 6e4
88
+ },
89
+ hooks: {
90
+ ttyCacheTtlMs: 6e4,
91
+ ttyCacheMax: 200
92
+ },
93
+ input: {
94
+ maxTextLength: 2e3,
95
+ enterKey: "C-m",
96
+ enterDelayMs: 100
97
+ },
98
+ screen: {
99
+ mode: "text",
100
+ defaultLines: 2e3,
101
+ maxLines: 2e3,
102
+ joinLines: false,
103
+ ansi: true,
104
+ altScreen: "auto",
105
+ image: {
106
+ enabled: true,
107
+ backend: "terminal",
108
+ format: "png",
109
+ cropPane: true,
110
+ timeoutMs: 5e3
111
+ }
112
+ },
113
+ logs: {
114
+ maxPaneLogBytes: 2e6,
115
+ maxEventLogBytes: 2e6,
116
+ retainRotations: 5
117
+ },
118
+ tmux: {
119
+ socketName: null,
120
+ socketPath: null,
121
+ primaryClient: null
122
+ }
123
+ };
124
+
125
+ //#endregion
126
+ //#region packages/shared/src/danger.ts
127
+ const normalizeCommandLines = (text) => {
128
+ return text.replace(/\r\n/g, "\n").split("\n").map((line) => line.toLowerCase().replace(/\s+/g, " ").trim()).filter((line) => line.length > 0);
129
+ };
130
+ const compileDangerPatterns = (patterns) => {
131
+ return patterns.map((pattern) => new RegExp(pattern, "i"));
132
+ };
133
+ const isDangerousCommand = (text, patterns) => {
134
+ return normalizeCommandLines(text).some((line) => patterns.some((pattern) => pattern.test(line)));
135
+ };
136
+
137
+ //#endregion
138
+ //#region packages/shared/src/paths.ts
139
+ const encodePaneId = (paneId) => {
140
+ return encodeURIComponent(paneId);
141
+ };
142
+ const sanitizeServerKey = (value) => {
143
+ return value.replace(/\//g, "_").replace(/[^a-zA-Z0-9_-]/g, "-");
144
+ };
145
+ const resolveServerKey = (socketName, socketPath) => {
146
+ if (socketName && socketName.trim().length > 0) return sanitizeServerKey(socketName);
147
+ if (socketPath && socketPath.trim().length > 0) return sanitizeServerKey(socketPath);
148
+ return "default";
149
+ };
150
+ const resolveLogPaths = (baseDir, serverKey, paneId) => {
151
+ const paneIdEncoded = encodePaneId(paneId);
152
+ const panesDir = path.join(baseDir, "panes", serverKey);
153
+ const eventsDir = path.join(baseDir, "events", serverKey);
154
+ return {
155
+ paneIdEncoded,
156
+ panesDir,
157
+ eventsDir,
158
+ paneLogPath: path.join(panesDir, `${paneIdEncoded}.log`),
159
+ eventLogPath: path.join(eventsDir, "claude.jsonl")
160
+ };
161
+ };
162
+
163
+ //#endregion
164
+ //#region packages/shared/src/schemas.ts
165
+ const sessionStateSchema = z.enum([
166
+ "RUNNING",
167
+ "WAITING_INPUT",
168
+ "WAITING_PERMISSION",
169
+ "UNKNOWN"
170
+ ]);
171
+ const allowedKeySchema = z.enum(allowedKeys);
172
+ const apiErrorSchema = z.object({
173
+ code: z.enum([
174
+ "INVALID_PANE",
175
+ "INVALID_PAYLOAD",
176
+ "DANGEROUS_COMMAND",
177
+ "READ_ONLY",
178
+ "NOT_FOUND",
179
+ "TMUX_UNAVAILABLE",
180
+ "RATE_LIMIT",
181
+ "INTERNAL"
182
+ ]),
183
+ message: z.string()
184
+ });
185
+ const screenResponseSchema = z.object({
186
+ ok: z.boolean(),
187
+ paneId: z.string(),
188
+ mode: z.enum(["text", "image"]),
189
+ capturedAt: z.string(),
190
+ lines: z.number().optional(),
191
+ truncated: z.boolean().nullable().optional(),
192
+ alternateOn: z.boolean().optional(),
193
+ screen: z.string().optional(),
194
+ imageBase64: z.string().optional(),
195
+ cropped: z.boolean().optional(),
196
+ fallbackReason: z.enum(["image_failed", "image_disabled"]).optional(),
197
+ error: apiErrorSchema.optional()
198
+ });
199
+ const commandResponseSchema = z.object({
200
+ ok: z.boolean(),
201
+ error: apiErrorSchema.optional()
202
+ });
203
+ const sessionSummarySchema = z.object({
204
+ paneId: z.string(),
205
+ sessionName: z.string(),
206
+ windowIndex: z.number(),
207
+ paneIndex: z.number(),
208
+ windowActivity: z.number().nullable(),
209
+ paneActive: z.boolean(),
210
+ currentCommand: z.string().nullable(),
211
+ currentPath: z.string().nullable(),
212
+ paneTty: z.string().nullable(),
213
+ title: z.string().nullable(),
214
+ agent: z.enum([
215
+ "codex",
216
+ "claude",
217
+ "unknown"
218
+ ]),
219
+ state: sessionStateSchema,
220
+ stateReason: z.string(),
221
+ lastMessage: z.string().nullable(),
222
+ lastOutputAt: z.string().nullable(),
223
+ lastEventAt: z.string().nullable(),
224
+ paneDead: z.boolean(),
225
+ alternateOn: z.boolean(),
226
+ pipeAttached: z.boolean(),
227
+ pipeConflict: z.boolean()
228
+ });
229
+ const sessionDetailSchema = sessionSummarySchema.extend({
230
+ startCommand: z.string().nullable(),
231
+ panePid: z.number().nullable()
232
+ });
233
+ const wsEnvelopeSchema = (typeSchema, dataSchema) => z.object({
234
+ type: typeSchema,
235
+ ts: z.string(),
236
+ reqId: z.string().optional(),
237
+ data: dataSchema
238
+ });
239
+ const wsClientMessageSchema = z.discriminatedUnion("type", [
240
+ wsEnvelopeSchema(z.literal("screen.request"), z.object({
241
+ paneId: z.string(),
242
+ lines: z.number().optional(),
243
+ mode: z.enum(["text", "image"]).optional()
244
+ })),
245
+ wsEnvelopeSchema(z.literal("send.text"), z.object({
246
+ paneId: z.string(),
247
+ text: z.string(),
248
+ enter: z.boolean().optional()
249
+ })),
250
+ wsEnvelopeSchema(z.literal("send.keys"), z.object({
251
+ paneId: z.string(),
252
+ keys: z.array(allowedKeySchema)
253
+ })),
254
+ wsEnvelopeSchema(z.literal("client.ping"), z.object({}).strict())
255
+ ]);
256
+ const wsServerMessageSchema = z.discriminatedUnion("type", [
257
+ wsEnvelopeSchema(z.literal("sessions.snapshot"), z.object({ sessions: z.array(sessionSummarySchema) })),
258
+ wsEnvelopeSchema(z.literal("session.updated"), z.object({ session: sessionSummarySchema })),
259
+ wsEnvelopeSchema(z.literal("session.removed"), z.object({ paneId: z.string() })),
260
+ wsEnvelopeSchema(z.literal("server.health"), z.object({ version: z.string() })),
261
+ wsEnvelopeSchema(z.literal("screen.response"), screenResponseSchema),
262
+ wsEnvelopeSchema(z.literal("command.response"), commandResponseSchema)
263
+ ]);
264
+ const claudeHookEventSchema = z.object({
265
+ ts: z.string(),
266
+ hook_event_name: z.enum([
267
+ "PreToolUse",
268
+ "PostToolUse",
269
+ "Notification",
270
+ "Stop",
271
+ "UserPromptSubmit"
272
+ ]),
273
+ notification_type: z.enum(["permission_prompt"]).optional(),
274
+ session_id: z.string(),
275
+ cwd: z.string().optional(),
276
+ tty: z.string().optional(),
277
+ tmux_pane: z.string().nullable().optional(),
278
+ transcript_path: z.string().optional(),
279
+ fallback: z.object({
280
+ cwd: z.string().optional(),
281
+ transcript_path: z.string().optional()
282
+ }).optional(),
283
+ payload: z.object({ raw: z.string() })
284
+ });
285
+ const configSchema = z.object({
286
+ bind: z.enum(["127.0.0.1", "0.0.0.0"]),
287
+ port: z.number(),
288
+ token: z.string(),
289
+ readOnly: z.boolean(),
290
+ attachOnServe: z.boolean(),
291
+ staticAuth: z.boolean(),
292
+ allowedOrigins: z.array(z.string()),
293
+ rateLimit: z.object({
294
+ send: z.object({
295
+ windowMs: z.number(),
296
+ max: z.number()
297
+ }),
298
+ screen: z.object({
299
+ windowMs: z.number(),
300
+ max: z.number()
301
+ })
302
+ }),
303
+ dangerKeys: z.array(z.string()),
304
+ dangerCommandPatterns: z.array(z.string()),
305
+ activity: z.object({
306
+ pollIntervalMs: z.number(),
307
+ runningThresholdMs: z.number(),
308
+ inactiveThresholdMs: z.number()
309
+ }),
310
+ hooks: z.object({
311
+ ttyCacheTtlMs: z.number(),
312
+ ttyCacheMax: z.number()
313
+ }),
314
+ input: z.object({
315
+ maxTextLength: z.number(),
316
+ enterKey: z.string().default("C-m"),
317
+ enterDelayMs: z.number().default(100)
318
+ }),
319
+ screen: z.object({
320
+ mode: z.enum(["text", "image"]),
321
+ defaultLines: z.number(),
322
+ maxLines: z.number(),
323
+ joinLines: z.boolean(),
324
+ ansi: z.boolean().default(true),
325
+ altScreen: z.enum([
326
+ "auto",
327
+ "on",
328
+ "off"
329
+ ]),
330
+ image: z.object({
331
+ enabled: z.boolean(),
332
+ backend: z.enum([
333
+ "alacritty",
334
+ "terminal",
335
+ "iterm",
336
+ "wezterm",
337
+ "ghostty"
338
+ ]),
339
+ format: z.enum(["png"]),
340
+ cropPane: z.boolean(),
341
+ timeoutMs: z.number()
342
+ })
343
+ }),
344
+ logs: z.object({
345
+ maxPaneLogBytes: z.number(),
346
+ maxEventLogBytes: z.number(),
347
+ retainRotations: z.number()
348
+ }),
349
+ tmux: z.object({
350
+ socketName: z.string().nullable(),
351
+ socketPath: z.string().nullable(),
352
+ primaryClient: z.string().nullable()
353
+ })
354
+ });
355
+
356
+ //#endregion
357
+ export { resolveServerKey as a, allowedKeys as c, resolveLogPaths as i, defaultConfig as l, configSchema as n, compileDangerPatterns as o, wsClientMessageSchema as r, isDangerousCommand as s, claudeHookEventSchema as t };
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { a as resolveServerKey } from "./src-CFxYk-pF.mjs";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ //#region packages/hooks/src/cli.ts
8
+ const readStdin = () => {
9
+ try {
10
+ return fs.readFileSync(0, "utf8");
11
+ } catch {
12
+ return "";
13
+ }
14
+ };
15
+ const encodeClaudeCwd = (cwd) => {
16
+ return cwd.replace(/[/.]/g, "-");
17
+ };
18
+ const resolveTranscriptPath = (cwd, sessionId) => {
19
+ if (!cwd || !sessionId) return null;
20
+ const encoded = encodeClaudeCwd(cwd);
21
+ return path.join(os.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
22
+ };
23
+ const loadConfig = () => {
24
+ const configPath = path.join(os.homedir(), ".tmux-agent-monitor", "config.json");
25
+ try {
26
+ const raw = fs.readFileSync(configPath, "utf8");
27
+ return JSON.parse(raw);
28
+ } catch {
29
+ return null;
30
+ }
31
+ };
32
+ const ensureDir = (dir) => {
33
+ fs.mkdirSync(dir, {
34
+ recursive: true,
35
+ mode: 448
36
+ });
37
+ };
38
+ const main = () => {
39
+ const hookEventName = process.argv[2];
40
+ if (!hookEventName) {
41
+ console.error("Usage: tmux-agent-monitor-hook <HookEventName>");
42
+ process.exit(1);
43
+ }
44
+ const rawInput = readStdin().trim();
45
+ if (!rawInput) process.exit(0);
46
+ let payload = {};
47
+ try {
48
+ payload = JSON.parse(rawInput);
49
+ } catch {
50
+ console.error("Invalid JSON payload");
51
+ process.exit(1);
52
+ }
53
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
54
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : void 0;
55
+ const tty = typeof payload.tty === "string" ? payload.tty : void 0;
56
+ const tmuxPane = typeof payload.tmux_pane === "string" ? payload.tmux_pane : process.env.TMUX_PANE ?? null;
57
+ const notificationType = typeof payload.notification_type === "string" ? payload.notification_type : void 0;
58
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : resolveTranscriptPath(cwd, sessionId);
59
+ const event = {
60
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
61
+ hook_event_name: hookEventName,
62
+ notification_type: notificationType,
63
+ session_id: sessionId ?? "",
64
+ cwd,
65
+ tty,
66
+ tmux_pane: tmuxPane ?? null,
67
+ transcript_path: transcriptPath ?? void 0,
68
+ fallback: tmuxPane === null ? {
69
+ cwd,
70
+ transcript_path: transcriptPath ?? void 0
71
+ } : void 0,
72
+ payload: { raw: rawInput }
73
+ };
74
+ const config = loadConfig();
75
+ const serverKey = resolveServerKey(config?.tmux?.socketName ?? null, config?.tmux?.socketPath ?? null);
76
+ const baseDir = path.join(os.homedir(), ".tmux-agent-monitor");
77
+ const eventsDir = path.join(baseDir, "events", serverKey);
78
+ const eventsPath = path.join(eventsDir, "claude.jsonl");
79
+ ensureDir(eventsDir);
80
+ fs.appendFileSync(eventsPath, `${JSON.stringify(event)}\n`, "utf8");
81
+ };
82
+ main();
83
+
84
+ //#endregion
85
+ export { };