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.
- package/LICENSE +21 -0
- package/README.md +97 -28
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/index.js +2575 -0
- package/dist/src-CFxYk-pF.mjs +357 -0
- package/dist/tmux-agent-monitor-hook.js +85 -0
- package/dist/web/assets/index-CITyEb9q.js +55 -0
- package/dist/web/assets/index-DQAPK1tz.css +1 -0
- package/dist/web/index.html +19 -0
- package/package.json +74 -7
|
@@ -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 { };
|