@preziosiraffaele/agent-watch 0.2.0 → 0.3.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/dist/agent-watchd.js +102 -22
- package/dist/aw.js +134 -51
- package/package.json +3 -1
package/dist/agent-watchd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/agent-core/src/types.ts
|
|
3
|
-
var AGENT_FILTER_KEYS = ["
|
|
3
|
+
var AGENT_FILTER_KEYS = ["repo"];
|
|
4
4
|
// packages/agent-core/src/protocol.ts
|
|
5
5
|
var DEFAULT_DAEMON_HOST = "127.0.0.1";
|
|
6
6
|
var DEFAULT_DAEMON_PORT = 3847;
|
|
@@ -30,6 +30,7 @@ var STATES = {
|
|
|
30
30
|
FAILED: "failed"
|
|
31
31
|
};
|
|
32
32
|
var STALE_AFTER_MS = 30 * 60 * 1000;
|
|
33
|
+
var MAX_EXITED_RECORDS = 50;
|
|
33
34
|
// packages/agent-core/src/events.ts
|
|
34
35
|
var CLAUDE_STATE = {
|
|
35
36
|
SessionStart: STATES.SESSION_STARTED,
|
|
@@ -41,9 +42,7 @@ var CLAUDE_STATE = {
|
|
|
41
42
|
PostToolUse: STATES.WORKING,
|
|
42
43
|
PostToolUseFailure: STATES.WORKING,
|
|
43
44
|
PostToolBatch: STATES.WORKING,
|
|
44
|
-
Notification: STATES.WAITING,
|
|
45
45
|
SubagentStart: STATES.RUNNING_TOOL,
|
|
46
|
-
SubagentStop: STATES.IDLE,
|
|
47
46
|
TaskCreated: STATES.RUNNING_TOOL,
|
|
48
47
|
TaskCompleted: STATES.WORKING,
|
|
49
48
|
Stop: STATES.IDLE,
|
|
@@ -88,18 +87,19 @@ function stateFor(agent, event) {
|
|
|
88
87
|
return STATES.WORKING;
|
|
89
88
|
}
|
|
90
89
|
// packages/agent-core/src/repo.ts
|
|
91
|
-
import { basename } from "path";
|
|
90
|
+
import { basename, dirname } from "path";
|
|
92
91
|
function inferRepoContext(folder, runGit) {
|
|
93
92
|
if (!folder)
|
|
94
|
-
return { repo: null,
|
|
93
|
+
return { repo: null, branch: null, project_root: folder };
|
|
95
94
|
const root = runGit(folder, ["rev-parse", "--show-toplevel"]);
|
|
96
95
|
if (!root)
|
|
97
|
-
return { repo: null,
|
|
96
|
+
return { repo: null, branch: null, project_root: folder };
|
|
98
97
|
const branch = runGit(folder, ["branch", "--show-current"]) || runGit(folder, ["rev-parse", "--short", "HEAD"]);
|
|
98
|
+
const commonGitDir = runGit(folder, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
99
99
|
return {
|
|
100
100
|
repo: basename(root),
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
branch: branch || null,
|
|
102
|
+
project_root: commonGitDir ? dirname(commonGitDir) : root
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
var realGitRunner = (folder, args) => {
|
|
@@ -196,7 +196,7 @@ function pickClaudeEventName(payload) {
|
|
|
196
196
|
// packages/agent-core/src/daemon-state.ts
|
|
197
197
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
198
198
|
import { homedir } from "os";
|
|
199
|
-
import { dirname, join } from "path";
|
|
199
|
+
import { dirname as dirname2, join } from "path";
|
|
200
200
|
var DAEMON_RUNTIME_DIR_NAME = ".agent-watch";
|
|
201
201
|
var DAEMON_STATE_FILE_NAME = "daemon.json";
|
|
202
202
|
function daemonRuntimeDir(home = homedir()) {
|
|
@@ -206,7 +206,7 @@ function daemonStatePath(runtimeDir = daemonRuntimeDir()) {
|
|
|
206
206
|
return join(runtimeDir, DAEMON_STATE_FILE_NAME);
|
|
207
207
|
}
|
|
208
208
|
function writeDaemonState(state, path = daemonStatePath()) {
|
|
209
|
-
mkdirSync(
|
|
209
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
210
210
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
211
211
|
writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}
|
|
212
212
|
`);
|
|
@@ -245,12 +245,11 @@ function createRegistry(options = {}) {
|
|
|
245
245
|
tool: "-",
|
|
246
246
|
folder: reg.folder,
|
|
247
247
|
repo: repo2.repo,
|
|
248
|
-
repo_root: repo2.repo_root,
|
|
249
248
|
branch: repo2.branch,
|
|
249
|
+
project_root: repo2.project_root,
|
|
250
250
|
updated: now().toISOString(),
|
|
251
251
|
agent_process_pid: null,
|
|
252
|
-
|
|
253
|
-
nvim_terminal_bufnr: reg.nvim?.terminalBufnr ?? null
|
|
252
|
+
client_ref: reg.client_ref ?? null
|
|
254
253
|
};
|
|
255
254
|
records.set(id, record);
|
|
256
255
|
broadcast({ type: "created", record });
|
|
@@ -260,28 +259,49 @@ function createRegistry(options = {}) {
|
|
|
260
259
|
const current = records.get(id);
|
|
261
260
|
if (!current)
|
|
262
261
|
return null;
|
|
262
|
+
const folder = patch.folder ?? current.folder;
|
|
263
|
+
const repo2 = folder !== current.folder ? inferRepoContext(folder, gitRunner) : null;
|
|
263
264
|
const next = {
|
|
264
265
|
...current,
|
|
265
266
|
agent_process_pid: patch.agent_process_pid !== undefined ? patch.agent_process_pid : current.agent_process_pid,
|
|
266
267
|
state: patch.state ?? current.state,
|
|
267
268
|
title: patch.title !== undefined ? patch.title : current.title,
|
|
269
|
+
client_ref: patch.client_ref !== undefined ? patch.client_ref : current.client_ref,
|
|
270
|
+
folder,
|
|
271
|
+
...repo2 ? {
|
|
272
|
+
repo: repo2.repo,
|
|
273
|
+
branch: repo2.branch,
|
|
274
|
+
project_root: repo2.project_root
|
|
275
|
+
} : {},
|
|
268
276
|
updated: now().toISOString()
|
|
269
277
|
};
|
|
270
278
|
records.set(id, next);
|
|
271
279
|
broadcast({ type: "updated", record: next });
|
|
272
280
|
return next;
|
|
273
281
|
};
|
|
282
|
+
const claimResume = (id, patch) => {
|
|
283
|
+
const current = records.get(id);
|
|
284
|
+
if (!current)
|
|
285
|
+
return null;
|
|
286
|
+
if (current.state !== STATES.EXITED)
|
|
287
|
+
return { type: "not_resumable", state: current.state };
|
|
288
|
+
const record = update(id, { ...patch, state: STATES.SESSION_STARTED });
|
|
289
|
+
if (!record)
|
|
290
|
+
return null;
|
|
291
|
+
return { type: "claimed", record };
|
|
292
|
+
};
|
|
274
293
|
const applyHook = (id, agent, hook) => {
|
|
275
294
|
const current = records.get(id);
|
|
276
295
|
if (!current)
|
|
277
296
|
return null;
|
|
297
|
+
if (current.state === STATES.EXITED)
|
|
298
|
+
return current;
|
|
278
299
|
const received_at = now().toISOString();
|
|
279
300
|
seq += 1;
|
|
280
|
-
const state = agent === "claude" && hook.event === "SubagentStop" && current.state === STATES.IDLE ? current.state : hook.state;
|
|
281
301
|
const next = {
|
|
282
302
|
...current,
|
|
283
303
|
agent: current.agent || agent,
|
|
284
|
-
state,
|
|
304
|
+
state: hook.state,
|
|
285
305
|
tool: hook.tool || current.tool,
|
|
286
306
|
session_id: hook.session_id ?? current.session_id,
|
|
287
307
|
recent_events: [...current.recent_events, { event: hook.event, received_at, seq }].slice(-3),
|
|
@@ -298,11 +318,44 @@ function createRegistry(options = {}) {
|
|
|
298
318
|
broadcast({ type: "removed", id });
|
|
299
319
|
return true;
|
|
300
320
|
};
|
|
321
|
+
const markExited = (id) => {
|
|
322
|
+
const current = records.get(id);
|
|
323
|
+
if (!current)
|
|
324
|
+
return null;
|
|
325
|
+
if (!current.session_id) {
|
|
326
|
+
records.delete(id);
|
|
327
|
+
broadcast({ type: "removed", id });
|
|
328
|
+
return { type: "removed" };
|
|
329
|
+
}
|
|
330
|
+
const next = {
|
|
331
|
+
...current,
|
|
332
|
+
state: STATES.EXITED,
|
|
333
|
+
agent_process_pid: null,
|
|
334
|
+
updated: now().toISOString()
|
|
335
|
+
};
|
|
336
|
+
records.set(id, next);
|
|
337
|
+
broadcast({ type: "updated", record: next });
|
|
338
|
+
evictExcessExited();
|
|
339
|
+
return { type: "exited", record: next };
|
|
340
|
+
};
|
|
341
|
+
const evictExcessExited = () => {
|
|
342
|
+
const exited = Array.from(records.values()).filter((record) => record.state === STATES.EXITED);
|
|
343
|
+
const excess = exited.length - MAX_EXITED_RECORDS;
|
|
344
|
+
if (excess <= 0)
|
|
345
|
+
return;
|
|
346
|
+
exited.sort((a, b) => a.updated.localeCompare(b.updated) || a.id - b.id);
|
|
347
|
+
for (const victim of exited.slice(0, excess)) {
|
|
348
|
+
records.delete(victim.id);
|
|
349
|
+
broadcast({ type: "removed", id: victim.id });
|
|
350
|
+
}
|
|
351
|
+
};
|
|
301
352
|
const refreshRepoContext = () => {
|
|
302
353
|
const recordsByFolder = new Map;
|
|
303
354
|
for (const record of records.values()) {
|
|
304
355
|
if (!record.folder)
|
|
305
356
|
continue;
|
|
357
|
+
if (record.state === STATES.EXITED)
|
|
358
|
+
continue;
|
|
306
359
|
const grouped = recordsByFolder.get(record.folder);
|
|
307
360
|
if (grouped) {
|
|
308
361
|
grouped.push(record);
|
|
@@ -314,15 +367,14 @@ function createRegistry(options = {}) {
|
|
|
314
367
|
for (const [folder, folderRecords] of recordsByFolder) {
|
|
315
368
|
const repo2 = inferRepoContext(folder, gitRunner);
|
|
316
369
|
for (const current of folderRecords) {
|
|
317
|
-
if (current.repo === repo2.repo && current.
|
|
370
|
+
if (current.repo === repo2.repo && current.branch === repo2.branch && current.project_root === repo2.project_root) {
|
|
318
371
|
continue;
|
|
319
372
|
}
|
|
320
373
|
const next = {
|
|
321
374
|
...current,
|
|
322
375
|
repo: repo2.repo,
|
|
323
|
-
repo_root: repo2.repo_root,
|
|
324
376
|
branch: repo2.branch,
|
|
325
|
-
|
|
377
|
+
project_root: repo2.project_root
|
|
326
378
|
};
|
|
327
379
|
records.set(next.id, next);
|
|
328
380
|
changed.push(next);
|
|
@@ -335,6 +387,8 @@ function createRegistry(options = {}) {
|
|
|
335
387
|
register,
|
|
336
388
|
update,
|
|
337
389
|
remove,
|
|
390
|
+
markExited,
|
|
391
|
+
claimResume,
|
|
338
392
|
applyHook,
|
|
339
393
|
refreshRepoContext,
|
|
340
394
|
get(id) {
|
|
@@ -505,6 +559,30 @@ async function handle(request, registry, queue, onShutdown) {
|
|
|
505
559
|
return text(404, "launch not found");
|
|
506
560
|
return json({ ok: true });
|
|
507
561
|
}
|
|
562
|
+
const launchResume = method === "POST" && path.match(/^\/launches\/(\d+)\/resume$/);
|
|
563
|
+
if (launchResume) {
|
|
564
|
+
const id = Number(launchResume[1]);
|
|
565
|
+
const patch = await readLaunchUpdate(request);
|
|
566
|
+
if (!patch)
|
|
567
|
+
return text(400, "invalid resume request");
|
|
568
|
+
const result = await queue.enqueue(id, () => registry.claimResume(id, patch));
|
|
569
|
+
if (!result)
|
|
570
|
+
return text(404, "launch not found");
|
|
571
|
+
if (result.type === "not_resumable") {
|
|
572
|
+
return json({ error: "not resumable", state: result.state }, { status: 409 });
|
|
573
|
+
}
|
|
574
|
+
return json(result.record);
|
|
575
|
+
}
|
|
576
|
+
const launchExit = method === "POST" && path.match(/^\/launches\/(\d+)\/exit$/);
|
|
577
|
+
if (launchExit) {
|
|
578
|
+
const id = Number(launchExit[1]);
|
|
579
|
+
const result = registry.markExited(id);
|
|
580
|
+
if (!result)
|
|
581
|
+
return text(404, "launch not found");
|
|
582
|
+
if (result.type === "removed")
|
|
583
|
+
return json({ ok: true });
|
|
584
|
+
return json(result.record);
|
|
585
|
+
}
|
|
508
586
|
const hookMatch = method === "POST" && path.match(/^\/hooks\/(claude|codex|agent)$/);
|
|
509
587
|
if (hookMatch) {
|
|
510
588
|
const agent = hookMatch[1];
|
|
@@ -540,10 +618,7 @@ async function readLaunchRegistration(request) {
|
|
|
540
618
|
agent: data.agent,
|
|
541
619
|
folder: data.folder,
|
|
542
620
|
title: typeof data.title === "string" ? data.title : null,
|
|
543
|
-
|
|
544
|
-
server: typeof data.nvim_server === "string" ? data.nvim_server : null,
|
|
545
|
-
terminalBufnr: typeof data.nvim_terminal_bufnr === "string" ? data.nvim_terminal_bufnr : null
|
|
546
|
-
}
|
|
621
|
+
client_ref: typeof data.client_ref === "string" ? data.client_ref : null
|
|
547
622
|
};
|
|
548
623
|
}
|
|
549
624
|
async function readLaunchUpdate(request) {
|
|
@@ -560,6 +635,11 @@ async function readLaunchUpdate(request) {
|
|
|
560
635
|
if ("title" in data) {
|
|
561
636
|
patch.title = typeof data.title === "string" ? data.title : null;
|
|
562
637
|
}
|
|
638
|
+
if (typeof data.folder === "string" && data.folder)
|
|
639
|
+
patch.folder = data.folder;
|
|
640
|
+
if ("client_ref" in data) {
|
|
641
|
+
patch.client_ref = typeof data.client_ref === "string" ? data.client_ref : null;
|
|
642
|
+
}
|
|
563
643
|
return patch;
|
|
564
644
|
}
|
|
565
645
|
async function readJson(request) {
|
package/dist/aw.js
CHANGED
|
@@ -47,9 +47,7 @@ var CLAUDE_STATE = {
|
|
|
47
47
|
PostToolUse: STATES.WORKING,
|
|
48
48
|
PostToolUseFailure: STATES.WORKING,
|
|
49
49
|
PostToolBatch: STATES.WORKING,
|
|
50
|
-
Notification: STATES.WAITING,
|
|
51
50
|
SubagentStart: STATES.RUNNING_TOOL,
|
|
52
|
-
SubagentStop: STATES.IDLE,
|
|
53
51
|
TaskCreated: STATES.RUNNING_TOOL,
|
|
54
52
|
TaskCompleted: STATES.WORKING,
|
|
55
53
|
Stop: STATES.IDLE,
|
|
@@ -149,8 +147,7 @@ function createDaemonClient(url) {
|
|
|
149
147
|
agent: reg.agent,
|
|
150
148
|
folder: reg.folder,
|
|
151
149
|
title: reg.title ?? null,
|
|
152
|
-
|
|
153
|
-
nvim_terminal_bufnr: reg.nvim?.terminalBufnr ?? null
|
|
150
|
+
client_ref: reg.client_ref ?? null
|
|
154
151
|
};
|
|
155
152
|
return post("/launches", body);
|
|
156
153
|
},
|
|
@@ -174,6 +171,40 @@ function createDaemonClient(url) {
|
|
|
174
171
|
throw new Error(`DELETE /launches/${id} failed: ${response.status}`);
|
|
175
172
|
return true;
|
|
176
173
|
},
|
|
174
|
+
async getLaunch(id) {
|
|
175
|
+
const agents = await this.listAgents();
|
|
176
|
+
return agents.find((a) => a.id === id) ?? null;
|
|
177
|
+
},
|
|
178
|
+
async resumeLaunch(id, ctx) {
|
|
179
|
+
const response = await fetch(`${url}/launches/${id}/resume`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "content-type": "application/json" },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
folder: ctx.folder,
|
|
184
|
+
client_ref: ctx.client_ref
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
if (response.status === 404)
|
|
188
|
+
return { type: "not_found" };
|
|
189
|
+
if (response.status === 409) {
|
|
190
|
+
const body = await response.json().catch(() => null);
|
|
191
|
+
return { type: "not_resumable", state: body?.state ?? "unknown" };
|
|
192
|
+
}
|
|
193
|
+
if (!response.ok)
|
|
194
|
+
throw new Error(`POST /launches/${id}/resume failed: ${response.status}`);
|
|
195
|
+
return { type: "resumed", record: await response.json() };
|
|
196
|
+
},
|
|
197
|
+
async reportExit(id) {
|
|
198
|
+
const response = await fetch(`${url}/launches/${id}/exit`, { method: "POST" });
|
|
199
|
+
if (response.status === 404)
|
|
200
|
+
return null;
|
|
201
|
+
if (!response.ok)
|
|
202
|
+
throw new Error(`POST /launches/${id}/exit failed: ${response.status}`);
|
|
203
|
+
const body = await response.json();
|
|
204
|
+
if ("ok" in body)
|
|
205
|
+
return null;
|
|
206
|
+
return body;
|
|
207
|
+
},
|
|
177
208
|
async listAgents(filters) {
|
|
178
209
|
const qs = filters ? new URLSearchParams(filters).toString() : "";
|
|
179
210
|
const response = await fetch(`${url}/agents${qs ? `?${qs}` : ""}`);
|
|
@@ -260,26 +291,11 @@ function resolveDaemonEntry() {
|
|
|
260
291
|
throw new Error("Could not locate agent-watchd entrypoint");
|
|
261
292
|
}
|
|
262
293
|
|
|
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
294
|
// packages/aw/src/commands/launch.ts
|
|
278
295
|
function parseLaunchArgv(argv) {
|
|
279
296
|
const rest = [];
|
|
280
297
|
let title = null;
|
|
281
|
-
let
|
|
282
|
-
let nvimBufnr = null;
|
|
298
|
+
let clientRef = null;
|
|
283
299
|
let separatorSeen = false;
|
|
284
300
|
for (let i = 0;i < argv.length; i++) {
|
|
285
301
|
const arg = argv[i] ?? "";
|
|
@@ -299,27 +315,19 @@ function parseLaunchArgv(argv) {
|
|
|
299
315
|
title = arg.slice("--title=".length);
|
|
300
316
|
continue;
|
|
301
317
|
}
|
|
302
|
-
if (arg === "--
|
|
303
|
-
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (arg.startsWith("--nvim-server=")) {
|
|
307
|
-
nvimServer = arg.slice("--nvim-server=".length);
|
|
318
|
+
if (arg === "--client-ref") {
|
|
319
|
+
clientRef = argv[++i] ?? "";
|
|
308
320
|
continue;
|
|
309
321
|
}
|
|
310
|
-
if (arg
|
|
311
|
-
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
if (arg.startsWith("--nvim-bufnr=")) {
|
|
315
|
-
nvimBufnr = arg.slice("--nvim-bufnr=".length);
|
|
322
|
+
if (arg.startsWith("--client-ref=")) {
|
|
323
|
+
clientRef = arg.slice("--client-ref=".length);
|
|
316
324
|
continue;
|
|
317
325
|
}
|
|
318
326
|
rest.push(arg);
|
|
319
327
|
}
|
|
320
328
|
return {
|
|
321
329
|
title: title || null,
|
|
322
|
-
|
|
330
|
+
client_ref: clientRef || null,
|
|
323
331
|
rest
|
|
324
332
|
};
|
|
325
333
|
}
|
|
@@ -332,18 +340,19 @@ async function runLaunch(args) {
|
|
|
332
340
|
return 1;
|
|
333
341
|
}
|
|
334
342
|
const client = await ensureDaemonRunning();
|
|
335
|
-
const baseNvim = nvimContextFromEnv(process.env);
|
|
336
|
-
const nvim = mergeNvim(baseNvim, parsed.nvim);
|
|
337
343
|
const record = await client.registerLaunch({
|
|
338
344
|
agent: args.agent,
|
|
339
|
-
folder: process.cwd(),
|
|
345
|
+
folder: args.folder ?? process.cwd(),
|
|
340
346
|
title: parsed.title,
|
|
341
|
-
|
|
347
|
+
client_ref: parsed.client_ref
|
|
342
348
|
});
|
|
349
|
+
return spawnAndWait(executable, record.id, parsed.rest, client);
|
|
350
|
+
}
|
|
351
|
+
async function spawnAndWait(executable, id, argv, client) {
|
|
343
352
|
const env = { ...process.env };
|
|
344
|
-
env[ENV.LAUNCH_ID] = String(
|
|
353
|
+
env[ENV.LAUNCH_ID] = String(id);
|
|
345
354
|
env[ENV.DAEMON_URL] = client.url;
|
|
346
|
-
const child = spawn2(executable,
|
|
355
|
+
const child = spawn2(executable, argv, { stdio: "inherit", env });
|
|
347
356
|
const cleanupSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
348
357
|
for (const signal of cleanupSignals) {
|
|
349
358
|
process.on(signal, () => {
|
|
@@ -352,21 +361,21 @@ async function runLaunch(args) {
|
|
|
352
361
|
});
|
|
353
362
|
}
|
|
354
363
|
if (typeof child.pid === "number") {
|
|
355
|
-
client.updateLaunch(
|
|
364
|
+
client.updateLaunch(id, { agent_process_pid: child.pid }).catch(() => {
|
|
356
365
|
return;
|
|
357
366
|
});
|
|
358
367
|
}
|
|
359
368
|
return await new Promise((resolve2) => {
|
|
360
369
|
child.on("exit", (code, signal) => {
|
|
361
370
|
const exitCode = signal ? 128 + signalNumber(signal) : code ?? 0;
|
|
362
|
-
client.
|
|
371
|
+
client.reportExit(id).catch(() => {
|
|
363
372
|
return;
|
|
364
373
|
}).finally(() => resolve2(exitCode));
|
|
365
374
|
});
|
|
366
375
|
child.on("error", (err) => {
|
|
367
|
-
process.stderr.write(`Could not launch ${
|
|
376
|
+
process.stderr.write(`Could not launch ${executable}: ${err.message}
|
|
368
377
|
`);
|
|
369
|
-
client.deleteLaunch(
|
|
378
|
+
client.deleteLaunch(id).catch(() => {
|
|
370
379
|
return;
|
|
371
380
|
}).finally(() => resolve2(1));
|
|
372
381
|
});
|
|
@@ -603,15 +612,15 @@ ${END}
|
|
|
603
612
|
writeFileSync3(file, content);
|
|
604
613
|
}
|
|
605
614
|
function ensureCodexFeatures(content) {
|
|
606
|
-
if (/^\s*\[features\]
|
|
607
|
-
if (/^\s*
|
|
608
|
-
return content.replace(/^\s*
|
|
615
|
+
if (/^\s*\[features\]\s*$/m.test(content)) {
|
|
616
|
+
if (/^\s*hooks\s*=/m.test(content)) {
|
|
617
|
+
return content.replace(/^\s*hooks\s*=.*$/m, "hooks = true");
|
|
609
618
|
}
|
|
610
619
|
return content.replace(/^\s*\[features\]\s*$/m, `[features]
|
|
611
|
-
|
|
620
|
+
hooks = true`);
|
|
612
621
|
}
|
|
613
622
|
return `[features]
|
|
614
|
-
|
|
623
|
+
hooks = true
|
|
615
624
|
|
|
616
625
|
${content}`;
|
|
617
626
|
}
|
|
@@ -689,6 +698,8 @@ function runHooksInstall(provider) {
|
|
|
689
698
|
const file = join3(HOME2, ".codex", "config.toml");
|
|
690
699
|
patchCodexConfig(file);
|
|
691
700
|
process.stdout.write(`Patched ${file}
|
|
701
|
+
`);
|
|
702
|
+
process.stdout.write(`Open Codex and run /hooks to review the installed hooks.
|
|
692
703
|
`);
|
|
693
704
|
return 0;
|
|
694
705
|
}
|
|
@@ -734,15 +745,84 @@ async function waitUntilStopped(client, timeoutMs) {
|
|
|
734
745
|
return false;
|
|
735
746
|
}
|
|
736
747
|
|
|
748
|
+
// packages/aw/src/commands/resume.ts
|
|
749
|
+
async function runResume(args) {
|
|
750
|
+
const [idStr, ...rest] = args;
|
|
751
|
+
if (!idStr) {
|
|
752
|
+
process.stderr.write(`Usage: aw resume <id> [--client-ref <token>] [-- agent args...]
|
|
753
|
+
`);
|
|
754
|
+
return 1;
|
|
755
|
+
}
|
|
756
|
+
const id = Number(idStr);
|
|
757
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
758
|
+
process.stderr.write(`Invalid launch id: ${idStr}
|
|
759
|
+
`);
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
const parsed = parseLaunchArgv(rest);
|
|
763
|
+
const client = await ensureDaemonRunning();
|
|
764
|
+
const record = await client.getLaunch(id);
|
|
765
|
+
if (!record) {
|
|
766
|
+
process.stderr.write(`Launch ${id} not found
|
|
767
|
+
`);
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
if (record.state !== STATES.EXITED) {
|
|
771
|
+
process.stderr.write(`Launch ${id} is not resumable (state: ${record.state})
|
|
772
|
+
`);
|
|
773
|
+
return 1;
|
|
774
|
+
}
|
|
775
|
+
if (!record.session_id) {
|
|
776
|
+
process.stderr.write(`Launch ${id} has no session_id
|
|
777
|
+
`);
|
|
778
|
+
return 1;
|
|
779
|
+
}
|
|
780
|
+
const executable = Bun.which(record.agent);
|
|
781
|
+
if (!executable) {
|
|
782
|
+
process.stderr.write(`Could not find ${record.agent} on PATH.
|
|
783
|
+
`);
|
|
784
|
+
return 1;
|
|
785
|
+
}
|
|
786
|
+
const argv = buildResumeArgs(record.agent, record.session_id);
|
|
787
|
+
if (parsed.rest.length)
|
|
788
|
+
argv.push("--", ...parsed.rest);
|
|
789
|
+
const claim = await client.resumeLaunch(id, {
|
|
790
|
+
folder: process.cwd(),
|
|
791
|
+
client_ref: parsed.client_ref
|
|
792
|
+
});
|
|
793
|
+
if (claim.type === "not_found") {
|
|
794
|
+
process.stderr.write(`Launch ${id} not found
|
|
795
|
+
`);
|
|
796
|
+
return 1;
|
|
797
|
+
}
|
|
798
|
+
if (claim.type === "not_resumable") {
|
|
799
|
+
process.stderr.write(`Launch ${id} is not resumable (state: ${claim.state})
|
|
800
|
+
`);
|
|
801
|
+
return 1;
|
|
802
|
+
}
|
|
803
|
+
return spawnAndWait(executable, id, argv, client);
|
|
804
|
+
}
|
|
805
|
+
function buildResumeArgs(agent, sessionId) {
|
|
806
|
+
switch (agent) {
|
|
807
|
+
case "claude":
|
|
808
|
+
return ["--resume", sessionId];
|
|
809
|
+
case "codex":
|
|
810
|
+
return ["resume", sessionId];
|
|
811
|
+
case "agent":
|
|
812
|
+
return ["--resume", sessionId];
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
737
816
|
// packages/aw/src/index.ts
|
|
738
817
|
var HELP = `aw \u2014 launcher and live view for terminal AI coding agents.
|
|
739
818
|
|
|
740
819
|
Usage:
|
|
741
|
-
aw claude [--title <title>] [--
|
|
742
|
-
aw codex [--title <title>] [--
|
|
743
|
-
aw agent [--title <title>] [--
|
|
820
|
+
aw claude [--title <title>] [--client-ref <token>] [-- claude args...]
|
|
821
|
+
aw codex [--title <title>] [--client-ref <token>] [-- codex args...]
|
|
822
|
+
aw agent [--title <title>] [--client-ref <token>] [-- agent args...]
|
|
744
823
|
aw stream
|
|
745
824
|
aw stop
|
|
825
|
+
aw resume <id> [--client-ref <token>] [-- agent args...]
|
|
746
826
|
aw hooks install <claude|codex|agent>
|
|
747
827
|
aw help
|
|
748
828
|
`;
|
|
@@ -761,6 +841,9 @@ async function main(argv) {
|
|
|
761
841
|
if (command === "stop") {
|
|
762
842
|
return runStop();
|
|
763
843
|
}
|
|
844
|
+
if (command === "resume") {
|
|
845
|
+
return runResume(rest);
|
|
846
|
+
}
|
|
764
847
|
if (command === "hooks") {
|
|
765
848
|
const [sub, provider] = rest;
|
|
766
849
|
if (sub !== "install" || !provider) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preziosiraffaele/agent-watch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Local observability layer for terminal-based AI coding agents.",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "bun build ./packages/aw/src/index.ts --outfile ./dist/aw.js --target bun && bun build ./packages/agent-watchd/src/index.ts --outfile ./dist/agent-watchd.js --target bun",
|
|
20
|
+
"link": "bun run build && bun link",
|
|
20
21
|
"prepare": "bun run build",
|
|
21
22
|
"test": "bun test",
|
|
22
23
|
"lint": "eslint .",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"@types/bun": "^1.1.0",
|
|
37
38
|
"eslint": "^9.26.0",
|
|
38
39
|
"eslint-config-prettier": "^10.1.8",
|
|
40
|
+
"eslint-plugin-jsdoc": "^62.9.0",
|
|
39
41
|
"prettier": "^3.8.3",
|
|
40
42
|
"typescript": "^5.6.0",
|
|
41
43
|
"typescript-eslint": "^8.20.0"
|