tmux-agent-monitor 0.0.1 → 0.0.3

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,369 @@
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 decodePaneId = (paneIdEncoded) => {
143
+ try {
144
+ const decoded = decodeURIComponent(paneIdEncoded);
145
+ for (let i = 0; i < decoded.length; i += 1) {
146
+ const code = decoded.charCodeAt(i);
147
+ if (code < 32 || code === 127) return paneIdEncoded;
148
+ }
149
+ return decoded;
150
+ } catch {
151
+ return paneIdEncoded;
152
+ }
153
+ };
154
+ const sanitizeServerKey = (value) => {
155
+ return value.replace(/\//g, "_").replace(/[^a-zA-Z0-9_-]/g, "-");
156
+ };
157
+ const resolveServerKey = (socketName, socketPath) => {
158
+ if (socketName && socketName.trim().length > 0) return sanitizeServerKey(socketName);
159
+ if (socketPath && socketPath.trim().length > 0) return sanitizeServerKey(socketPath);
160
+ return "default";
161
+ };
162
+ const resolveLogPaths = (baseDir, serverKey, paneId) => {
163
+ const paneIdEncoded = encodePaneId(paneId);
164
+ const panesDir = path.join(baseDir, "panes", serverKey);
165
+ const eventsDir = path.join(baseDir, "events", serverKey);
166
+ return {
167
+ paneIdEncoded,
168
+ panesDir,
169
+ eventsDir,
170
+ paneLogPath: path.join(panesDir, `${paneIdEncoded}.log`),
171
+ eventLogPath: path.join(eventsDir, "claude.jsonl")
172
+ };
173
+ };
174
+
175
+ //#endregion
176
+ //#region packages/shared/src/schemas.ts
177
+ const sessionStateSchema = z.enum([
178
+ "RUNNING",
179
+ "WAITING_INPUT",
180
+ "WAITING_PERMISSION",
181
+ "UNKNOWN"
182
+ ]);
183
+ const allowedKeySchema = z.enum(allowedKeys);
184
+ const apiErrorSchema = z.object({
185
+ code: z.enum([
186
+ "INVALID_PANE",
187
+ "INVALID_PAYLOAD",
188
+ "DANGEROUS_COMMAND",
189
+ "READ_ONLY",
190
+ "NOT_FOUND",
191
+ "TMUX_UNAVAILABLE",
192
+ "RATE_LIMIT",
193
+ "INTERNAL"
194
+ ]),
195
+ message: z.string()
196
+ });
197
+ const screenResponseSchema = z.object({
198
+ ok: z.boolean(),
199
+ paneId: z.string(),
200
+ mode: z.enum(["text", "image"]),
201
+ capturedAt: z.string(),
202
+ lines: z.number().optional(),
203
+ truncated: z.boolean().nullable().optional(),
204
+ alternateOn: z.boolean().optional(),
205
+ screen: z.string().optional(),
206
+ imageBase64: z.string().optional(),
207
+ cropped: z.boolean().optional(),
208
+ fallbackReason: z.enum(["image_failed", "image_disabled"]).optional(),
209
+ error: apiErrorSchema.optional()
210
+ });
211
+ const commandResponseSchema = z.object({
212
+ ok: z.boolean(),
213
+ error: apiErrorSchema.optional()
214
+ });
215
+ const sessionSummarySchema = z.object({
216
+ paneId: z.string(),
217
+ sessionName: z.string(),
218
+ windowIndex: z.number(),
219
+ paneIndex: z.number(),
220
+ windowActivity: z.number().nullable(),
221
+ paneActive: z.boolean(),
222
+ currentCommand: z.string().nullable(),
223
+ currentPath: z.string().nullable(),
224
+ paneTty: z.string().nullable(),
225
+ title: z.string().nullable(),
226
+ agent: z.enum([
227
+ "codex",
228
+ "claude",
229
+ "unknown"
230
+ ]),
231
+ state: sessionStateSchema,
232
+ stateReason: z.string(),
233
+ lastMessage: z.string().nullable(),
234
+ lastOutputAt: z.string().nullable(),
235
+ lastEventAt: z.string().nullable(),
236
+ paneDead: z.boolean(),
237
+ alternateOn: z.boolean(),
238
+ pipeAttached: z.boolean(),
239
+ pipeConflict: z.boolean()
240
+ });
241
+ const sessionDetailSchema = sessionSummarySchema.extend({
242
+ startCommand: z.string().nullable(),
243
+ panePid: z.number().nullable()
244
+ });
245
+ const wsEnvelopeSchema = (typeSchema, dataSchema) => z.object({
246
+ type: typeSchema,
247
+ ts: z.string(),
248
+ reqId: z.string().optional(),
249
+ data: dataSchema
250
+ });
251
+ const wsClientMessageSchema = z.discriminatedUnion("type", [
252
+ wsEnvelopeSchema(z.literal("screen.request"), z.object({
253
+ paneId: z.string(),
254
+ lines: z.number().optional(),
255
+ mode: z.enum(["text", "image"]).optional()
256
+ })),
257
+ wsEnvelopeSchema(z.literal("send.text"), z.object({
258
+ paneId: z.string(),
259
+ text: z.string(),
260
+ enter: z.boolean().optional()
261
+ })),
262
+ wsEnvelopeSchema(z.literal("send.keys"), z.object({
263
+ paneId: z.string(),
264
+ keys: z.array(allowedKeySchema)
265
+ })),
266
+ wsEnvelopeSchema(z.literal("client.ping"), z.object({}).strict())
267
+ ]);
268
+ const wsServerMessageSchema = z.discriminatedUnion("type", [
269
+ wsEnvelopeSchema(z.literal("sessions.snapshot"), z.object({ sessions: z.array(sessionSummarySchema) })),
270
+ wsEnvelopeSchema(z.literal("session.updated"), z.object({ session: sessionSummarySchema })),
271
+ wsEnvelopeSchema(z.literal("session.removed"), z.object({ paneId: z.string() })),
272
+ wsEnvelopeSchema(z.literal("server.health"), z.object({ version: z.string() })),
273
+ wsEnvelopeSchema(z.literal("screen.response"), screenResponseSchema),
274
+ wsEnvelopeSchema(z.literal("command.response"), commandResponseSchema)
275
+ ]);
276
+ const claudeHookEventSchema = z.object({
277
+ ts: z.string(),
278
+ hook_event_name: z.enum([
279
+ "PreToolUse",
280
+ "PostToolUse",
281
+ "Notification",
282
+ "Stop",
283
+ "UserPromptSubmit"
284
+ ]),
285
+ notification_type: z.enum(["permission_prompt"]).optional(),
286
+ session_id: z.string(),
287
+ cwd: z.string().optional(),
288
+ tty: z.string().optional(),
289
+ tmux_pane: z.string().nullable().optional(),
290
+ transcript_path: z.string().optional(),
291
+ fallback: z.object({
292
+ cwd: z.string().optional(),
293
+ transcript_path: z.string().optional()
294
+ }).optional(),
295
+ payload: z.object({ raw: z.string() })
296
+ });
297
+ const configSchema = z.object({
298
+ bind: z.enum(["127.0.0.1", "0.0.0.0"]),
299
+ port: z.number(),
300
+ token: z.string(),
301
+ readOnly: z.boolean(),
302
+ attachOnServe: z.boolean(),
303
+ staticAuth: z.boolean(),
304
+ allowedOrigins: z.array(z.string()),
305
+ rateLimit: z.object({
306
+ send: z.object({
307
+ windowMs: z.number(),
308
+ max: z.number()
309
+ }),
310
+ screen: z.object({
311
+ windowMs: z.number(),
312
+ max: z.number()
313
+ })
314
+ }),
315
+ dangerKeys: z.array(z.string()),
316
+ dangerCommandPatterns: z.array(z.string()),
317
+ activity: z.object({
318
+ pollIntervalMs: z.number(),
319
+ runningThresholdMs: z.number(),
320
+ inactiveThresholdMs: z.number()
321
+ }),
322
+ hooks: z.object({
323
+ ttyCacheTtlMs: z.number(),
324
+ ttyCacheMax: z.number()
325
+ }),
326
+ input: z.object({
327
+ maxTextLength: z.number(),
328
+ enterKey: z.string().default("C-m"),
329
+ enterDelayMs: z.number().default(100)
330
+ }),
331
+ screen: z.object({
332
+ mode: z.enum(["text", "image"]),
333
+ defaultLines: z.number(),
334
+ maxLines: z.number(),
335
+ joinLines: z.boolean(),
336
+ ansi: z.boolean().default(true),
337
+ altScreen: z.enum([
338
+ "auto",
339
+ "on",
340
+ "off"
341
+ ]),
342
+ image: z.object({
343
+ enabled: z.boolean(),
344
+ backend: z.enum([
345
+ "alacritty",
346
+ "terminal",
347
+ "iterm",
348
+ "wezterm",
349
+ "ghostty"
350
+ ]),
351
+ format: z.enum(["png"]),
352
+ cropPane: z.boolean(),
353
+ timeoutMs: z.number()
354
+ })
355
+ }),
356
+ logs: z.object({
357
+ maxPaneLogBytes: z.number(),
358
+ maxEventLogBytes: z.number(),
359
+ retainRotations: z.number()
360
+ }),
361
+ tmux: z.object({
362
+ socketName: z.string().nullable(),
363
+ socketPath: z.string().nullable(),
364
+ primaryClient: z.string().nullable()
365
+ })
366
+ });
367
+
368
+ //#endregion
369
+ export { resolveLogPaths as a, isDangerousCommand as c, decodePaneId as i, allowedKeys as l, configSchema as n, resolveServerKey as o, wsClientMessageSchema as r, compileDangerPatterns as s, claudeHookEventSchema as t, defaultConfig as u };
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { o as resolveServerKey } from "./src-C_y43gN3.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 { };