@preziosiraffaele/agent-watch 0.2.0
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/bin/agent-watchd +2 -0
- package/bin/aw +5 -0
- package/dist/agent-watchd.js +603 -0
- package/dist/aw.js +780 -0
- package/package.json +43 -0
package/dist/aw.js
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/agent-core/src/protocol.ts
|
|
3
|
+
var DEFAULT_DAEMON_HOST = "127.0.0.1";
|
|
4
|
+
var DEFAULT_DAEMON_PORT = 3847;
|
|
5
|
+
var DEFAULT_DAEMON_URL = `http://${DEFAULT_DAEMON_HOST}:${DEFAULT_DAEMON_PORT}`;
|
|
6
|
+
var ENV = {
|
|
7
|
+
LAUNCH_ID: "AGENT_WATCH_LAUNCH_ID",
|
|
8
|
+
DAEMON_URL: "AGENT_WATCH_DAEMON_URL",
|
|
9
|
+
DAEMON_PORT: "AGENT_WATCH_DAEMON_PORT",
|
|
10
|
+
DAEMON_HOST: "AGENT_WATCH_DAEMON_HOST"
|
|
11
|
+
};
|
|
12
|
+
var SUPPORTED_AGENTS = ["claude", "codex", "agent"];
|
|
13
|
+
function isSupportedAgent(name) {
|
|
14
|
+
return SUPPORTED_AGENTS.includes(name);
|
|
15
|
+
}
|
|
16
|
+
function daemonUrlFromEnv(env) {
|
|
17
|
+
const direct = env[ENV.DAEMON_URL];
|
|
18
|
+
if (direct)
|
|
19
|
+
return direct;
|
|
20
|
+
const port = env[ENV.DAEMON_PORT] ?? String(DEFAULT_DAEMON_PORT);
|
|
21
|
+
const host = env[ENV.DAEMON_HOST] ?? DEFAULT_DAEMON_HOST;
|
|
22
|
+
return `http://${host}:${port}`;
|
|
23
|
+
}
|
|
24
|
+
// packages/agent-core/src/states.ts
|
|
25
|
+
var STATES = {
|
|
26
|
+
SESSION_STARTED: "session_started",
|
|
27
|
+
WORKING: "working",
|
|
28
|
+
RUNNING_TOOL: "running_tool",
|
|
29
|
+
RUNNING_SHELL: "running_shell",
|
|
30
|
+
NEEDS_APPROVAL: "needs_approval",
|
|
31
|
+
EDITED_FILE: "edited_file",
|
|
32
|
+
WAITING: "waiting",
|
|
33
|
+
IDLE: "idle",
|
|
34
|
+
STALE: "stale",
|
|
35
|
+
EXITED: "exited",
|
|
36
|
+
FAILED: "failed"
|
|
37
|
+
};
|
|
38
|
+
var STALE_AFTER_MS = 30 * 60 * 1000;
|
|
39
|
+
// packages/agent-core/src/events.ts
|
|
40
|
+
var CLAUDE_STATE = {
|
|
41
|
+
SessionStart: STATES.SESSION_STARTED,
|
|
42
|
+
UserPromptSubmit: STATES.WORKING,
|
|
43
|
+
UserPromptExpansion: STATES.WORKING,
|
|
44
|
+
PreToolUse: STATES.RUNNING_TOOL,
|
|
45
|
+
PermissionRequest: STATES.NEEDS_APPROVAL,
|
|
46
|
+
PermissionDenied: STATES.WORKING,
|
|
47
|
+
PostToolUse: STATES.WORKING,
|
|
48
|
+
PostToolUseFailure: STATES.WORKING,
|
|
49
|
+
PostToolBatch: STATES.WORKING,
|
|
50
|
+
Notification: STATES.WAITING,
|
|
51
|
+
SubagentStart: STATES.RUNNING_TOOL,
|
|
52
|
+
SubagentStop: STATES.IDLE,
|
|
53
|
+
TaskCreated: STATES.RUNNING_TOOL,
|
|
54
|
+
TaskCompleted: STATES.WORKING,
|
|
55
|
+
Stop: STATES.IDLE,
|
|
56
|
+
StopFailure: STATES.IDLE,
|
|
57
|
+
SessionEnd: STATES.IDLE
|
|
58
|
+
};
|
|
59
|
+
var CODEX_STATE = {
|
|
60
|
+
SessionStart: STATES.SESSION_STARTED,
|
|
61
|
+
UserPromptSubmit: STATES.WORKING,
|
|
62
|
+
PreToolUse: STATES.RUNNING_TOOL,
|
|
63
|
+
PermissionRequest: STATES.NEEDS_APPROVAL,
|
|
64
|
+
PostToolUse: STATES.WORKING,
|
|
65
|
+
Stop: STATES.IDLE
|
|
66
|
+
};
|
|
67
|
+
var AGENT_STATE = {
|
|
68
|
+
sessionStart: STATES.SESSION_STARTED,
|
|
69
|
+
beforeSubmitPrompt: STATES.WORKING,
|
|
70
|
+
preToolUse: STATES.RUNNING_TOOL,
|
|
71
|
+
beforeShellExecution: STATES.RUNNING_SHELL,
|
|
72
|
+
afterShellExecution: STATES.WORKING,
|
|
73
|
+
beforeMCPExecution: STATES.RUNNING_TOOL,
|
|
74
|
+
afterMCPExecution: STATES.WORKING,
|
|
75
|
+
beforeReadFile: STATES.RUNNING_TOOL,
|
|
76
|
+
afterFileEdit: STATES.EDITED_FILE,
|
|
77
|
+
postToolUse: STATES.WORKING,
|
|
78
|
+
postToolUseFailure: STATES.WORKING,
|
|
79
|
+
subagentStart: STATES.RUNNING_TOOL,
|
|
80
|
+
subagentStop: STATES.WORKING,
|
|
81
|
+
stop: STATES.IDLE,
|
|
82
|
+
sessionEnd: STATES.IDLE
|
|
83
|
+
};
|
|
84
|
+
var CLAUDE_EVENTS = Object.keys(CLAUDE_STATE);
|
|
85
|
+
var CODEX_EVENTS = Object.keys(CODEX_STATE);
|
|
86
|
+
var AGENT_EVENTS = Object.keys(AGENT_STATE);
|
|
87
|
+
// packages/agent-core/src/daemon-state.ts
|
|
88
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
89
|
+
import { homedir } from "os";
|
|
90
|
+
import { dirname, join } from "path";
|
|
91
|
+
var DAEMON_STATE_VERSION = 1;
|
|
92
|
+
var DAEMON_RUNTIME_DIR_NAME = ".agent-watch";
|
|
93
|
+
var DAEMON_STATE_FILE_NAME = "daemon.json";
|
|
94
|
+
function daemonRuntimeDir(home = homedir()) {
|
|
95
|
+
return join(home, DAEMON_RUNTIME_DIR_NAME);
|
|
96
|
+
}
|
|
97
|
+
function daemonStatePath(runtimeDir = daemonRuntimeDir()) {
|
|
98
|
+
return join(runtimeDir, DAEMON_STATE_FILE_NAME);
|
|
99
|
+
}
|
|
100
|
+
function readDaemonState(path = daemonStatePath()) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
103
|
+
return parseDaemonState(parsed);
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseDaemonState(value) {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
110
|
+
return null;
|
|
111
|
+
const data = value;
|
|
112
|
+
if (data.version !== DAEMON_STATE_VERSION)
|
|
113
|
+
return null;
|
|
114
|
+
if (typeof data.url !== "string" || !data.url)
|
|
115
|
+
return null;
|
|
116
|
+
if (typeof data.pid !== "number" || !Number.isInteger(data.pid) || data.pid <= 0)
|
|
117
|
+
return null;
|
|
118
|
+
if (typeof data.started_at !== "string" || !data.started_at)
|
|
119
|
+
return null;
|
|
120
|
+
return {
|
|
121
|
+
version: DAEMON_STATE_VERSION,
|
|
122
|
+
url: data.url,
|
|
123
|
+
pid: data.pid,
|
|
124
|
+
started_at: data.started_at
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// packages/aw/src/commands/launch.ts
|
|
128
|
+
import { spawn as spawn2 } from "child_process";
|
|
129
|
+
|
|
130
|
+
// packages/aw/src/daemon.ts
|
|
131
|
+
import { spawn } from "child_process";
|
|
132
|
+
import { mkdirSync as mkdirSync2, existsSync, openSync } from "fs";
|
|
133
|
+
import { join as join2, resolve } from "path";
|
|
134
|
+
function createDaemonClient(url) {
|
|
135
|
+
const post = async (path, body) => {
|
|
136
|
+
const response = await fetch(`${url}${path}`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
body: JSON.stringify(body)
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok)
|
|
142
|
+
throw new Error(`POST ${path} failed: ${response.status}`);
|
|
143
|
+
return await response.json();
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
url,
|
|
147
|
+
async registerLaunch(reg) {
|
|
148
|
+
const body = {
|
|
149
|
+
agent: reg.agent,
|
|
150
|
+
folder: reg.folder,
|
|
151
|
+
title: reg.title ?? null,
|
|
152
|
+
nvim_server: reg.nvim?.server ?? null,
|
|
153
|
+
nvim_terminal_bufnr: reg.nvim?.terminalBufnr ?? null
|
|
154
|
+
};
|
|
155
|
+
return post("/launches", body);
|
|
156
|
+
},
|
|
157
|
+
async updateLaunch(id, patch) {
|
|
158
|
+
const response = await fetch(`${url}/launches/${id}`, {
|
|
159
|
+
method: "PATCH",
|
|
160
|
+
headers: { "content-type": "application/json" },
|
|
161
|
+
body: JSON.stringify(patch)
|
|
162
|
+
});
|
|
163
|
+
if (response.status === 404)
|
|
164
|
+
return null;
|
|
165
|
+
if (!response.ok)
|
|
166
|
+
throw new Error(`PATCH /launches/${id} failed: ${response.status}`);
|
|
167
|
+
return await response.json();
|
|
168
|
+
},
|
|
169
|
+
async deleteLaunch(id) {
|
|
170
|
+
const response = await fetch(`${url}/launches/${id}`, { method: "DELETE" });
|
|
171
|
+
if (response.status === 404)
|
|
172
|
+
return false;
|
|
173
|
+
if (!response.ok)
|
|
174
|
+
throw new Error(`DELETE /launches/${id} failed: ${response.status}`);
|
|
175
|
+
return true;
|
|
176
|
+
},
|
|
177
|
+
async listAgents(filters) {
|
|
178
|
+
const qs = filters ? new URLSearchParams(filters).toString() : "";
|
|
179
|
+
const response = await fetch(`${url}/agents${qs ? `?${qs}` : ""}`);
|
|
180
|
+
if (!response.ok)
|
|
181
|
+
throw new Error(`GET /agents failed: ${response.status}`);
|
|
182
|
+
const body = await response.json();
|
|
183
|
+
return body.agents;
|
|
184
|
+
},
|
|
185
|
+
async health() {
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch(`${url}/healthz`);
|
|
188
|
+
return response.ok;
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async shutdown() {
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(`${url}/shutdown`, { method: "POST" });
|
|
196
|
+
return response.ok;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function ensureDaemonRunning(options = {}) {
|
|
204
|
+
const candidate = await findHealthyDaemon(options);
|
|
205
|
+
if (candidate)
|
|
206
|
+
return candidate;
|
|
207
|
+
const url = options.url ?? daemonUrlFromEnv(process.env);
|
|
208
|
+
const client = createDaemonClient(url);
|
|
209
|
+
spawnDaemon(options.daemonBin, options.runtimeDir);
|
|
210
|
+
const deadline = Date.now() + (options.startTimeoutMs ?? 5000);
|
|
211
|
+
while (Date.now() < deadline) {
|
|
212
|
+
if (await client.health())
|
|
213
|
+
return client;
|
|
214
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
215
|
+
}
|
|
216
|
+
throw new Error(`Could not start agent-watchd on ${url}`);
|
|
217
|
+
}
|
|
218
|
+
async function findHealthyDaemon(options) {
|
|
219
|
+
const explicitUrl = options.url ?? process.env[ENV.DAEMON_URL];
|
|
220
|
+
if (explicitUrl) {
|
|
221
|
+
const client = createDaemonClient(explicitUrl);
|
|
222
|
+
if (await client.health())
|
|
223
|
+
return client;
|
|
224
|
+
}
|
|
225
|
+
const state = readDaemonState(options.statePath ?? daemonStatePath());
|
|
226
|
+
if (state) {
|
|
227
|
+
const client = createDaemonClient(state.url);
|
|
228
|
+
if (await client.health())
|
|
229
|
+
return client;
|
|
230
|
+
}
|
|
231
|
+
const defaultUrl = daemonUrlFromEnv(process.env);
|
|
232
|
+
if (defaultUrl !== explicitUrl) {
|
|
233
|
+
const client = createDaemonClient(defaultUrl);
|
|
234
|
+
if (await client.health())
|
|
235
|
+
return client;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function spawnDaemon(daemonEntryPath, runtimeDir = daemonRuntimeDir()) {
|
|
240
|
+
mkdirSync2(runtimeDir, { recursive: true });
|
|
241
|
+
const entry = daemonEntryPath ?? resolveDaemonEntry();
|
|
242
|
+
const log = openSync(join2(runtimeDir, "agent-watchd.log"), "a");
|
|
243
|
+
const child = spawn(process.execPath, [entry], {
|
|
244
|
+
detached: true,
|
|
245
|
+
stdio: ["ignore", log, log],
|
|
246
|
+
env: process.env
|
|
247
|
+
});
|
|
248
|
+
child.unref();
|
|
249
|
+
}
|
|
250
|
+
function resolveDaemonEntry() {
|
|
251
|
+
const fromPackage = resolve(import.meta.dir, "..", "..", "agent-watchd", "src", "index.ts");
|
|
252
|
+
if (existsSync(fromPackage))
|
|
253
|
+
return fromPackage;
|
|
254
|
+
const fromBin = resolve(import.meta.dir, "..", "..", "..", "bin", "agent-watchd");
|
|
255
|
+
if (existsSync(fromBin))
|
|
256
|
+
return fromBin;
|
|
257
|
+
const fromPath = Bun.which("agent-watchd");
|
|
258
|
+
if (fromPath)
|
|
259
|
+
return fromPath;
|
|
260
|
+
throw new Error("Could not locate agent-watchd entrypoint");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// packages/aw/src/nvim.ts
|
|
264
|
+
function nvimContextFromEnv(env) {
|
|
265
|
+
return {
|
|
266
|
+
server: env.NVIM ?? null,
|
|
267
|
+
terminalBufnr: null
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function mergeNvim(base, override) {
|
|
271
|
+
return {
|
|
272
|
+
server: override.server ?? base.server,
|
|
273
|
+
terminalBufnr: override.terminalBufnr ?? base.terminalBufnr
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// packages/aw/src/commands/launch.ts
|
|
278
|
+
function parseLaunchArgv(argv) {
|
|
279
|
+
const rest = [];
|
|
280
|
+
let title = null;
|
|
281
|
+
let nvimServer = null;
|
|
282
|
+
let nvimBufnr = null;
|
|
283
|
+
let separatorSeen = false;
|
|
284
|
+
for (let i = 0;i < argv.length; i++) {
|
|
285
|
+
const arg = argv[i] ?? "";
|
|
286
|
+
if (separatorSeen) {
|
|
287
|
+
rest.push(arg);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (arg === "--") {
|
|
291
|
+
separatorSeen = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (arg === "--title") {
|
|
295
|
+
title = argv[++i] ?? "";
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (arg.startsWith("--title=")) {
|
|
299
|
+
title = arg.slice("--title=".length);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (arg === "--nvim-server") {
|
|
303
|
+
nvimServer = argv[++i] ?? "";
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (arg.startsWith("--nvim-server=")) {
|
|
307
|
+
nvimServer = arg.slice("--nvim-server=".length);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (arg === "--nvim-bufnr") {
|
|
311
|
+
nvimBufnr = argv[++i] ?? "";
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (arg.startsWith("--nvim-bufnr=")) {
|
|
315
|
+
nvimBufnr = arg.slice("--nvim-bufnr=".length);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
rest.push(arg);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
title: title || null,
|
|
322
|
+
nvim: { server: nvimServer, terminalBufnr: nvimBufnr },
|
|
323
|
+
rest
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async function runLaunch(args) {
|
|
327
|
+
const parsed = parseLaunchArgv(args.argv);
|
|
328
|
+
const executable = Bun.which(args.agent);
|
|
329
|
+
if (!executable) {
|
|
330
|
+
process.stderr.write(`Could not find ${args.agent} on PATH.
|
|
331
|
+
`);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
const client = await ensureDaemonRunning();
|
|
335
|
+
const baseNvim = nvimContextFromEnv(process.env);
|
|
336
|
+
const nvim = mergeNvim(baseNvim, parsed.nvim);
|
|
337
|
+
const record = await client.registerLaunch({
|
|
338
|
+
agent: args.agent,
|
|
339
|
+
folder: process.cwd(),
|
|
340
|
+
title: parsed.title,
|
|
341
|
+
nvim
|
|
342
|
+
});
|
|
343
|
+
const env = { ...process.env };
|
|
344
|
+
env[ENV.LAUNCH_ID] = String(record.id);
|
|
345
|
+
env[ENV.DAEMON_URL] = client.url;
|
|
346
|
+
const child = spawn2(executable, parsed.rest, { stdio: "inherit", env });
|
|
347
|
+
const cleanupSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
348
|
+
for (const signal of cleanupSignals) {
|
|
349
|
+
process.on(signal, () => {
|
|
350
|
+
if (!child.killed)
|
|
351
|
+
child.kill(signal);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (typeof child.pid === "number") {
|
|
355
|
+
client.updateLaunch(record.id, { agent_process_pid: child.pid }).catch(() => {
|
|
356
|
+
return;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return await new Promise((resolve2) => {
|
|
360
|
+
child.on("exit", (code, signal) => {
|
|
361
|
+
const exitCode = signal ? 128 + signalNumber(signal) : code ?? 0;
|
|
362
|
+
client.deleteLaunch(record.id).catch(() => {
|
|
363
|
+
return;
|
|
364
|
+
}).finally(() => resolve2(exitCode));
|
|
365
|
+
});
|
|
366
|
+
child.on("error", (err) => {
|
|
367
|
+
process.stderr.write(`Could not launch ${args.agent}: ${err.message}
|
|
368
|
+
`);
|
|
369
|
+
client.deleteLaunch(record.id).catch(() => {
|
|
370
|
+
return;
|
|
371
|
+
}).finally(() => resolve2(1));
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function signalNumber(signal) {
|
|
376
|
+
const map = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
377
|
+
return map[signal] ?? 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// packages/aw/src/render.ts
|
|
381
|
+
import { homedir as homedir2 } from "os";
|
|
382
|
+
var HOME = homedir2();
|
|
383
|
+
var COLUMNS = {
|
|
384
|
+
id: {
|
|
385
|
+
label: "ID",
|
|
386
|
+
width: 5,
|
|
387
|
+
value: (row) => String(row.id)
|
|
388
|
+
},
|
|
389
|
+
agent: {
|
|
390
|
+
label: "AGENT",
|
|
391
|
+
width: 8,
|
|
392
|
+
value: (row) => trunc(row.agent || "-", 7)
|
|
393
|
+
},
|
|
394
|
+
state: {
|
|
395
|
+
label: "STATE",
|
|
396
|
+
width: 16,
|
|
397
|
+
colorState: true,
|
|
398
|
+
value: (row) => row.state || "-"
|
|
399
|
+
},
|
|
400
|
+
title: {
|
|
401
|
+
label: "TITLE",
|
|
402
|
+
width: 22,
|
|
403
|
+
value: (row) => trunc(row.title || "-", 21)
|
|
404
|
+
},
|
|
405
|
+
repo: {
|
|
406
|
+
label: "REPO",
|
|
407
|
+
width: 18,
|
|
408
|
+
value: (row) => trunc(row.repo || "-", 17)
|
|
409
|
+
},
|
|
410
|
+
branch: {
|
|
411
|
+
label: "BRANCH",
|
|
412
|
+
width: 18,
|
|
413
|
+
value: (row) => trunc(row.branch || "-", 17)
|
|
414
|
+
},
|
|
415
|
+
folder: {
|
|
416
|
+
label: "FOLDER",
|
|
417
|
+
width: 24,
|
|
418
|
+
dynamic: true,
|
|
419
|
+
value: (row, width) => trunc(shortHome(row.folder || "-"), width)
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
var STATE_COLORS = {
|
|
423
|
+
idle: 32,
|
|
424
|
+
working: 36,
|
|
425
|
+
running_tool: 33,
|
|
426
|
+
running_shell: 33,
|
|
427
|
+
needs_approval: 31,
|
|
428
|
+
edited_file: 35,
|
|
429
|
+
session_started: 34,
|
|
430
|
+
waiting: 36,
|
|
431
|
+
stale: 90,
|
|
432
|
+
exited: 90,
|
|
433
|
+
failed: 31
|
|
434
|
+
};
|
|
435
|
+
var DEFAULT_COLUMNS = ["id", "agent", "state", "title", "repo", "branch", "folder"];
|
|
436
|
+
function renderTable(records, options) {
|
|
437
|
+
const columns = options.columns ?? DEFAULT_COLUMNS;
|
|
438
|
+
const dynamicColumn = columns.find((name) => COLUMNS[name]?.dynamic) ?? columns[columns.length - 1];
|
|
439
|
+
const fixedWidth = columns.filter((name) => name !== dynamicColumn).reduce((total, name) => total + (COLUMNS[name]?.width ?? 0), 0);
|
|
440
|
+
const dynamicWidth = Math.max(12, options.width - fixedWidth - 5);
|
|
441
|
+
const widths = new Map;
|
|
442
|
+
for (const name of columns) {
|
|
443
|
+
widths.set(name, name === dynamicColumn ? dynamicWidth : COLUMNS[name]?.width ?? 0);
|
|
444
|
+
}
|
|
445
|
+
const lines = [];
|
|
446
|
+
lines.push(columns.map((name) => {
|
|
447
|
+
const width = widths.get(name) ?? 0;
|
|
448
|
+
const def = COLUMNS[name];
|
|
449
|
+
return name === dynamicColumn ? trunc(def?.label ?? name, width) : pad(def?.label ?? name, width);
|
|
450
|
+
}).join(""));
|
|
451
|
+
if (records.length === 0) {
|
|
452
|
+
lines.push("(no agent sessions found)");
|
|
453
|
+
} else {
|
|
454
|
+
for (const row of records) {
|
|
455
|
+
lines.push(columns.map((name) => {
|
|
456
|
+
const width = widths.get(name) ?? 0;
|
|
457
|
+
const def = COLUMNS[name];
|
|
458
|
+
if (!def)
|
|
459
|
+
return pad("-", width);
|
|
460
|
+
const raw = def.value(row, width);
|
|
461
|
+
if (def.colorState && options.color) {
|
|
462
|
+
const colored = colorize(raw);
|
|
463
|
+
return pad(colored, width, raw.length);
|
|
464
|
+
}
|
|
465
|
+
return name === dynamicColumn ? trunc(raw, width) : pad(raw, width);
|
|
466
|
+
}).join(""));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return `${lines.join(`
|
|
470
|
+
`)}
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
function colorize(state) {
|
|
474
|
+
const code = STATE_COLORS[state] ?? 37;
|
|
475
|
+
return `\x1B[${code}m${state}\x1B[0m`;
|
|
476
|
+
}
|
|
477
|
+
function trunc(value, width) {
|
|
478
|
+
if (value.length <= width)
|
|
479
|
+
return value;
|
|
480
|
+
if (width <= 3)
|
|
481
|
+
return value.slice(0, width);
|
|
482
|
+
return `${value.slice(0, width - 3)}...`;
|
|
483
|
+
}
|
|
484
|
+
function pad(value, width, visibleLength) {
|
|
485
|
+
const length = visibleLength ?? value.length;
|
|
486
|
+
if (length >= width)
|
|
487
|
+
return value;
|
|
488
|
+
return value + " ".repeat(width - length);
|
|
489
|
+
}
|
|
490
|
+
function shortHome(value) {
|
|
491
|
+
if (HOME && value.startsWith(HOME))
|
|
492
|
+
return `~${value.slice(HOME.length)}`;
|
|
493
|
+
return value;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// packages/aw/src/commands/stream.ts
|
|
497
|
+
var FOOTER = `
|
|
498
|
+
Press Ctrl+C to exit.
|
|
499
|
+
`;
|
|
500
|
+
async function runStream() {
|
|
501
|
+
const client = await ensureDaemonRunning();
|
|
502
|
+
let stop = false;
|
|
503
|
+
process.on("SIGINT", () => {
|
|
504
|
+
stop = true;
|
|
505
|
+
process.stdout.write(`
|
|
506
|
+
`);
|
|
507
|
+
process.exit(0);
|
|
508
|
+
});
|
|
509
|
+
while (!stop) {
|
|
510
|
+
let records;
|
|
511
|
+
try {
|
|
512
|
+
records = await client.listAgents();
|
|
513
|
+
} catch (err) {
|
|
514
|
+
process.stderr.write(`Could not reach daemon: ${err.message}
|
|
515
|
+
`);
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
519
|
+
process.stdout.write(renderTable(records, {
|
|
520
|
+
color: process.stdout.isTTY ?? false,
|
|
521
|
+
width: process.stdout.columns ?? 100
|
|
522
|
+
}));
|
|
523
|
+
process.stdout.write(FOOTER);
|
|
524
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1000));
|
|
525
|
+
}
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// packages/aw/src/commands/hooks.ts
|
|
530
|
+
import { homedir as homedir3 } from "os";
|
|
531
|
+
import { join as join3 } from "path";
|
|
532
|
+
|
|
533
|
+
// packages/aw/src/config-patch/claude.ts
|
|
534
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, copyFileSync, existsSync as existsSync2 } from "fs";
|
|
535
|
+
import { dirname as dirname2 } from "path";
|
|
536
|
+
function patchClaudeSettings(file) {
|
|
537
|
+
mkdirSync3(dirname2(file), { recursive: true });
|
|
538
|
+
const backup = `${file}.agent-watch.bak`;
|
|
539
|
+
if (existsSync2(file) && !existsSync2(backup))
|
|
540
|
+
copyFileSync(file, backup);
|
|
541
|
+
const parsed = readSettings(file);
|
|
542
|
+
parsed.hooks ??= {};
|
|
543
|
+
for (const event of CLAUDE_EVENTS) {
|
|
544
|
+
const command = curlCommand("claude", event);
|
|
545
|
+
const groups = parsed.hooks[event] ?? [];
|
|
546
|
+
const filtered = groups.filter((group) => !groupHasAgentWatch(group));
|
|
547
|
+
filtered.push({ hooks: [{ type: "command", command }] });
|
|
548
|
+
parsed.hooks[event] = filtered;
|
|
549
|
+
}
|
|
550
|
+
writeFileSync2(file, `${JSON.stringify(parsed, null, 2)}
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
function readSettings(file) {
|
|
554
|
+
if (!existsSync2(file))
|
|
555
|
+
return {};
|
|
556
|
+
try {
|
|
557
|
+
const data = JSON.parse(readFileSync2(file, "utf8"));
|
|
558
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
559
|
+
return data;
|
|
560
|
+
}
|
|
561
|
+
return {};
|
|
562
|
+
} catch {
|
|
563
|
+
return {};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function groupHasAgentWatch(group) {
|
|
567
|
+
if (!Array.isArray(group?.hooks))
|
|
568
|
+
return false;
|
|
569
|
+
return group.hooks.some((hook) => {
|
|
570
|
+
const command = typeof hook?.command === "string" ? hook.command : "";
|
|
571
|
+
return command.includes("AGENT_WATCH_DAEMON_URL") || command.includes("agent-watch");
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function curlCommand(provider, event) {
|
|
575
|
+
return [
|
|
576
|
+
"curl -sS -X POST",
|
|
577
|
+
`"$AGENT_WATCH_DAEMON_URL/hooks/${provider}?launch_id=$AGENT_WATCH_LAUNCH_ID&event=${event}"`,
|
|
578
|
+
"-H 'content-type: application/json'",
|
|
579
|
+
"--data-binary @-",
|
|
580
|
+
">/dev/null || true"
|
|
581
|
+
].join(" ");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// packages/aw/src/config-patch/codex.ts
|
|
585
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, existsSync as existsSync3 } from "fs";
|
|
586
|
+
import { dirname as dirname3 } from "path";
|
|
587
|
+
var BEGIN = "# BEGIN agent-watch codex hooks";
|
|
588
|
+
var END = "# END agent-watch codex hooks";
|
|
589
|
+
function patchCodexConfig(file) {
|
|
590
|
+
mkdirSync4(dirname3(file), { recursive: true });
|
|
591
|
+
const backup = `${file}.agent-watch.bak`;
|
|
592
|
+
if (existsSync3(file) && !existsSync3(backup))
|
|
593
|
+
copyFileSync2(file, backup);
|
|
594
|
+
let content = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
595
|
+
content = ensureCodexFeatures(content);
|
|
596
|
+
content = removeManagedBlock(content).trimEnd();
|
|
597
|
+
content += `
|
|
598
|
+
|
|
599
|
+
${BEGIN}
|
|
600
|
+
${codexHookBlock()}
|
|
601
|
+
${END}
|
|
602
|
+
`;
|
|
603
|
+
writeFileSync3(file, content);
|
|
604
|
+
}
|
|
605
|
+
function ensureCodexFeatures(content) {
|
|
606
|
+
if (/^\s*\[features\]/m.test(content)) {
|
|
607
|
+
if (/^\s*codex_hooks\s*=/m.test(content)) {
|
|
608
|
+
return content.replace(/^\s*codex_hooks\s*=.*$/m, "codex_hooks = true");
|
|
609
|
+
}
|
|
610
|
+
return content.replace(/^\s*\[features\]\s*$/m, `[features]
|
|
611
|
+
codex_hooks = true`);
|
|
612
|
+
}
|
|
613
|
+
return `[features]
|
|
614
|
+
codex_hooks = true
|
|
615
|
+
|
|
616
|
+
${content}`;
|
|
617
|
+
}
|
|
618
|
+
function removeManagedBlock(content) {
|
|
619
|
+
const escapedBegin = BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
620
|
+
const escapedEnd = END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
621
|
+
return content.replace(new RegExp(`\\n?${escapedBegin}[\\s\\S]*?${escapedEnd}\\n?`, "g"), `
|
|
622
|
+
`);
|
|
623
|
+
}
|
|
624
|
+
function codexHookBlock() {
|
|
625
|
+
return CODEX_EVENTS.map((event) => {
|
|
626
|
+
const matcher = event === "PreToolUse" || event === "PermissionRequest" || event === "PostToolUse" ? `matcher = "*"
|
|
627
|
+
` : "";
|
|
628
|
+
const command = curlCommand("codex", event);
|
|
629
|
+
return `[[hooks.${event}]]
|
|
630
|
+
${matcher}hooks = [{ type = "command", command = ${quote(command)} }]`;
|
|
631
|
+
}).join(`
|
|
632
|
+
|
|
633
|
+
`);
|
|
634
|
+
}
|
|
635
|
+
function quote(value) {
|
|
636
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// packages/aw/src/config-patch/agent.ts
|
|
640
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, copyFileSync as copyFileSync3, existsSync as existsSync4 } from "fs";
|
|
641
|
+
import { dirname as dirname4 } from "path";
|
|
642
|
+
function patchAgentHooks(file) {
|
|
643
|
+
mkdirSync5(dirname4(file), { recursive: true });
|
|
644
|
+
const backup = `${file}.agent-watch.bak`;
|
|
645
|
+
if (existsSync4(file) && !existsSync4(backup))
|
|
646
|
+
copyFileSync3(file, backup);
|
|
647
|
+
const parsed = readHooks(file);
|
|
648
|
+
parsed.version = 1;
|
|
649
|
+
parsed.hooks ??= {};
|
|
650
|
+
for (const event of AGENT_EVENTS) {
|
|
651
|
+
const command = curlCommand("agent", event);
|
|
652
|
+
const entries = parsed.hooks[event] ?? [];
|
|
653
|
+
const filtered = entries.filter((hook) => !isAgentWatchHook(hook));
|
|
654
|
+
filtered.push({ command });
|
|
655
|
+
parsed.hooks[event] = filtered;
|
|
656
|
+
}
|
|
657
|
+
writeFileSync4(file, `${JSON.stringify(parsed, null, 2)}
|
|
658
|
+
`);
|
|
659
|
+
}
|
|
660
|
+
function readHooks(file) {
|
|
661
|
+
if (!existsSync4(file))
|
|
662
|
+
return {};
|
|
663
|
+
try {
|
|
664
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
665
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
666
|
+
return parsed;
|
|
667
|
+
}
|
|
668
|
+
return {};
|
|
669
|
+
} catch {
|
|
670
|
+
return {};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function isAgentWatchHook(hook) {
|
|
674
|
+
const command = typeof hook?.command === "string" ? hook.command : "";
|
|
675
|
+
return command.includes("AGENT_WATCH_DAEMON_URL") || command.includes("agent-watch");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// packages/aw/src/commands/hooks.ts
|
|
679
|
+
var HOME2 = homedir3();
|
|
680
|
+
function runHooksInstall(provider) {
|
|
681
|
+
if (provider === "claude") {
|
|
682
|
+
const file = join3(HOME2, ".claude", "settings.json");
|
|
683
|
+
patchClaudeSettings(file);
|
|
684
|
+
process.stdout.write(`Patched ${file}
|
|
685
|
+
`);
|
|
686
|
+
return 0;
|
|
687
|
+
}
|
|
688
|
+
if (provider === "codex") {
|
|
689
|
+
const file = join3(HOME2, ".codex", "config.toml");
|
|
690
|
+
patchCodexConfig(file);
|
|
691
|
+
process.stdout.write(`Patched ${file}
|
|
692
|
+
`);
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
if (provider === "agent") {
|
|
696
|
+
const file = join3(HOME2, ".cursor", "hooks.json");
|
|
697
|
+
patchAgentHooks(file);
|
|
698
|
+
process.stdout.write(`Patched ${file}
|
|
699
|
+
`);
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
process.stderr.write(`Unknown provider: ${provider}
|
|
703
|
+
`);
|
|
704
|
+
return 1;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// packages/aw/src/commands/stop.ts
|
|
708
|
+
async function runStop() {
|
|
709
|
+
const url = daemonUrlFromEnv(process.env);
|
|
710
|
+
const client = createDaemonClient(url);
|
|
711
|
+
if (!await client.health()) {
|
|
712
|
+
process.stdout.write(`agent-watchd is not running
|
|
713
|
+
`);
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
await client.shutdown();
|
|
717
|
+
if (await waitUntilStopped(client, 5000)) {
|
|
718
|
+
process.stdout.write(`agent-watchd stopped
|
|
719
|
+
`);
|
|
720
|
+
return 0;
|
|
721
|
+
}
|
|
722
|
+
process.stderr.write(`Timed out waiting for agent-watchd to stop
|
|
723
|
+
`);
|
|
724
|
+
return 1;
|
|
725
|
+
}
|
|
726
|
+
async function waitUntilStopped(client, timeoutMs) {
|
|
727
|
+
const deadline = Date.now() + timeoutMs;
|
|
728
|
+
while (Date.now() < deadline) {
|
|
729
|
+
if (!await client.health()) {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
733
|
+
}
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// packages/aw/src/index.ts
|
|
738
|
+
var HELP = `aw \u2014 launcher and live view for terminal AI coding agents.
|
|
739
|
+
|
|
740
|
+
Usage:
|
|
741
|
+
aw claude [--title <title>] [--nvim-server <addr>] [--nvim-bufnr <nr>] [-- claude args...]
|
|
742
|
+
aw codex [--title <title>] [--nvim-server <addr>] [--nvim-bufnr <nr>] [-- codex args...]
|
|
743
|
+
aw agent [--title <title>] [--nvim-server <addr>] [--nvim-bufnr <nr>] [-- agent args...]
|
|
744
|
+
aw stream
|
|
745
|
+
aw stop
|
|
746
|
+
aw hooks install <claude|codex|agent>
|
|
747
|
+
aw help
|
|
748
|
+
`;
|
|
749
|
+
async function main(argv) {
|
|
750
|
+
const [command, ...rest] = argv;
|
|
751
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
752
|
+
process.stdout.write(HELP);
|
|
753
|
+
return 0;
|
|
754
|
+
}
|
|
755
|
+
if (isSupportedAgent(command)) {
|
|
756
|
+
return runLaunch({ agent: command, argv: rest });
|
|
757
|
+
}
|
|
758
|
+
if (command === "stream") {
|
|
759
|
+
return runStream();
|
|
760
|
+
}
|
|
761
|
+
if (command === "stop") {
|
|
762
|
+
return runStop();
|
|
763
|
+
}
|
|
764
|
+
if (command === "hooks") {
|
|
765
|
+
const [sub, provider] = rest;
|
|
766
|
+
if (sub !== "install" || !provider) {
|
|
767
|
+
process.stderr.write(`Usage: aw hooks install <claude|codex|cursor>
|
|
768
|
+
`);
|
|
769
|
+
return 1;
|
|
770
|
+
}
|
|
771
|
+
return runHooksInstall(provider);
|
|
772
|
+
}
|
|
773
|
+
process.stderr.write(`Unknown command: ${command}
|
|
774
|
+
|
|
775
|
+
${HELP}`);
|
|
776
|
+
return 1;
|
|
777
|
+
}
|
|
778
|
+
export {
|
|
779
|
+
main
|
|
780
|
+
};
|