ax-agents 0.0.1-alpha.5 → 0.0.1-alpha.7
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/README.md +6 -2
- package/ax.js +990 -368
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// ax
|
|
3
|
+
// ax - CLI for interacting with AI agents (Codex, Claude) via `tmux`.
|
|
4
|
+
// Usage: ax --help
|
|
4
5
|
//
|
|
5
6
|
// Exit codes:
|
|
6
7
|
// 0 - success / ready
|
|
@@ -8,17 +9,29 @@
|
|
|
8
9
|
// 2 - rate limited
|
|
9
10
|
// 3 - awaiting confirmation
|
|
10
11
|
// 4 - thinking
|
|
11
|
-
//
|
|
12
|
-
// Usage: ./ax.js --help
|
|
13
|
-
// ./axcodex.js --help (symlink)
|
|
14
|
-
// ./axclaude.js --help (symlink)
|
|
15
12
|
|
|
16
13
|
import { execSync, spawnSync, spawn } from "node:child_process";
|
|
17
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
fstatSync,
|
|
16
|
+
statSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
appendFileSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
watch,
|
|
26
|
+
openSync,
|
|
27
|
+
readSync,
|
|
28
|
+
closeSync,
|
|
29
|
+
} from "node:fs";
|
|
18
30
|
import { randomUUID } from "node:crypto";
|
|
19
31
|
import { fileURLToPath } from "node:url";
|
|
20
32
|
import path from "node:path";
|
|
21
33
|
import os from "node:os";
|
|
34
|
+
import { parseArgs } from "node:util";
|
|
22
35
|
|
|
23
36
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
37
|
const __dirname = path.dirname(__filename);
|
|
@@ -110,8 +123,9 @@ const VERSION = packageJson.version;
|
|
|
110
123
|
*/
|
|
111
124
|
|
|
112
125
|
/**
|
|
126
|
+
* @typedef {{matcher: string, hooks: Array<{type: string, command: string, timeout?: number}>}} ClaudeHookEntry
|
|
113
127
|
* @typedef {Object} ClaudeSettings
|
|
114
|
-
* @property {{UserPromptSubmit?:
|
|
128
|
+
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PostToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
115
129
|
*/
|
|
116
130
|
|
|
117
131
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
@@ -221,7 +235,9 @@ function tmuxKill(session) {
|
|
|
221
235
|
*/
|
|
222
236
|
function tmuxNewSession(session, command) {
|
|
223
237
|
// Use spawnSync to avoid command injection via session/command
|
|
224
|
-
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
238
|
+
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
239
|
+
encoding: "utf-8",
|
|
240
|
+
});
|
|
225
241
|
if (result.status !== 0) throw new Error(result.stderr || "tmux new-session failed");
|
|
226
242
|
}
|
|
227
243
|
|
|
@@ -230,7 +246,9 @@ function tmuxNewSession(session, command) {
|
|
|
230
246
|
*/
|
|
231
247
|
function tmuxCurrentSession() {
|
|
232
248
|
if (!process.env.TMUX) return null;
|
|
233
|
-
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
249
|
+
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
250
|
+
encoding: "utf-8",
|
|
251
|
+
});
|
|
234
252
|
if (result.status !== 0) return null;
|
|
235
253
|
return result.stdout.trim();
|
|
236
254
|
}
|
|
@@ -242,9 +260,13 @@ function tmuxCurrentSession() {
|
|
|
242
260
|
*/
|
|
243
261
|
function isYoloSession(session) {
|
|
244
262
|
try {
|
|
245
|
-
const result = spawnSync(
|
|
246
|
-
|
|
247
|
-
|
|
263
|
+
const result = spawnSync(
|
|
264
|
+
"tmux",
|
|
265
|
+
["display-message", "-t", session, "-p", "#{pane_start_command}"],
|
|
266
|
+
{
|
|
267
|
+
encoding: "utf-8",
|
|
268
|
+
},
|
|
269
|
+
);
|
|
248
270
|
if (result.status !== 0) return false;
|
|
249
271
|
const cmd = result.stdout.trim();
|
|
250
272
|
return cmd.includes("--dangerously-");
|
|
@@ -264,8 +286,14 @@ const POLL_MS = parseInt(process.env.AX_POLL_MS || "200", 10);
|
|
|
264
286
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.AX_TIMEOUT_MS || "120000", 10);
|
|
265
287
|
const REVIEW_TIMEOUT_MS = parseInt(process.env.AX_REVIEW_TIMEOUT_MS || "900000", 10); // 15 minutes
|
|
266
288
|
const STARTUP_TIMEOUT_MS = parseInt(process.env.AX_STARTUP_TIMEOUT_MS || "30000", 10);
|
|
267
|
-
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
|
|
268
|
-
|
|
289
|
+
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
|
|
290
|
+
process.env.AX_ARCHANGEL_STARTUP_TIMEOUT_MS || "60000",
|
|
291
|
+
10,
|
|
292
|
+
);
|
|
293
|
+
const ARCHANGEL_RESPONSE_TIMEOUT_MS = parseInt(
|
|
294
|
+
process.env.AX_ARCHANGEL_RESPONSE_TIMEOUT_MS || "300000",
|
|
295
|
+
10,
|
|
296
|
+
); // 5 minutes
|
|
269
297
|
const ARCHANGEL_HEALTH_CHECK_MS = parseInt(process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000", 10);
|
|
270
298
|
const STABLE_MS = parseInt(process.env.AX_STABLE_MS || "1000", 10);
|
|
271
299
|
const APPROVE_DELAY_MS = parseInt(process.env.AX_APPROVE_DELAY_MS || "100", 10);
|
|
@@ -278,6 +306,21 @@ const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
|
|
|
278
306
|
const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
279
307
|
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
280
308
|
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} session
|
|
311
|
+
* @param {(screen: string) => boolean} predicate
|
|
312
|
+
* @param {number} [timeoutMs]
|
|
313
|
+
* @returns {Promise<string>}
|
|
314
|
+
*/
|
|
315
|
+
class TimeoutError extends Error {
|
|
316
|
+
/** @param {string} [session] */
|
|
317
|
+
constructor(session) {
|
|
318
|
+
super("timeout");
|
|
319
|
+
this.name = "TimeoutError";
|
|
320
|
+
this.session = session;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
281
324
|
/**
|
|
282
325
|
* @param {string} session
|
|
283
326
|
* @param {(screen: string) => boolean} predicate
|
|
@@ -291,7 +334,7 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
291
334
|
if (predicate(screen)) return screen;
|
|
292
335
|
await sleep(POLL_MS);
|
|
293
336
|
}
|
|
294
|
-
throw new
|
|
337
|
+
throw new TimeoutError(session);
|
|
295
338
|
}
|
|
296
339
|
|
|
297
340
|
// =============================================================================
|
|
@@ -304,7 +347,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
304
347
|
function findCallerPid() {
|
|
305
348
|
let pid = process.ppid;
|
|
306
349
|
while (pid > 1) {
|
|
307
|
-
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
350
|
+
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
351
|
+
encoding: "utf-8",
|
|
352
|
+
});
|
|
308
353
|
if (result.status !== 0) break;
|
|
309
354
|
const parts = result.stdout.trim().split(/\s+/);
|
|
310
355
|
const ppid = parseInt(parts[0], 10);
|
|
@@ -317,6 +362,38 @@ function findCallerPid() {
|
|
|
317
362
|
return null;
|
|
318
363
|
}
|
|
319
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Find orphaned claude/codex processes (PPID=1, reparented to init/launchd)
|
|
367
|
+
* @returns {{pid: string, command: string}[]}
|
|
368
|
+
*/
|
|
369
|
+
function findOrphanedProcesses() {
|
|
370
|
+
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf-8" });
|
|
371
|
+
|
|
372
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const orphans = [];
|
|
377
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
378
|
+
// Parse: " PID PPID command args..."
|
|
379
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
|
|
380
|
+
if (!match) continue;
|
|
381
|
+
|
|
382
|
+
const [, pid, ppid, args] = match;
|
|
383
|
+
|
|
384
|
+
// Must have PPID=1 (orphaned/reparented to init)
|
|
385
|
+
if (ppid !== "1") continue;
|
|
386
|
+
|
|
387
|
+
// Command must START with claude or codex (excludes tmux which also has PPID=1)
|
|
388
|
+
const cmd = args.split(/\s+/)[0];
|
|
389
|
+
if (cmd !== "claude" && cmd !== "codex") continue;
|
|
390
|
+
|
|
391
|
+
orphans.push({ pid, command: args.slice(0, 60) });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return orphans;
|
|
395
|
+
}
|
|
396
|
+
|
|
320
397
|
// =============================================================================
|
|
321
398
|
// Helpers - stdin
|
|
322
399
|
// =============================================================================
|
|
@@ -345,6 +422,86 @@ async function readStdin() {
|
|
|
345
422
|
}
|
|
346
423
|
|
|
347
424
|
// =============================================================================
|
|
425
|
+
// =============================================================================
|
|
426
|
+
// Helpers - CLI argument parsing
|
|
427
|
+
// =============================================================================
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Parse CLI arguments using Node.js built-in parseArgs.
|
|
431
|
+
* @param {string[]} args - Command line arguments (without node and script path)
|
|
432
|
+
* @returns {{ flags: ParsedFlags, positionals: string[] }}
|
|
433
|
+
*
|
|
434
|
+
* @typedef {Object} ParsedFlags
|
|
435
|
+
* @property {boolean} wait
|
|
436
|
+
* @property {boolean} noWait
|
|
437
|
+
* @property {boolean} yolo
|
|
438
|
+
* @property {boolean} fresh
|
|
439
|
+
* @property {boolean} reasoning
|
|
440
|
+
* @property {boolean} follow
|
|
441
|
+
* @property {boolean} all
|
|
442
|
+
* @property {boolean} orphans
|
|
443
|
+
* @property {boolean} force
|
|
444
|
+
* @property {boolean} version
|
|
445
|
+
* @property {boolean} help
|
|
446
|
+
* @property {string} [tool]
|
|
447
|
+
* @property {string} [session]
|
|
448
|
+
* @property {number} [timeout]
|
|
449
|
+
* @property {number} [tail]
|
|
450
|
+
* @property {number} [limit]
|
|
451
|
+
* @property {string} [branch]
|
|
452
|
+
*/
|
|
453
|
+
function parseCliArgs(args) {
|
|
454
|
+
const { values, positionals } = parseArgs({
|
|
455
|
+
args,
|
|
456
|
+
options: {
|
|
457
|
+
// Boolean flags
|
|
458
|
+
wait: { type: "boolean", default: false },
|
|
459
|
+
"no-wait": { type: "boolean", default: false },
|
|
460
|
+
yolo: { type: "boolean", default: false },
|
|
461
|
+
fresh: { type: "boolean", default: false },
|
|
462
|
+
reasoning: { type: "boolean", default: false },
|
|
463
|
+
follow: { type: "boolean", short: "f", default: false },
|
|
464
|
+
all: { type: "boolean", default: false },
|
|
465
|
+
orphans: { type: "boolean", default: false },
|
|
466
|
+
force: { type: "boolean", default: false },
|
|
467
|
+
version: { type: "boolean", short: "V", default: false },
|
|
468
|
+
help: { type: "boolean", short: "h", default: false },
|
|
469
|
+
// Value flags
|
|
470
|
+
tool: { type: "string" },
|
|
471
|
+
session: { type: "string" },
|
|
472
|
+
timeout: { type: "string" },
|
|
473
|
+
tail: { type: "string" },
|
|
474
|
+
limit: { type: "string" },
|
|
475
|
+
branch: { type: "string" },
|
|
476
|
+
},
|
|
477
|
+
allowPositionals: true,
|
|
478
|
+
strict: false, // Don't error on unknown flags
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
flags: {
|
|
483
|
+
wait: Boolean(values.wait),
|
|
484
|
+
noWait: Boolean(values["no-wait"]),
|
|
485
|
+
yolo: Boolean(values.yolo),
|
|
486
|
+
fresh: Boolean(values.fresh),
|
|
487
|
+
reasoning: Boolean(values.reasoning),
|
|
488
|
+
follow: Boolean(values.follow),
|
|
489
|
+
all: Boolean(values.all),
|
|
490
|
+
orphans: Boolean(values.orphans),
|
|
491
|
+
force: Boolean(values.force),
|
|
492
|
+
version: Boolean(values.version),
|
|
493
|
+
help: Boolean(values.help),
|
|
494
|
+
tool: /** @type {string | undefined} */ (values.tool),
|
|
495
|
+
session: /** @type {string | undefined} */ (values.session),
|
|
496
|
+
timeout: values.timeout !== undefined ? Number(values.timeout) : undefined,
|
|
497
|
+
tail: values.tail !== undefined ? Number(values.tail) : undefined,
|
|
498
|
+
limit: values.limit !== undefined ? Number(values.limit) : undefined,
|
|
499
|
+
branch: /** @type {string | undefined} */ (values.branch),
|
|
500
|
+
},
|
|
501
|
+
positionals,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
348
505
|
// Helpers - session tracking
|
|
349
506
|
// =============================================================================
|
|
350
507
|
|
|
@@ -360,13 +517,17 @@ function parseSessionName(session) {
|
|
|
360
517
|
const rest = match[2];
|
|
361
518
|
|
|
362
519
|
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
363
|
-
const archangelMatch = rest.match(
|
|
520
|
+
const archangelMatch = rest.match(
|
|
521
|
+
/^archangel-(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
522
|
+
);
|
|
364
523
|
if (archangelMatch) {
|
|
365
524
|
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
366
525
|
}
|
|
367
526
|
|
|
368
527
|
// Partner: {tool}-partner-{uuid}
|
|
369
|
-
const partnerMatch = rest.match(
|
|
528
|
+
const partnerMatch = rest.match(
|
|
529
|
+
/^partner-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
530
|
+
);
|
|
370
531
|
if (partnerMatch) {
|
|
371
532
|
return { tool, uuid: partnerMatch[1] };
|
|
372
533
|
}
|
|
@@ -399,9 +560,13 @@ function getClaudeProjectPath(cwd) {
|
|
|
399
560
|
*/
|
|
400
561
|
function getTmuxSessionCwd(sessionName) {
|
|
401
562
|
try {
|
|
402
|
-
const result = spawnSync(
|
|
403
|
-
|
|
404
|
-
|
|
563
|
+
const result = spawnSync(
|
|
564
|
+
"tmux",
|
|
565
|
+
["display-message", "-t", sessionName, "-p", "#{pane_current_path}"],
|
|
566
|
+
{
|
|
567
|
+
encoding: "utf-8",
|
|
568
|
+
},
|
|
569
|
+
);
|
|
405
570
|
if (result.status === 0) return result.stdout.trim();
|
|
406
571
|
} catch (err) {
|
|
407
572
|
debugError("getTmuxSessionCwd", err);
|
|
@@ -425,7 +590,9 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
425
590
|
if (existsSync(indexPath)) {
|
|
426
591
|
try {
|
|
427
592
|
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
428
|
-
const entry = index.entries?.find(
|
|
593
|
+
const entry = index.entries?.find(
|
|
594
|
+
/** @param {{sessionId: string, fullPath?: string}} e */ (e) => e.sessionId === sessionId,
|
|
595
|
+
);
|
|
429
596
|
if (entry?.fullPath) return entry.fullPath;
|
|
430
597
|
} catch (err) {
|
|
431
598
|
debugError("findClaudeLogPath", err);
|
|
@@ -447,9 +614,13 @@ function findCodexLogPath(sessionName) {
|
|
|
447
614
|
// For Codex, we need to match by timing since we can't control the session ID
|
|
448
615
|
// Get tmux session creation time
|
|
449
616
|
try {
|
|
450
|
-
const result = spawnSync(
|
|
451
|
-
|
|
452
|
-
|
|
617
|
+
const result = spawnSync(
|
|
618
|
+
"tmux",
|
|
619
|
+
["display-message", "-t", sessionName, "-p", "#{session_created}"],
|
|
620
|
+
{
|
|
621
|
+
encoding: "utf-8",
|
|
622
|
+
},
|
|
623
|
+
);
|
|
453
624
|
if (result.status !== 0) return null;
|
|
454
625
|
const createdTs = parseInt(result.stdout.trim(), 10) * 1000; // tmux gives seconds, we need ms
|
|
455
626
|
if (isNaN(createdTs)) return null;
|
|
@@ -483,7 +654,11 @@ function findCodexLogPath(sessionName) {
|
|
|
483
654
|
// Log file should be created shortly after session start
|
|
484
655
|
// Allow small negative diff (-2s) for clock skew, up to 60s for slow starts
|
|
485
656
|
if (diff >= -2000 && diff < 60000) {
|
|
486
|
-
candidates.push({
|
|
657
|
+
candidates.push({
|
|
658
|
+
file,
|
|
659
|
+
diff: Math.abs(diff),
|
|
660
|
+
path: path.join(dayDir, file),
|
|
661
|
+
});
|
|
487
662
|
}
|
|
488
663
|
}
|
|
489
664
|
|
|
@@ -496,7 +671,6 @@ function findCodexLogPath(sessionName) {
|
|
|
496
671
|
}
|
|
497
672
|
}
|
|
498
673
|
|
|
499
|
-
|
|
500
674
|
/**
|
|
501
675
|
* Extract assistant text responses from a JSONL log file.
|
|
502
676
|
* This provides clean text without screen-scraped artifacts.
|
|
@@ -542,6 +716,130 @@ function getAssistantText(logPath, index = 0) {
|
|
|
542
716
|
}
|
|
543
717
|
}
|
|
544
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Read new complete JSON lines from a log file since the given offset.
|
|
721
|
+
* @param {string | null} logPath
|
|
722
|
+
* @param {number} fromOffset
|
|
723
|
+
* @returns {{ entries: object[], newOffset: number }}
|
|
724
|
+
*/
|
|
725
|
+
function tailJsonl(logPath, fromOffset) {
|
|
726
|
+
if (!logPath || !existsSync(logPath)) {
|
|
727
|
+
return { entries: [], newOffset: fromOffset };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const stats = statSync(logPath);
|
|
731
|
+
if (stats.size <= fromOffset) {
|
|
732
|
+
return { entries: [], newOffset: fromOffset };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const fd = openSync(logPath, "r");
|
|
736
|
+
const buffer = Buffer.alloc(stats.size - fromOffset);
|
|
737
|
+
readSync(fd, buffer, 0, buffer.length, fromOffset);
|
|
738
|
+
closeSync(fd);
|
|
739
|
+
|
|
740
|
+
const text = buffer.toString("utf-8");
|
|
741
|
+
const lines = text.split("\n");
|
|
742
|
+
|
|
743
|
+
// Last line may be incomplete - don't parse it yet
|
|
744
|
+
const complete = lines.slice(0, -1).filter(Boolean);
|
|
745
|
+
const incomplete = lines[lines.length - 1];
|
|
746
|
+
|
|
747
|
+
const entries = [];
|
|
748
|
+
for (const line of complete) {
|
|
749
|
+
try {
|
|
750
|
+
entries.push(JSON.parse(line));
|
|
751
|
+
} catch {
|
|
752
|
+
// Skip malformed lines
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Offset advances by complete lines only
|
|
757
|
+
const newOffset = fromOffset + text.length - incomplete.length;
|
|
758
|
+
return { entries, newOffset };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* @typedef {{command?: string, file_path?: string, path?: string, pattern?: string}} ToolInput
|
|
763
|
+
*/
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Format a JSONL entry for streaming display.
|
|
767
|
+
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
768
|
+
* @returns {string | null}
|
|
769
|
+
*/
|
|
770
|
+
function formatEntry(entry) {
|
|
771
|
+
// Skip tool_result entries (they can be very verbose)
|
|
772
|
+
if (entry.type === "tool_result") return null;
|
|
773
|
+
|
|
774
|
+
// Only process assistant entries
|
|
775
|
+
if (entry.type !== "assistant") return null;
|
|
776
|
+
|
|
777
|
+
const parts = entry.message?.content || [];
|
|
778
|
+
const output = [];
|
|
779
|
+
|
|
780
|
+
for (const part of parts) {
|
|
781
|
+
if (part.type === "text" && part.text) {
|
|
782
|
+
output.push(part.text);
|
|
783
|
+
} else if (part.type === "tool_use" || part.type === "tool_call") {
|
|
784
|
+
const name = part.name || part.tool || "tool";
|
|
785
|
+
const input = part.input || part.arguments || {};
|
|
786
|
+
let summary;
|
|
787
|
+
if (name === "Bash" && input.command) {
|
|
788
|
+
summary = input.command.slice(0, 50);
|
|
789
|
+
} else {
|
|
790
|
+
const target = input.file_path || input.path || input.pattern || "";
|
|
791
|
+
summary = target.split("/").pop() || target.slice(0, 30);
|
|
792
|
+
}
|
|
793
|
+
output.push(`> ${name}(${summary})`);
|
|
794
|
+
}
|
|
795
|
+
// Skip thinking blocks - internal reasoning
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return output.length > 0 ? output.join("\n") : null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Extract pending tool from confirmation screen.
|
|
803
|
+
* @param {string} screen
|
|
804
|
+
* @returns {string | null}
|
|
805
|
+
*/
|
|
806
|
+
function extractPendingToolFromScreen(screen) {
|
|
807
|
+
const lines = screen.split("\n");
|
|
808
|
+
|
|
809
|
+
// Check recent lines for tool confirmation patterns
|
|
810
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 15); i--) {
|
|
811
|
+
const line = lines[i];
|
|
812
|
+
// Match tool confirmation patterns like "Bash: command" or "Write: /path/file"
|
|
813
|
+
const match = line.match(
|
|
814
|
+
/^\s*(Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch|NotebookEdit|Skill|TodoWrite|TodoRead):\s*(.{1,40})/,
|
|
815
|
+
);
|
|
816
|
+
if (match) {
|
|
817
|
+
return `${match[1]}: ${match[2].trim()}`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Format confirmation output with helpful commands
|
|
826
|
+
* @param {string} screen
|
|
827
|
+
* @param {Agent} _agent
|
|
828
|
+
* @returns {string}
|
|
829
|
+
*/
|
|
830
|
+
function formatConfirmationOutput(screen, _agent) {
|
|
831
|
+
const pendingTool = extractPendingToolFromScreen(screen);
|
|
832
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
833
|
+
|
|
834
|
+
let output = pendingTool || "Confirmation required";
|
|
835
|
+
output += "\n\ne.g.";
|
|
836
|
+
output += `\n ${cli} approve # for y/n prompts`;
|
|
837
|
+
output += `\n ${cli} reject`;
|
|
838
|
+
output += `\n ${cli} select N # for numbered menus`;
|
|
839
|
+
|
|
840
|
+
return output;
|
|
841
|
+
}
|
|
842
|
+
|
|
545
843
|
/**
|
|
546
844
|
* @returns {string[]}
|
|
547
845
|
*/
|
|
@@ -622,7 +920,7 @@ function loadAgentConfigs() {
|
|
|
622
920
|
try {
|
|
623
921
|
const content = readFileSync(path.join(agentsDir, file), "utf-8");
|
|
624
922
|
const config = parseAgentConfig(file, content);
|
|
625
|
-
if (config &&
|
|
923
|
+
if (config && "error" in config) {
|
|
626
924
|
console.error(`ERROR: ${file}: ${config.error}`);
|
|
627
925
|
continue;
|
|
628
926
|
}
|
|
@@ -653,7 +951,9 @@ function parseAgentConfig(filename, content) {
|
|
|
653
951
|
return { error: `Missing frontmatter. File must start with '---'` };
|
|
654
952
|
}
|
|
655
953
|
if (!normalized.includes("\n---\n")) {
|
|
656
|
-
return {
|
|
954
|
+
return {
|
|
955
|
+
error: `Frontmatter not closed. Add '---' on its own line after the YAML block`,
|
|
956
|
+
};
|
|
657
957
|
}
|
|
658
958
|
return { error: `Invalid frontmatter format` };
|
|
659
959
|
}
|
|
@@ -674,9 +974,13 @@ function parseAgentConfig(filename, content) {
|
|
|
674
974
|
const fieldName = line.trim().match(/^(\w+):/)?.[1];
|
|
675
975
|
if (fieldName && !knownFields.includes(fieldName)) {
|
|
676
976
|
// Suggest closest match
|
|
677
|
-
const suggestions = knownFields.filter(
|
|
977
|
+
const suggestions = knownFields.filter(
|
|
978
|
+
(f) => f[0] === fieldName[0] || fieldName.includes(f.slice(0, 3)),
|
|
979
|
+
);
|
|
678
980
|
const hint = suggestions.length > 0 ? ` Did you mean '${suggestions[0]}'?` : "";
|
|
679
|
-
return {
|
|
981
|
+
return {
|
|
982
|
+
error: `Unknown field '${fieldName}'.${hint} Valid fields: ${knownFields.join(", ")}`,
|
|
983
|
+
};
|
|
680
984
|
}
|
|
681
985
|
}
|
|
682
986
|
|
|
@@ -694,7 +998,9 @@ function parseAgentConfig(filename, content) {
|
|
|
694
998
|
const rawValue = intervalMatch[1].trim();
|
|
695
999
|
const parsed = parseInt(rawValue, 10);
|
|
696
1000
|
if (isNaN(parsed)) {
|
|
697
|
-
return {
|
|
1001
|
+
return {
|
|
1002
|
+
error: `Invalid interval '${rawValue}'. Must be a number (seconds)`,
|
|
1003
|
+
};
|
|
698
1004
|
}
|
|
699
1005
|
interval = Math.max(10, Math.min(3600, parsed)); // Clamp to 10s - 1hr
|
|
700
1006
|
}
|
|
@@ -706,16 +1012,22 @@ function parseAgentConfig(filename, content) {
|
|
|
706
1012
|
const rawWatch = watchLine[1].trim();
|
|
707
1013
|
// Must be array format
|
|
708
1014
|
if (!rawWatch.startsWith("[") || !rawWatch.endsWith("]")) {
|
|
709
|
-
return {
|
|
1015
|
+
return {
|
|
1016
|
+
error: `Invalid watch format. Must be an array: watch: ["src/**/*.ts"]`,
|
|
1017
|
+
};
|
|
710
1018
|
}
|
|
711
1019
|
const inner = rawWatch.slice(1, -1).trim();
|
|
712
1020
|
if (!inner) {
|
|
713
|
-
return {
|
|
1021
|
+
return {
|
|
1022
|
+
error: `Empty watch array. Add at least one pattern: watch: ["**/*"]`,
|
|
1023
|
+
};
|
|
714
1024
|
}
|
|
715
1025
|
watchPatterns = inner.split(",").map((p) => p.trim().replace(/^["']|["']$/g, ""));
|
|
716
1026
|
// Validate patterns aren't empty
|
|
717
1027
|
if (watchPatterns.some((p) => !p)) {
|
|
718
|
-
return {
|
|
1028
|
+
return {
|
|
1029
|
+
error: `Invalid watch pattern. Check for trailing commas or empty values`,
|
|
1030
|
+
};
|
|
719
1031
|
}
|
|
720
1032
|
}
|
|
721
1033
|
|
|
@@ -831,7 +1143,9 @@ function gcMailbox(maxAgeHours = 24) {
|
|
|
831
1143
|
/** @returns {string} */
|
|
832
1144
|
function getCurrentBranch() {
|
|
833
1145
|
try {
|
|
834
|
-
return execSync("git branch --show-current 2>/dev/null", {
|
|
1146
|
+
return execSync("git branch --show-current 2>/dev/null", {
|
|
1147
|
+
encoding: "utf-8",
|
|
1148
|
+
}).trim();
|
|
835
1149
|
} catch {
|
|
836
1150
|
return "unknown";
|
|
837
1151
|
}
|
|
@@ -840,7 +1154,9 @@ function getCurrentBranch() {
|
|
|
840
1154
|
/** @returns {string} */
|
|
841
1155
|
function getCurrentCommit() {
|
|
842
1156
|
try {
|
|
843
|
-
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
1157
|
+
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
1158
|
+
encoding: "utf-8",
|
|
1159
|
+
}).trim();
|
|
844
1160
|
} catch {
|
|
845
1161
|
return "unknown";
|
|
846
1162
|
}
|
|
@@ -864,7 +1180,9 @@ function getMainBranch() {
|
|
|
864
1180
|
/** @returns {string} */
|
|
865
1181
|
function getStagedDiff() {
|
|
866
1182
|
try {
|
|
867
|
-
return execSync("git diff --cached 2>/dev/null", {
|
|
1183
|
+
return execSync("git diff --cached 2>/dev/null", {
|
|
1184
|
+
encoding: "utf-8",
|
|
1185
|
+
}).trim();
|
|
868
1186
|
} catch {
|
|
869
1187
|
return "";
|
|
870
1188
|
}
|
|
@@ -889,20 +1207,18 @@ function getRecentCommitsDiff(hoursAgo = 4) {
|
|
|
889
1207
|
const since = `--since="${hoursAgo} hours ago"`;
|
|
890
1208
|
|
|
891
1209
|
// Get list of commits in range
|
|
892
|
-
const commits = execSync(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
).trim();
|
|
1210
|
+
const commits = execSync(`git log ${mainBranch}..HEAD ${since} --oneline 2>/dev/null`, {
|
|
1211
|
+
encoding: "utf-8",
|
|
1212
|
+
}).trim();
|
|
896
1213
|
|
|
897
1214
|
if (!commits) return "";
|
|
898
1215
|
|
|
899
1216
|
// Get diff for those commits
|
|
900
1217
|
const firstCommit = commits.split("\n").filter(Boolean).pop()?.split(" ")[0];
|
|
901
1218
|
if (!firstCommit) return "";
|
|
902
|
-
return execSync(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
).trim();
|
|
1219
|
+
return execSync(`git diff ${firstCommit}^..HEAD 2>/dev/null`, {
|
|
1220
|
+
encoding: "utf-8",
|
|
1221
|
+
}).trim();
|
|
906
1222
|
} catch {
|
|
907
1223
|
return "";
|
|
908
1224
|
}
|
|
@@ -917,7 +1233,10 @@ function truncateDiff(diff, maxLines = 200) {
|
|
|
917
1233
|
if (!diff) return "";
|
|
918
1234
|
const lines = diff.split("\n");
|
|
919
1235
|
if (lines.length <= maxLines) return diff;
|
|
920
|
-
return
|
|
1236
|
+
return (
|
|
1237
|
+
lines.slice(0, maxLines).join("\n") +
|
|
1238
|
+
`\n\n... (truncated, ${lines.length - maxLines} more lines)`
|
|
1239
|
+
);
|
|
921
1240
|
}
|
|
922
1241
|
|
|
923
1242
|
/**
|
|
@@ -1002,18 +1321,23 @@ function findCurrentClaudeSession() {
|
|
|
1002
1321
|
const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
|
|
1003
1322
|
if (existsSync(claudeProjectDir)) {
|
|
1004
1323
|
try {
|
|
1005
|
-
const files = readdirSync(claudeProjectDir).filter(f => f.endsWith(".jsonl"));
|
|
1324
|
+
const files = readdirSync(claudeProjectDir).filter((f) => f.endsWith(".jsonl"));
|
|
1006
1325
|
for (const file of files) {
|
|
1007
1326
|
const uuid = file.replace(".jsonl", "");
|
|
1008
1327
|
// Skip if we already have this from tmux sessions
|
|
1009
|
-
if (candidates.some(c => c.uuid === uuid)) continue;
|
|
1328
|
+
if (candidates.some((c) => c.uuid === uuid)) continue;
|
|
1010
1329
|
|
|
1011
1330
|
const logPath = path.join(claudeProjectDir, file);
|
|
1012
1331
|
try {
|
|
1013
1332
|
const stat = statSync(logPath);
|
|
1014
1333
|
// Only consider logs modified in the last hour (active sessions)
|
|
1015
1334
|
if (Date.now() - stat.mtimeMs < MAILBOX_MAX_AGE_MS) {
|
|
1016
|
-
candidates.push({
|
|
1335
|
+
candidates.push({
|
|
1336
|
+
session: null,
|
|
1337
|
+
uuid,
|
|
1338
|
+
mtime: stat.mtimeMs,
|
|
1339
|
+
logPath,
|
|
1340
|
+
});
|
|
1017
1341
|
}
|
|
1018
1342
|
} catch (err) {
|
|
1019
1343
|
debugError("findCurrentClaudeSession:logStat", err);
|
|
@@ -1085,7 +1409,9 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1085
1409
|
if (typeof c === "string" && c.length > 10) {
|
|
1086
1410
|
entries.push({ type: "user", text: c });
|
|
1087
1411
|
} else if (Array.isArray(c)) {
|
|
1088
|
-
const text = c.find(
|
|
1412
|
+
const text = c.find(
|
|
1413
|
+
/** @param {{type: string, text?: string}} x */ (x) => x.type === "text",
|
|
1414
|
+
)?.text;
|
|
1089
1415
|
if (text && text.length > 10) {
|
|
1090
1416
|
entries.push({ type: "user", text });
|
|
1091
1417
|
}
|
|
@@ -1093,7 +1419,10 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1093
1419
|
} else if (entry.type === "assistant") {
|
|
1094
1420
|
/** @type {{type: string, text?: string}[]} */
|
|
1095
1421
|
const parts = entry.message?.content || [];
|
|
1096
|
-
const text = parts
|
|
1422
|
+
const text = parts
|
|
1423
|
+
.filter((p) => p.type === "text")
|
|
1424
|
+
.map((p) => p.text || "")
|
|
1425
|
+
.join("\n");
|
|
1097
1426
|
// Only include assistant responses with meaningful text
|
|
1098
1427
|
if (text && text.length > 20) {
|
|
1099
1428
|
entries.push({ type: "assistant", text });
|
|
@@ -1105,7 +1434,7 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1105
1434
|
}
|
|
1106
1435
|
|
|
1107
1436
|
// Format recent conversation
|
|
1108
|
-
const formatted = entries.slice(-maxEntries).map(e => {
|
|
1437
|
+
const formatted = entries.slice(-maxEntries).map((e) => {
|
|
1109
1438
|
const preview = e.text.slice(0, 500).replace(/\n/g, " ");
|
|
1110
1439
|
return `**${e.type === "user" ? "User" : "Assistant"}**: ${preview}`;
|
|
1111
1440
|
});
|
|
@@ -1148,10 +1477,16 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1148
1477
|
|
|
1149
1478
|
// Parse all entries
|
|
1150
1479
|
/** @type {any[]} */
|
|
1151
|
-
const entries = lines
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1480
|
+
const entries = lines
|
|
1481
|
+
.map((line, idx) => {
|
|
1482
|
+
try {
|
|
1483
|
+
return { idx, ...JSON.parse(line) };
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
debugError("extractFileEditContext:parse", err);
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
})
|
|
1489
|
+
.filter(Boolean);
|
|
1155
1490
|
|
|
1156
1491
|
// Find Write/Edit tool calls for this file (scan backwards, want most recent)
|
|
1157
1492
|
/** @type {any} */
|
|
@@ -1164,9 +1499,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1164
1499
|
|
|
1165
1500
|
/** @type {any[]} */
|
|
1166
1501
|
const msgContent = entry.message?.content || [];
|
|
1167
|
-
const toolCalls = msgContent.filter(
|
|
1168
|
-
(
|
|
1169
|
-
|
|
1502
|
+
const toolCalls = msgContent.filter(
|
|
1503
|
+
(/** @type {any} */ c) =>
|
|
1504
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1505
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1170
1506
|
);
|
|
1171
1507
|
|
|
1172
1508
|
for (const tc of toolCalls) {
|
|
@@ -1217,8 +1553,9 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1217
1553
|
|
|
1218
1554
|
/** @type {any[]} */
|
|
1219
1555
|
const msgContent = entry.message?.content || [];
|
|
1220
|
-
const readCalls = msgContent.filter(
|
|
1221
|
-
(
|
|
1556
|
+
const readCalls = msgContent.filter(
|
|
1557
|
+
(/** @type {any} */ c) =>
|
|
1558
|
+
(c.type === "tool_use" || c.type === "tool_call") && c.name === "Read",
|
|
1222
1559
|
);
|
|
1223
1560
|
|
|
1224
1561
|
for (const rc of readCalls) {
|
|
@@ -1233,9 +1570,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1233
1570
|
if (entry.type !== "assistant") continue;
|
|
1234
1571
|
/** @type {any[]} */
|
|
1235
1572
|
const msgContent = entry.message?.content || [];
|
|
1236
|
-
const edits = msgContent.filter(
|
|
1237
|
-
(
|
|
1238
|
-
|
|
1573
|
+
const edits = msgContent.filter(
|
|
1574
|
+
(/** @type {any} */ c) =>
|
|
1575
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1576
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1239
1577
|
);
|
|
1240
1578
|
for (const e of edits) {
|
|
1241
1579
|
const input = e.input || e.arguments || {};
|
|
@@ -1250,11 +1588,11 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1250
1588
|
toolCall: {
|
|
1251
1589
|
name: editEntry.toolCall.name,
|
|
1252
1590
|
input: editEntry.toolCall.input || editEntry.toolCall.arguments,
|
|
1253
|
-
id: editEntry.toolCall.id
|
|
1591
|
+
id: editEntry.toolCall.id,
|
|
1254
1592
|
},
|
|
1255
1593
|
subsequentErrors,
|
|
1256
1594
|
readsBefore: [...new Set(readsBefore)].slice(0, 10),
|
|
1257
|
-
editSequence
|
|
1595
|
+
editSequence,
|
|
1258
1596
|
};
|
|
1259
1597
|
}
|
|
1260
1598
|
|
|
@@ -1348,10 +1686,11 @@ function watchForChanges(patterns, callback) {
|
|
|
1348
1686
|
}
|
|
1349
1687
|
}
|
|
1350
1688
|
|
|
1351
|
-
return () => {
|
|
1689
|
+
return () => {
|
|
1690
|
+
for (const w of watchers) w.close();
|
|
1691
|
+
};
|
|
1352
1692
|
}
|
|
1353
1693
|
|
|
1354
|
-
|
|
1355
1694
|
// =============================================================================
|
|
1356
1695
|
// State
|
|
1357
1696
|
// =============================================================================
|
|
@@ -1373,7 +1712,7 @@ const State = {
|
|
|
1373
1712
|
* @param {string} config.promptSymbol - Symbol indicating ready state
|
|
1374
1713
|
* @param {string[]} [config.spinners] - Spinner characters indicating thinking
|
|
1375
1714
|
* @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
|
|
1376
|
-
* @param {string[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1715
|
+
* @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1377
1716
|
* @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
|
|
1378
1717
|
* @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
|
|
1379
1718
|
* @returns {string} The detected state
|
|
@@ -1391,14 +1730,33 @@ function detectState(screen, config) {
|
|
|
1391
1730
|
return State.RATE_LIMITED;
|
|
1392
1731
|
}
|
|
1393
1732
|
|
|
1394
|
-
//
|
|
1733
|
+
// Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
|
|
1734
|
+
const confirmPatterns = config.confirmPatterns || [];
|
|
1735
|
+
for (const pattern of confirmPatterns) {
|
|
1736
|
+
if (typeof pattern === "function") {
|
|
1737
|
+
// Functions check lastLines first (most specific), then recentLines
|
|
1738
|
+
if (pattern(lastLines)) return State.CONFIRMING;
|
|
1739
|
+
if (pattern(recentLines)) return State.CONFIRMING;
|
|
1740
|
+
} else {
|
|
1741
|
+
// String patterns check recentLines (bounded range)
|
|
1742
|
+
if (recentLines.includes(pattern)) return State.CONFIRMING;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Thinking - spinners (check last lines only to avoid false positives from timing messages like "✻ Crunched for 32s")
|
|
1395
1747
|
const spinners = config.spinners || [];
|
|
1396
|
-
if (spinners.some((s) =>
|
|
1748
|
+
if (spinners.some((s) => lastLines.includes(s))) {
|
|
1397
1749
|
return State.THINKING;
|
|
1398
1750
|
}
|
|
1399
|
-
// Thinking - text patterns (last lines)
|
|
1751
|
+
// Thinking - text patterns (last lines) - supports strings, regexes, and functions
|
|
1400
1752
|
const thinkingPatterns = config.thinkingPatterns || [];
|
|
1401
|
-
if (
|
|
1753
|
+
if (
|
|
1754
|
+
thinkingPatterns.some((p) => {
|
|
1755
|
+
if (typeof p === "function") return p(lastLines);
|
|
1756
|
+
if (p instanceof RegExp) return p.test(lastLines);
|
|
1757
|
+
return lastLines.includes(p);
|
|
1758
|
+
})
|
|
1759
|
+
) {
|
|
1402
1760
|
return State.THINKING;
|
|
1403
1761
|
}
|
|
1404
1762
|
|
|
@@ -1410,26 +1768,13 @@ function detectState(screen, config) {
|
|
|
1410
1768
|
}
|
|
1411
1769
|
}
|
|
1412
1770
|
|
|
1413
|
-
// Confirming - check recent lines (not full screen to avoid history false positives)
|
|
1414
|
-
const confirmPatterns = config.confirmPatterns || [];
|
|
1415
|
-
for (const pattern of confirmPatterns) {
|
|
1416
|
-
if (typeof pattern === "function") {
|
|
1417
|
-
// Functions check lastLines first (most specific), then recentLines
|
|
1418
|
-
if (pattern(lastLines)) return State.CONFIRMING;
|
|
1419
|
-
if (pattern(recentLines)) return State.CONFIRMING;
|
|
1420
|
-
} else {
|
|
1421
|
-
// String patterns check recentLines (bounded range)
|
|
1422
|
-
if (recentLines.includes(pattern)) return State.CONFIRMING;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
1771
|
// Ready - only if prompt symbol is visible AND not followed by pasted content
|
|
1427
1772
|
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
1428
1773
|
if (lastLines.includes(config.promptSymbol)) {
|
|
1429
1774
|
// Check if any line has the prompt followed by pasted content indicator
|
|
1430
1775
|
const linesArray = lastLines.split("\n");
|
|
1431
1776
|
const promptWithPaste = linesArray.some(
|
|
1432
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text")
|
|
1777
|
+
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
1433
1778
|
);
|
|
1434
1779
|
if (!promptWithPaste) {
|
|
1435
1780
|
return State.READY;
|
|
@@ -1457,12 +1802,13 @@ function detectState(screen, config) {
|
|
|
1457
1802
|
/**
|
|
1458
1803
|
* @typedef {Object} AgentConfigInput
|
|
1459
1804
|
* @property {string} name
|
|
1805
|
+
* @property {string} displayName
|
|
1460
1806
|
* @property {string} startCommand
|
|
1461
1807
|
* @property {string} yoloCommand
|
|
1462
1808
|
* @property {string} promptSymbol
|
|
1463
1809
|
* @property {string[]} [spinners]
|
|
1464
1810
|
* @property {RegExp} [rateLimitPattern]
|
|
1465
|
-
* @property {string[]} [thinkingPatterns]
|
|
1811
|
+
* @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
|
|
1466
1812
|
* @property {ConfirmPattern[]} [confirmPatterns]
|
|
1467
1813
|
* @property {UpdatePromptPatterns | null} [updatePromptPatterns]
|
|
1468
1814
|
* @property {string[]} [responseMarkers]
|
|
@@ -1472,6 +1818,8 @@ function detectState(screen, config) {
|
|
|
1472
1818
|
* @property {string} [approveKey]
|
|
1473
1819
|
* @property {string} [rejectKey]
|
|
1474
1820
|
* @property {string} [safeAllowedTools]
|
|
1821
|
+
* @property {string | null} [sessionIdFlag]
|
|
1822
|
+
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
1475
1823
|
*/
|
|
1476
1824
|
|
|
1477
1825
|
class Agent {
|
|
@@ -1482,6 +1830,8 @@ class Agent {
|
|
|
1482
1830
|
/** @type {string} */
|
|
1483
1831
|
this.name = config.name;
|
|
1484
1832
|
/** @type {string} */
|
|
1833
|
+
this.displayName = config.displayName;
|
|
1834
|
+
/** @type {string} */
|
|
1485
1835
|
this.startCommand = config.startCommand;
|
|
1486
1836
|
/** @type {string} */
|
|
1487
1837
|
this.yoloCommand = config.yoloCommand;
|
|
@@ -1491,7 +1841,7 @@ class Agent {
|
|
|
1491
1841
|
this.spinners = config.spinners || [];
|
|
1492
1842
|
/** @type {RegExp | undefined} */
|
|
1493
1843
|
this.rateLimitPattern = config.rateLimitPattern;
|
|
1494
|
-
/** @type {string[]} */
|
|
1844
|
+
/** @type {(string | RegExp | ((lines: string) => boolean))[]} */
|
|
1495
1845
|
this.thinkingPatterns = config.thinkingPatterns || [];
|
|
1496
1846
|
/** @type {ConfirmPattern[]} */
|
|
1497
1847
|
this.confirmPatterns = config.confirmPatterns || [];
|
|
@@ -1511,6 +1861,10 @@ class Agent {
|
|
|
1511
1861
|
this.rejectKey = config.rejectKey || "n";
|
|
1512
1862
|
/** @type {string | undefined} */
|
|
1513
1863
|
this.safeAllowedTools = config.safeAllowedTools;
|
|
1864
|
+
/** @type {string | null} */
|
|
1865
|
+
this.sessionIdFlag = config.sessionIdFlag || null;
|
|
1866
|
+
/** @type {((sessionName: string) => string | null) | null} */
|
|
1867
|
+
this.logPathFinder = config.logPathFinder || null;
|
|
1514
1868
|
}
|
|
1515
1869
|
|
|
1516
1870
|
/**
|
|
@@ -1528,11 +1882,11 @@ class Agent {
|
|
|
1528
1882
|
} else {
|
|
1529
1883
|
base = this.startCommand;
|
|
1530
1884
|
}
|
|
1531
|
-
//
|
|
1532
|
-
if (this.
|
|
1885
|
+
// Some agents support session ID flags for deterministic session tracking
|
|
1886
|
+
if (this.sessionIdFlag && sessionName) {
|
|
1533
1887
|
const parsed = parseSessionName(sessionName);
|
|
1534
1888
|
if (parsed?.uuid) {
|
|
1535
|
-
return `${base}
|
|
1889
|
+
return `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
|
|
1536
1890
|
}
|
|
1537
1891
|
}
|
|
1538
1892
|
return base;
|
|
@@ -1590,13 +1944,8 @@ class Agent {
|
|
|
1590
1944
|
* @returns {string | null}
|
|
1591
1945
|
*/
|
|
1592
1946
|
findLogPath(sessionName) {
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const uuid = parsed?.uuid;
|
|
1596
|
-
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
1597
|
-
}
|
|
1598
|
-
if (this.name === "codex") {
|
|
1599
|
-
return findCodexLogPath(sessionName);
|
|
1947
|
+
if (this.logPathFinder) {
|
|
1948
|
+
return this.logPathFinder(sessionName);
|
|
1600
1949
|
}
|
|
1601
1950
|
return null;
|
|
1602
1951
|
}
|
|
@@ -1640,7 +1989,12 @@ class Agent {
|
|
|
1640
1989
|
if (/^(run|execute|create|delete|modify|write)/i.test(line)) return line;
|
|
1641
1990
|
}
|
|
1642
1991
|
|
|
1643
|
-
return
|
|
1992
|
+
return (
|
|
1993
|
+
lines
|
|
1994
|
+
.filter((l) => l && !l.match(/^[╭╮╰╯│─]+$/))
|
|
1995
|
+
.slice(0, 2)
|
|
1996
|
+
.join(" | ") || "action"
|
|
1997
|
+
);
|
|
1644
1998
|
}
|
|
1645
1999
|
|
|
1646
2000
|
/**
|
|
@@ -1716,7 +2070,9 @@ class Agent {
|
|
|
1716
2070
|
|
|
1717
2071
|
// Fallback: extract after last prompt
|
|
1718
2072
|
if (filtered.length === 0) {
|
|
1719
|
-
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
2073
|
+
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
2074
|
+
l.startsWith(this.promptSymbol),
|
|
2075
|
+
);
|
|
1720
2076
|
if (lastPromptIdx >= 0 && lastPromptIdx < lines.length - 1) {
|
|
1721
2077
|
const afterPrompt = lines
|
|
1722
2078
|
.slice(lastPromptIdx + 1)
|
|
@@ -1730,14 +2086,14 @@ class Agent {
|
|
|
1730
2086
|
// This handles the case where Claude finished and shows a new empty prompt
|
|
1731
2087
|
if (lastPromptIdx >= 0) {
|
|
1732
2088
|
const lastPromptLine = lines[lastPromptIdx];
|
|
1733
|
-
const isEmptyPrompt =
|
|
1734
|
-
|
|
2089
|
+
const isEmptyPrompt =
|
|
2090
|
+
lastPromptLine.trim() === this.promptSymbol || lastPromptLine.match(/^❯\s*$/);
|
|
1735
2091
|
if (isEmptyPrompt) {
|
|
1736
2092
|
// Find the previous prompt (user's input) and extract content between
|
|
1737
2093
|
// Note: [Pasted text is Claude's truncated output indicator, NOT a prompt
|
|
1738
|
-
const prevPromptIdx = lines
|
|
1739
|
-
(
|
|
1740
|
-
|
|
2094
|
+
const prevPromptIdx = lines
|
|
2095
|
+
.slice(0, lastPromptIdx)
|
|
2096
|
+
.findLastIndex((/** @type {string} */ l) => l.startsWith(this.promptSymbol));
|
|
1741
2097
|
if (prevPromptIdx >= 0) {
|
|
1742
2098
|
const betweenPrompts = lines
|
|
1743
2099
|
.slice(prevPromptIdx + 1, lastPromptIdx)
|
|
@@ -1758,23 +2114,25 @@ class Agent {
|
|
|
1758
2114
|
* @returns {string}
|
|
1759
2115
|
*/
|
|
1760
2116
|
cleanResponse(response) {
|
|
1761
|
-
return
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2117
|
+
return (
|
|
2118
|
+
response
|
|
2119
|
+
// Remove tool call lines (Search, Read, Grep, etc.)
|
|
2120
|
+
.replace(/^[⏺•]\s*(Search|Read|Grep|Glob|Write|Edit|Bash)\([^)]*\).*$/gm, "")
|
|
2121
|
+
// Remove tool result lines
|
|
2122
|
+
.replace(/^⎿\s+.*$/gm, "")
|
|
2123
|
+
// Remove "Sautéed for Xs" timing lines
|
|
2124
|
+
.replace(/^✻\s+Sautéed for.*$/gm, "")
|
|
2125
|
+
// Remove expand hints
|
|
2126
|
+
.replace(/\(ctrl\+o to expand\)/g, "")
|
|
2127
|
+
// Clean up multiple blank lines
|
|
2128
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
2129
|
+
// Original cleanup
|
|
2130
|
+
.replace(/^[•⏺-]\s*/, "")
|
|
2131
|
+
.replace(/^\*\*(.+)\*\*/, "$1")
|
|
2132
|
+
.replace(/\n /g, "\n")
|
|
2133
|
+
.replace(/─+\s*$/, "")
|
|
2134
|
+
.trim()
|
|
2135
|
+
);
|
|
1778
2136
|
}
|
|
1779
2137
|
|
|
1780
2138
|
/**
|
|
@@ -1815,6 +2173,7 @@ class Agent {
|
|
|
1815
2173
|
|
|
1816
2174
|
const CodexAgent = new Agent({
|
|
1817
2175
|
name: "codex",
|
|
2176
|
+
displayName: "Codex",
|
|
1818
2177
|
startCommand: "codex --sandbox read-only",
|
|
1819
2178
|
yoloCommand: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
1820
2179
|
promptSymbol: "›",
|
|
@@ -1834,6 +2193,7 @@ const CodexAgent = new Agent({
|
|
|
1834
2193
|
chromePatterns: ["context left", "for shortcuts"],
|
|
1835
2194
|
reviewOptions: { pr: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
1836
2195
|
envVar: "AX_SESSION",
|
|
2196
|
+
logPathFinder: findCodexLogPath,
|
|
1837
2197
|
});
|
|
1838
2198
|
|
|
1839
2199
|
// =============================================================================
|
|
@@ -1842,12 +2202,15 @@ const CodexAgent = new Agent({
|
|
|
1842
2202
|
|
|
1843
2203
|
const ClaudeAgent = new Agent({
|
|
1844
2204
|
name: "claude",
|
|
2205
|
+
displayName: "Claude",
|
|
1845
2206
|
startCommand: "claude",
|
|
1846
2207
|
yoloCommand: "claude --dangerously-skip-permissions",
|
|
1847
2208
|
promptSymbol: "❯",
|
|
1848
|
-
|
|
2209
|
+
// Claude Code spinners: ·✢✳✶✻✽ (from cli.js source)
|
|
2210
|
+
spinners: ["·", "✢", "✳", "✶", "✻", "✽"],
|
|
1849
2211
|
rateLimitPattern: /rate.?limit/i,
|
|
1850
|
-
|
|
2212
|
+
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2213
|
+
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
1851
2214
|
confirmPatterns: [
|
|
1852
2215
|
"Do you want to make this edit",
|
|
1853
2216
|
"Do you want to run this command",
|
|
@@ -1857,12 +2220,28 @@ const ClaudeAgent = new Agent({
|
|
|
1857
2220
|
],
|
|
1858
2221
|
updatePromptPatterns: null,
|
|
1859
2222
|
responseMarkers: ["⏺", "•", "- ", "**"],
|
|
1860
|
-
chromePatterns: [
|
|
2223
|
+
chromePatterns: [
|
|
2224
|
+
"↵ send",
|
|
2225
|
+
"Esc to cancel",
|
|
2226
|
+
"shortcuts",
|
|
2227
|
+
"for more options",
|
|
2228
|
+
"docs.anthropic.com",
|
|
2229
|
+
"⏵⏵",
|
|
2230
|
+
"bypass permissions",
|
|
2231
|
+
"shift+Tab to cycle",
|
|
2232
|
+
],
|
|
1861
2233
|
reviewOptions: null,
|
|
1862
|
-
safeAllowedTools: "Bash(git:*) Read Glob Grep",
|
|
2234
|
+
safeAllowedTools: "Bash(git:*) Read Glob Grep", // Default: auto-approve read-only tools
|
|
1863
2235
|
envVar: "AX_SESSION",
|
|
1864
2236
|
approveKey: "1",
|
|
1865
2237
|
rejectKey: "Escape",
|
|
2238
|
+
sessionIdFlag: "--session-id",
|
|
2239
|
+
logPathFinder: (sessionName) => {
|
|
2240
|
+
const parsed = parseSessionName(sessionName);
|
|
2241
|
+
const uuid = parsed?.uuid;
|
|
2242
|
+
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2243
|
+
return null;
|
|
2244
|
+
},
|
|
1866
2245
|
});
|
|
1867
2246
|
|
|
1868
2247
|
// =============================================================================
|
|
@@ -1883,7 +2262,11 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1883
2262
|
const initialState = agent.getState(initialScreen);
|
|
1884
2263
|
|
|
1885
2264
|
// Already in terminal state
|
|
1886
|
-
if (
|
|
2265
|
+
if (
|
|
2266
|
+
initialState === State.RATE_LIMITED ||
|
|
2267
|
+
initialState === State.CONFIRMING ||
|
|
2268
|
+
initialState === State.READY
|
|
2269
|
+
) {
|
|
1887
2270
|
return { state: initialState, screen: initialScreen };
|
|
1888
2271
|
}
|
|
1889
2272
|
|
|
@@ -1896,30 +2279,38 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1896
2279
|
return { state, screen };
|
|
1897
2280
|
}
|
|
1898
2281
|
}
|
|
1899
|
-
throw new
|
|
2282
|
+
throw new TimeoutError(session);
|
|
1900
2283
|
}
|
|
1901
2284
|
|
|
1902
2285
|
/**
|
|
1903
|
-
*
|
|
1904
|
-
* Waits for screen activity before considering the response complete.
|
|
2286
|
+
* Core polling loop for waiting on agent responses.
|
|
1905
2287
|
* @param {Agent} agent
|
|
1906
2288
|
* @param {string} session
|
|
1907
|
-
* @param {number}
|
|
2289
|
+
* @param {number} timeoutMs
|
|
2290
|
+
* @param {{onPoll?: (screen: string, state: string) => void, onStateChange?: (state: string, lastState: string | null, screen: string) => void, onReady?: (screen: string) => void}} [hooks]
|
|
1908
2291
|
* @returns {Promise<{state: string, screen: string}>}
|
|
1909
2292
|
*/
|
|
1910
|
-
async function
|
|
2293
|
+
async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
2294
|
+
const { onPoll, onStateChange, onReady } = hooks;
|
|
1911
2295
|
const start = Date.now();
|
|
1912
2296
|
const initialScreen = tmuxCapture(session);
|
|
1913
2297
|
|
|
1914
2298
|
let lastScreen = initialScreen;
|
|
2299
|
+
let lastState = null;
|
|
1915
2300
|
let stableAt = null;
|
|
1916
2301
|
let sawActivity = false;
|
|
1917
2302
|
|
|
1918
2303
|
while (Date.now() - start < timeoutMs) {
|
|
1919
|
-
await sleep(POLL_MS);
|
|
1920
2304
|
const screen = tmuxCapture(session);
|
|
1921
2305
|
const state = agent.getState(screen);
|
|
1922
2306
|
|
|
2307
|
+
if (onPoll) onPoll(screen, state);
|
|
2308
|
+
|
|
2309
|
+
if (state !== lastState) {
|
|
2310
|
+
if (onStateChange) onStateChange(state, lastState, screen);
|
|
2311
|
+
lastState = state;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
1923
2314
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING) {
|
|
1924
2315
|
return { state, screen };
|
|
1925
2316
|
}
|
|
@@ -1934,6 +2325,7 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1934
2325
|
|
|
1935
2326
|
if (sawActivity && stableAt && Date.now() - stableAt >= STABLE_MS) {
|
|
1936
2327
|
if (state === State.READY) {
|
|
2328
|
+
if (onReady) onReady(screen);
|
|
1937
2329
|
return { state, screen };
|
|
1938
2330
|
}
|
|
1939
2331
|
}
|
|
@@ -1941,26 +2333,86 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1941
2333
|
if (state === State.THINKING) {
|
|
1942
2334
|
sawActivity = true;
|
|
1943
2335
|
}
|
|
2336
|
+
|
|
2337
|
+
await sleep(POLL_MS);
|
|
1944
2338
|
}
|
|
1945
|
-
throw new
|
|
2339
|
+
throw new TimeoutError(session);
|
|
1946
2340
|
}
|
|
1947
2341
|
|
|
1948
2342
|
/**
|
|
1949
|
-
*
|
|
1950
|
-
*
|
|
2343
|
+
* Wait for agent response without streaming output.
|
|
2344
|
+
* @param {Agent} agent
|
|
2345
|
+
* @param {string} session
|
|
2346
|
+
* @param {number} [timeoutMs]
|
|
2347
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2348
|
+
*/
|
|
2349
|
+
async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2350
|
+
return pollForResponse(agent, session, timeoutMs);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/**
|
|
2354
|
+
* Wait for agent response with streaming output to console.
|
|
1951
2355
|
* @param {Agent} agent
|
|
1952
2356
|
* @param {string} session
|
|
1953
2357
|
* @param {number} [timeoutMs]
|
|
1954
2358
|
* @returns {Promise<{state: string, screen: string}>}
|
|
1955
2359
|
*/
|
|
1956
|
-
async function
|
|
2360
|
+
async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2361
|
+
let logPath = agent.findLogPath(session);
|
|
2362
|
+
let logOffset = logPath && existsSync(logPath) ? statSync(logPath).size : 0;
|
|
2363
|
+
let printedThinking = false;
|
|
2364
|
+
|
|
2365
|
+
const streamNewEntries = () => {
|
|
2366
|
+
if (!logPath) {
|
|
2367
|
+
logPath = agent.findLogPath(session);
|
|
2368
|
+
if (logPath && existsSync(logPath)) {
|
|
2369
|
+
logOffset = statSync(logPath).size;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (logPath) {
|
|
2373
|
+
const { entries, newOffset } = tailJsonl(logPath, logOffset);
|
|
2374
|
+
logOffset = newOffset;
|
|
2375
|
+
for (const entry of entries) {
|
|
2376
|
+
const formatted = formatEntry(entry);
|
|
2377
|
+
if (formatted) console.log(formatted);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
|
|
2382
|
+
return pollForResponse(agent, session, timeoutMs, {
|
|
2383
|
+
onPoll: () => streamNewEntries(),
|
|
2384
|
+
onStateChange: (state, lastState, screen) => {
|
|
2385
|
+
if (state === State.THINKING && !printedThinking) {
|
|
2386
|
+
console.log("[THINKING]");
|
|
2387
|
+
printedThinking = true;
|
|
2388
|
+
} else if (state === State.CONFIRMING) {
|
|
2389
|
+
const pendingTool = extractPendingToolFromScreen(screen);
|
|
2390
|
+
console.log(pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]");
|
|
2391
|
+
}
|
|
2392
|
+
if (lastState === State.THINKING && state !== State.THINKING) {
|
|
2393
|
+
printedThinking = false;
|
|
2394
|
+
}
|
|
2395
|
+
},
|
|
2396
|
+
onReady: () => streamNewEntries(),
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
/**
|
|
2401
|
+
* Auto-approve loop that keeps approving confirmations until the agent is ready or rate limited.
|
|
2402
|
+
* @param {Agent} agent
|
|
2403
|
+
* @param {string} session
|
|
2404
|
+
* @param {number} timeoutMs
|
|
2405
|
+
* @param {Function} waitFn - waitForResponse or streamResponse
|
|
2406
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2407
|
+
*/
|
|
2408
|
+
async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
1957
2409
|
const deadline = Date.now() + timeoutMs;
|
|
1958
2410
|
|
|
1959
2411
|
while (Date.now() < deadline) {
|
|
1960
2412
|
const remaining = deadline - Date.now();
|
|
1961
2413
|
if (remaining <= 0) break;
|
|
1962
2414
|
|
|
1963
|
-
const { state, screen } = await
|
|
2415
|
+
const { state, screen } = await waitFn(agent, session, remaining);
|
|
1964
2416
|
|
|
1965
2417
|
if (state === State.RATE_LIMITED || state === State.READY) {
|
|
1966
2418
|
return { state, screen };
|
|
@@ -1972,11 +2424,10 @@ async function autoApproveLoop(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1972
2424
|
continue;
|
|
1973
2425
|
}
|
|
1974
2426
|
|
|
1975
|
-
// Unexpected state - log and continue polling
|
|
1976
2427
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
1977
2428
|
}
|
|
1978
2429
|
|
|
1979
|
-
throw new
|
|
2430
|
+
throw new TimeoutError(session);
|
|
1980
2431
|
}
|
|
1981
2432
|
|
|
1982
2433
|
/**
|
|
@@ -2035,9 +2486,22 @@ function cmdAgents() {
|
|
|
2035
2486
|
|
|
2036
2487
|
if (agentSessions.length === 0) {
|
|
2037
2488
|
console.log("No agents running");
|
|
2489
|
+
// Still check for orphans
|
|
2490
|
+
const orphans = findOrphanedProcesses();
|
|
2491
|
+
if (orphans.length > 0) {
|
|
2492
|
+
console.log(`\nOrphaned (${orphans.length}):`);
|
|
2493
|
+
for (const { pid, command } of orphans) {
|
|
2494
|
+
console.log(` PID ${pid}: ${command}`);
|
|
2495
|
+
}
|
|
2496
|
+
console.log(`\n Run 'ax kill --orphans' to clean up`);
|
|
2497
|
+
}
|
|
2038
2498
|
return;
|
|
2039
2499
|
}
|
|
2040
2500
|
|
|
2501
|
+
// Get default session for each agent type
|
|
2502
|
+
const claudeDefault = ClaudeAgent.getDefaultSession();
|
|
2503
|
+
const codexDefault = CodexAgent.getDefaultSession();
|
|
2504
|
+
|
|
2041
2505
|
// Get info for each agent
|
|
2042
2506
|
const agents = agentSessions.map((session) => {
|
|
2043
2507
|
const parsed = /** @type {ParsedSession} */ (parseSessionName(session));
|
|
@@ -2046,30 +2510,45 @@ function cmdAgents() {
|
|
|
2046
2510
|
const state = agent.getState(screen);
|
|
2047
2511
|
const logPath = agent.findLogPath(session);
|
|
2048
2512
|
const type = parsed.archangelName ? "archangel" : "-";
|
|
2513
|
+
const isDefault =
|
|
2514
|
+
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
2515
|
+
(parsed.tool === "codex" && session === codexDefault);
|
|
2049
2516
|
|
|
2050
2517
|
return {
|
|
2051
2518
|
session,
|
|
2052
2519
|
tool: parsed.tool,
|
|
2053
2520
|
state: state || "unknown",
|
|
2521
|
+
target: isDefault ? "*" : "",
|
|
2054
2522
|
type,
|
|
2055
2523
|
log: logPath || "-",
|
|
2056
2524
|
};
|
|
2057
2525
|
});
|
|
2058
2526
|
|
|
2059
|
-
// Print table
|
|
2527
|
+
// Print sessions table
|
|
2060
2528
|
const maxSession = Math.max(7, ...agents.map((a) => a.session.length));
|
|
2061
2529
|
const maxTool = Math.max(4, ...agents.map((a) => a.tool.length));
|
|
2062
2530
|
const maxState = Math.max(5, ...agents.map((a) => a.state.length));
|
|
2531
|
+
const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
|
|
2063
2532
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2064
2533
|
|
|
2065
2534
|
console.log(
|
|
2066
|
-
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)} LOG
|
|
2535
|
+
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} LOG`,
|
|
2067
2536
|
);
|
|
2068
2537
|
for (const a of agents) {
|
|
2069
2538
|
console.log(
|
|
2070
|
-
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.log}
|
|
2539
|
+
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.log}`,
|
|
2071
2540
|
);
|
|
2072
2541
|
}
|
|
2542
|
+
|
|
2543
|
+
// Print orphaned processes if any
|
|
2544
|
+
const orphans = findOrphanedProcesses();
|
|
2545
|
+
if (orphans.length > 0) {
|
|
2546
|
+
console.log(`\nOrphaned (${orphans.length}):`);
|
|
2547
|
+
for (const { pid, command } of orphans) {
|
|
2548
|
+
console.log(` PID ${pid}: ${command}`);
|
|
2549
|
+
}
|
|
2550
|
+
console.log(`\n Run 'ax kill --orphans' to clean up`);
|
|
2551
|
+
}
|
|
2073
2552
|
}
|
|
2074
2553
|
|
|
2075
2554
|
// =============================================================================
|
|
@@ -2114,7 +2593,9 @@ function startArchangel(config, parentSession = null) {
|
|
|
2114
2593
|
env,
|
|
2115
2594
|
});
|
|
2116
2595
|
child.unref();
|
|
2117
|
-
console.log(
|
|
2596
|
+
console.log(
|
|
2597
|
+
`Summoning: ${config.name} (pid ${child.pid})${parentSession ? ` [parent: ${parentSession.session}]` : ""}`,
|
|
2598
|
+
);
|
|
2118
2599
|
}
|
|
2119
2600
|
|
|
2120
2601
|
// =============================================================================
|
|
@@ -2150,7 +2631,9 @@ async function cmdArchangel(agentName) {
|
|
|
2150
2631
|
// Check agent CLI is installed before trying to start
|
|
2151
2632
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
2152
2633
|
if (cliCheck.status !== 0) {
|
|
2153
|
-
console.error(
|
|
2634
|
+
console.error(
|
|
2635
|
+
`[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`,
|
|
2636
|
+
);
|
|
2154
2637
|
process.exit(1);
|
|
2155
2638
|
}
|
|
2156
2639
|
|
|
@@ -2212,7 +2695,7 @@ async function cmdArchangel(agentName) {
|
|
|
2212
2695
|
isProcessing = true;
|
|
2213
2696
|
|
|
2214
2697
|
const files = [...changedFiles];
|
|
2215
|
-
changedFiles = new Set();
|
|
2698
|
+
changedFiles = new Set(); // atomic swap to avoid losing changes during processing
|
|
2216
2699
|
|
|
2217
2700
|
try {
|
|
2218
2701
|
// Get parent session log path for JSONL extraction
|
|
@@ -2221,7 +2704,8 @@ async function cmdArchangel(agentName) {
|
|
|
2221
2704
|
|
|
2222
2705
|
// Build file-specific context from JSONL
|
|
2223
2706
|
const fileContexts = [];
|
|
2224
|
-
for (const file of files.slice(0, 5)) {
|
|
2707
|
+
for (const file of files.slice(0, 5)) {
|
|
2708
|
+
// Limit to 5 files
|
|
2225
2709
|
const ctx = extractFileEditContext(logPath, file);
|
|
2226
2710
|
if (ctx) {
|
|
2227
2711
|
fileContexts.push({ file, ...ctx });
|
|
@@ -2248,26 +2732,34 @@ async function cmdArchangel(agentName) {
|
|
|
2248
2732
|
}
|
|
2249
2733
|
|
|
2250
2734
|
if (ctx.readsBefore.length > 0) {
|
|
2251
|
-
const reads = ctx.readsBefore.map(f => f.split("/").pop()).join(", ");
|
|
2735
|
+
const reads = ctx.readsBefore.map((f) => f.split("/").pop()).join(", ");
|
|
2252
2736
|
prompt += `**Files read before:** ${reads}\n`;
|
|
2253
2737
|
}
|
|
2254
2738
|
}
|
|
2255
2739
|
|
|
2256
2740
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
2257
2741
|
|
|
2258
|
-
const gitContext = buildGitContext(
|
|
2742
|
+
const gitContext = buildGitContext(
|
|
2743
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2744
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2745
|
+
);
|
|
2259
2746
|
if (gitContext) {
|
|
2260
2747
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2261
2748
|
}
|
|
2262
2749
|
|
|
2263
|
-
prompt +=
|
|
2750
|
+
prompt +=
|
|
2751
|
+
'\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
|
|
2264
2752
|
} else {
|
|
2265
2753
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2266
2754
|
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
2267
|
-
const gitContext = buildGitContext(
|
|
2755
|
+
const gitContext = buildGitContext(
|
|
2756
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2757
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2758
|
+
);
|
|
2268
2759
|
|
|
2269
2760
|
if (parentContext) {
|
|
2270
|
-
prompt +=
|
|
2761
|
+
prompt +=
|
|
2762
|
+
"\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
|
|
2271
2763
|
}
|
|
2272
2764
|
|
|
2273
2765
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
@@ -2276,10 +2768,10 @@ async function cmdArchangel(agentName) {
|
|
|
2276
2768
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2277
2769
|
}
|
|
2278
2770
|
|
|
2279
|
-
prompt +=
|
|
2771
|
+
prompt +=
|
|
2772
|
+
'\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
|
|
2280
2773
|
}
|
|
2281
2774
|
|
|
2282
|
-
|
|
2283
2775
|
// Check session still exists
|
|
2284
2776
|
if (!tmuxHasSession(sessionName)) {
|
|
2285
2777
|
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
@@ -2308,22 +2800,30 @@ async function cmdArchangel(agentName) {
|
|
|
2308
2800
|
await sleep(100); // Ensure Enter is processed
|
|
2309
2801
|
|
|
2310
2802
|
// Wait for response
|
|
2311
|
-
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2803
|
+
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2804
|
+
agent,
|
|
2805
|
+
sessionName,
|
|
2806
|
+
ARCHANGEL_RESPONSE_TIMEOUT_MS,
|
|
2807
|
+
);
|
|
2312
2808
|
|
|
2313
2809
|
if (endState === State.RATE_LIMITED) {
|
|
2314
2810
|
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2315
2811
|
process.exit(2);
|
|
2316
2812
|
}
|
|
2317
2813
|
|
|
2318
|
-
|
|
2319
2814
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2320
2815
|
|
|
2321
2816
|
// Sanity check: skip garbage responses (screen scraping artifacts)
|
|
2322
|
-
const isGarbage =
|
|
2817
|
+
const isGarbage =
|
|
2818
|
+
cleanedResponse.includes("[Pasted text") ||
|
|
2323
2819
|
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2324
2820
|
cleanedResponse.length < 20;
|
|
2325
2821
|
|
|
2326
|
-
if (
|
|
2822
|
+
if (
|
|
2823
|
+
cleanedResponse &&
|
|
2824
|
+
!isGarbage &&
|
|
2825
|
+
!cleanedResponse.toLowerCase().includes("no issues found")
|
|
2826
|
+
) {
|
|
2327
2827
|
writeToMailbox({
|
|
2328
2828
|
agent: /** @type {string} */ (agentName),
|
|
2329
2829
|
session: sessionName,
|
|
@@ -2345,7 +2845,10 @@ async function cmdArchangel(agentName) {
|
|
|
2345
2845
|
|
|
2346
2846
|
function scheduleProcessChanges() {
|
|
2347
2847
|
processChanges().catch((err) => {
|
|
2348
|
-
console.error(
|
|
2848
|
+
console.error(
|
|
2849
|
+
`[archangel:${agentName}] Unhandled error:`,
|
|
2850
|
+
err instanceof Error ? err.message : err,
|
|
2851
|
+
);
|
|
2349
2852
|
});
|
|
2350
2853
|
}
|
|
2351
2854
|
|
|
@@ -2499,7 +3002,7 @@ async function cmdRecall(name = null) {
|
|
|
2499
3002
|
}
|
|
2500
3003
|
|
|
2501
3004
|
// Version of the hook script template - bump when making changes
|
|
2502
|
-
const HOOK_SCRIPT_VERSION = "
|
|
3005
|
+
const HOOK_SCRIPT_VERSION = "4";
|
|
2503
3006
|
|
|
2504
3007
|
function ensureMailboxHookScript() {
|
|
2505
3008
|
const hooksDir = HOOKS_DIR;
|
|
@@ -2522,24 +3025,49 @@ ${versionMarker}
|
|
|
2522
3025
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2523
3026
|
import { dirname, join } from "node:path";
|
|
2524
3027
|
import { fileURLToPath } from "node:url";
|
|
3028
|
+
import { createHash } from "node:crypto";
|
|
2525
3029
|
|
|
2526
3030
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2527
3031
|
const AI_DIR = join(__dirname, "..");
|
|
2528
3032
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
2529
3033
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
2530
|
-
const LAST_SEEN = join(AI_DIR, "mailbox-last-seen");
|
|
2531
3034
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2532
3035
|
|
|
3036
|
+
// Read hook input from stdin
|
|
3037
|
+
let hookInput = {};
|
|
3038
|
+
try {
|
|
3039
|
+
const stdinData = readFileSync(0, "utf-8").trim();
|
|
3040
|
+
if (stdinData) hookInput = JSON.parse(stdinData);
|
|
3041
|
+
} catch (err) {
|
|
3042
|
+
if (DEBUG) console.error("[hook] stdin parse:", err.message);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
const sessionId = hookInput.session_id || "";
|
|
3046
|
+
const hookEvent = hookInput.hook_event_name || "";
|
|
3047
|
+
|
|
3048
|
+
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
3049
|
+
|
|
3050
|
+
// NO-OP for archangel or partner sessions
|
|
3051
|
+
if (sessionId.includes("-archangel-") || sessionId.includes("-partner-")) {
|
|
3052
|
+
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3053
|
+
process.exit(0);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// Per-session last-seen tracking (single JSON file, self-cleaning)
|
|
3057
|
+
const sessionHash = sessionId ? createHash("md5").update(sessionId).digest("hex").slice(0, 8) : "default";
|
|
3058
|
+
const LAST_SEEN_FILE = join(AI_DIR, "mailbox-last-seen.json");
|
|
3059
|
+
|
|
2533
3060
|
if (!existsSync(MAILBOX)) process.exit(0);
|
|
2534
3061
|
|
|
2535
|
-
let
|
|
3062
|
+
let lastSeenMap = {};
|
|
2536
3063
|
try {
|
|
2537
|
-
if (existsSync(
|
|
2538
|
-
|
|
3064
|
+
if (existsSync(LAST_SEEN_FILE)) {
|
|
3065
|
+
lastSeenMap = JSON.parse(readFileSync(LAST_SEEN_FILE, "utf-8"));
|
|
2539
3066
|
}
|
|
2540
3067
|
} catch (err) {
|
|
2541
3068
|
if (DEBUG) console.error("[hook] readLastSeen:", err.message);
|
|
2542
3069
|
}
|
|
3070
|
+
const lastSeen = lastSeenMap[sessionHash] || 0;
|
|
2543
3071
|
|
|
2544
3072
|
const now = Date.now();
|
|
2545
3073
|
const lines = readFileSync(MAILBOX, "utf-8").trim().split("\\n").filter(Boolean);
|
|
@@ -2561,21 +3089,39 @@ for (const line of lines) {
|
|
|
2561
3089
|
}
|
|
2562
3090
|
|
|
2563
3091
|
if (relevant.length > 0) {
|
|
2564
|
-
console.log("## Background Agents");
|
|
2565
|
-
console.log("");
|
|
2566
|
-
console.log("Background agents watching your files found:");
|
|
2567
|
-
console.log("");
|
|
2568
3092
|
const sessionPrefixes = new Set();
|
|
3093
|
+
let messageLines = [];
|
|
3094
|
+
messageLines.push("## Background Agents");
|
|
3095
|
+
messageLines.push("");
|
|
3096
|
+
messageLines.push("Background agents watching your files found:");
|
|
3097
|
+
messageLines.push("");
|
|
2569
3098
|
for (const { agent, sessionPrefix, message } of relevant) {
|
|
2570
3099
|
if (sessionPrefix) sessionPrefixes.add(sessionPrefix);
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
3100
|
+
messageLines.push("**[" + agent + "]**");
|
|
3101
|
+
messageLines.push("");
|
|
3102
|
+
messageLines.push(message);
|
|
3103
|
+
messageLines.push("");
|
|
2575
3104
|
}
|
|
2576
3105
|
const sessionList = [...sessionPrefixes].map(s => "\\\`./ax.js log " + s + "\\\`").join(" or ");
|
|
2577
|
-
|
|
2578
|
-
|
|
3106
|
+
messageLines.push("> For more context: \\\`./ax.js mailbox\\\`" + (sessionList ? " or " + sessionList : ""));
|
|
3107
|
+
|
|
3108
|
+
const formattedMessage = messageLines.join("\\n");
|
|
3109
|
+
|
|
3110
|
+
// For Stop hook, return blocking JSON to force acknowledgment
|
|
3111
|
+
if (hookEvent === "Stop") {
|
|
3112
|
+
console.log(JSON.stringify({ decision: "block", reason: formattedMessage }));
|
|
3113
|
+
} else {
|
|
3114
|
+
// For other hooks, just output the context
|
|
3115
|
+
console.log(formattedMessage);
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// Update last-seen and prune entries older than 24 hours
|
|
3119
|
+
const PRUNE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
3120
|
+
lastSeenMap[sessionHash] = now;
|
|
3121
|
+
for (const key of Object.keys(lastSeenMap)) {
|
|
3122
|
+
if (now - lastSeenMap[key] > PRUNE_AGE_MS) delete lastSeenMap[key];
|
|
3123
|
+
}
|
|
3124
|
+
writeFileSync(LAST_SEEN_FILE, JSON.stringify(lastSeenMap));
|
|
2579
3125
|
}
|
|
2580
3126
|
|
|
2581
3127
|
process.exit(0);
|
|
@@ -2590,18 +3136,9 @@ process.exit(0);
|
|
|
2590
3136
|
console.log(`\nTo enable manually, add to .claude/settings.json:\n`);
|
|
2591
3137
|
console.log(`{
|
|
2592
3138
|
"hooks": {
|
|
2593
|
-
"UserPromptSubmit": [
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
"hooks": [
|
|
2597
|
-
{
|
|
2598
|
-
"type": "command",
|
|
2599
|
-
"command": "node .ai/hooks/mailbox-inject.js",
|
|
2600
|
-
"timeout": 5
|
|
2601
|
-
}
|
|
2602
|
-
]
|
|
2603
|
-
}
|
|
2604
|
-
]
|
|
3139
|
+
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3140
|
+
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3141
|
+
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }]
|
|
2605
3142
|
}
|
|
2606
3143
|
}`);
|
|
2607
3144
|
}
|
|
@@ -2611,6 +3148,7 @@ function ensureClaudeHookConfig() {
|
|
|
2611
3148
|
const settingsDir = ".claude";
|
|
2612
3149
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
2613
3150
|
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
3151
|
+
const hookEvents = ["UserPromptSubmit", "PostToolUse", "Stop"];
|
|
2614
3152
|
|
|
2615
3153
|
try {
|
|
2616
3154
|
/** @type {ClaudeSettings} */
|
|
@@ -2629,33 +3167,41 @@ function ensureClaudeHookConfig() {
|
|
|
2629
3167
|
|
|
2630
3168
|
// Ensure hooks structure exists
|
|
2631
3169
|
if (!settings.hooks) settings.hooks = {};
|
|
2632
|
-
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
2633
|
-
|
|
2634
|
-
// Check if our hook is already configured
|
|
2635
|
-
const hookExists = settings.hooks.UserPromptSubmit.some(
|
|
2636
|
-
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
2637
|
-
(entry) => entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand)
|
|
2638
|
-
);
|
|
2639
3170
|
|
|
2640
|
-
|
|
2641
|
-
|
|
3171
|
+
let anyAdded = false;
|
|
3172
|
+
|
|
3173
|
+
for (const eventName of hookEvents) {
|
|
3174
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
3175
|
+
|
|
3176
|
+
// Check if our hook is already configured for this event
|
|
3177
|
+
const hookExists = settings.hooks[eventName].some(
|
|
3178
|
+
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
3179
|
+
(entry) =>
|
|
3180
|
+
entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand),
|
|
3181
|
+
);
|
|
3182
|
+
|
|
3183
|
+
if (!hookExists) {
|
|
3184
|
+
// Add the hook for this event
|
|
3185
|
+
settings.hooks[eventName].push({
|
|
3186
|
+
matcher: "",
|
|
3187
|
+
hooks: [
|
|
3188
|
+
{
|
|
3189
|
+
type: "command",
|
|
3190
|
+
command: hookCommand,
|
|
3191
|
+
timeout: 5,
|
|
3192
|
+
},
|
|
3193
|
+
],
|
|
3194
|
+
});
|
|
3195
|
+
anyAdded = true;
|
|
3196
|
+
}
|
|
2642
3197
|
}
|
|
2643
3198
|
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
hooks:
|
|
2648
|
-
|
|
2649
|
-
type: "command",
|
|
2650
|
-
command: hookCommand,
|
|
2651
|
-
timeout: 5,
|
|
2652
|
-
},
|
|
2653
|
-
],
|
|
2654
|
-
});
|
|
3199
|
+
if (anyAdded) {
|
|
3200
|
+
// Write settings
|
|
3201
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3202
|
+
console.log(`Configured hooks in: ${settingsPath}`);
|
|
3203
|
+
}
|
|
2655
3204
|
|
|
2656
|
-
// Write settings
|
|
2657
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2658
|
-
console.log(`Configured hook in: ${settingsPath}`);
|
|
2659
3205
|
return true;
|
|
2660
3206
|
} catch {
|
|
2661
3207
|
// If we can't configure automatically, return false so manual instructions are shown
|
|
@@ -2665,9 +3211,31 @@ function ensureClaudeHookConfig() {
|
|
|
2665
3211
|
|
|
2666
3212
|
/**
|
|
2667
3213
|
* @param {string | null | undefined} session
|
|
2668
|
-
* @param {{all?: boolean}} [options]
|
|
3214
|
+
* @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
|
|
2669
3215
|
*/
|
|
2670
|
-
function cmdKill(session, { all = false } = {}) {
|
|
3216
|
+
function cmdKill(session, { all = false, orphans = false, force = false } = {}) {
|
|
3217
|
+
// Handle orphaned processes
|
|
3218
|
+
if (orphans) {
|
|
3219
|
+
const orphanedProcesses = findOrphanedProcesses();
|
|
3220
|
+
|
|
3221
|
+
if (orphanedProcesses.length === 0) {
|
|
3222
|
+
console.log("No orphaned processes found");
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
const signal = force ? "-9" : "-15"; // SIGKILL vs SIGTERM
|
|
3227
|
+
let killed = 0;
|
|
3228
|
+
for (const { pid, command } of orphanedProcesses) {
|
|
3229
|
+
const result = spawnSync("kill", [signal, pid]);
|
|
3230
|
+
if (result.status === 0) {
|
|
3231
|
+
console.log(`Killed: PID ${pid} (${command.slice(0, 40)})`);
|
|
3232
|
+
killed++;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
console.log(`Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
2671
3239
|
// If specific session provided, kill just that one
|
|
2672
3240
|
if (session) {
|
|
2673
3241
|
if (!tmuxHasSession(session)) {
|
|
@@ -2727,7 +3295,9 @@ function cmdAttach(session) {
|
|
|
2727
3295
|
}
|
|
2728
3296
|
|
|
2729
3297
|
// Hand over to tmux attach
|
|
2730
|
-
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
3298
|
+
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
3299
|
+
stdio: "inherit",
|
|
3300
|
+
});
|
|
2731
3301
|
process.exit(result.status || 0);
|
|
2732
3302
|
}
|
|
2733
3303
|
|
|
@@ -2786,13 +3356,15 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2786
3356
|
|
|
2787
3357
|
if (newLines.length === 0) return;
|
|
2788
3358
|
|
|
2789
|
-
const entries = newLines
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
3359
|
+
const entries = newLines
|
|
3360
|
+
.map((line) => {
|
|
3361
|
+
try {
|
|
3362
|
+
return JSON.parse(line);
|
|
3363
|
+
} catch {
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
})
|
|
3367
|
+
.filter(Boolean);
|
|
2796
3368
|
|
|
2797
3369
|
const output = [];
|
|
2798
3370
|
if (isInitial) {
|
|
@@ -2805,7 +3377,10 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2805
3377
|
const ts = entry.timestamp || entry.ts || entry.createdAt;
|
|
2806
3378
|
if (ts && ts !== lastTimestamp) {
|
|
2807
3379
|
const date = new Date(ts);
|
|
2808
|
-
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
3380
|
+
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
3381
|
+
hour: "2-digit",
|
|
3382
|
+
minute: "2-digit",
|
|
3383
|
+
});
|
|
2809
3384
|
if (formatted.isUserMessage) {
|
|
2810
3385
|
output.push(`\n### ${timeStr}\n`);
|
|
2811
3386
|
}
|
|
@@ -2855,7 +3430,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2855
3430
|
if (type === "user" || type === "human") {
|
|
2856
3431
|
const text = extractTextContent(content);
|
|
2857
3432
|
if (text) {
|
|
2858
|
-
return {
|
|
3433
|
+
return {
|
|
3434
|
+
text: `**User**: ${truncate(text, TRUNCATE_USER_LEN)}\n`,
|
|
3435
|
+
isUserMessage: true,
|
|
3436
|
+
};
|
|
2859
3437
|
}
|
|
2860
3438
|
}
|
|
2861
3439
|
|
|
@@ -2871,10 +3449,12 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2871
3449
|
// Extract tool calls (compressed)
|
|
2872
3450
|
const tools = extractToolCalls(content);
|
|
2873
3451
|
if (tools.length > 0) {
|
|
2874
|
-
const toolSummary = tools
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3452
|
+
const toolSummary = tools
|
|
3453
|
+
.map((t) => {
|
|
3454
|
+
if (t.error) return `${t.name}(${t.target}) ✗`;
|
|
3455
|
+
return `${t.name}(${t.target})`;
|
|
3456
|
+
})
|
|
3457
|
+
.join(", ");
|
|
2878
3458
|
parts.push(`> ${toolSummary}\n`);
|
|
2879
3459
|
}
|
|
2880
3460
|
|
|
@@ -2896,7 +3476,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2896
3476
|
const error = entry.error || entry.is_error;
|
|
2897
3477
|
if (error) {
|
|
2898
3478
|
const name = entry.tool_name || entry.name || "tool";
|
|
2899
|
-
return {
|
|
3479
|
+
return {
|
|
3480
|
+
text: `> ${name} ✗ (${truncate(String(error), 100)})\n`,
|
|
3481
|
+
isUserMessage: false,
|
|
3482
|
+
};
|
|
2900
3483
|
}
|
|
2901
3484
|
}
|
|
2902
3485
|
|
|
@@ -2933,7 +3516,8 @@ function extractToolCalls(content) {
|
|
|
2933
3516
|
const name = c.name || c.tool || "tool";
|
|
2934
3517
|
const input = c.input || c.arguments || {};
|
|
2935
3518
|
// Extract a reasonable target from the input
|
|
2936
|
-
const target =
|
|
3519
|
+
const target =
|
|
3520
|
+
input.file_path || input.path || input.command?.slice(0, 30) || input.pattern || "";
|
|
2937
3521
|
const shortTarget = target.split("/").pop() || target.slice(0, 20);
|
|
2938
3522
|
return { name, target: shortTarget, error: c.error };
|
|
2939
3523
|
});
|
|
@@ -2978,8 +3562,14 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
2978
3562
|
|
|
2979
3563
|
for (const entry of entries) {
|
|
2980
3564
|
const ts = new Date(entry.timestamp);
|
|
2981
|
-
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
2982
|
-
|
|
3565
|
+
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3566
|
+
hour: "2-digit",
|
|
3567
|
+
minute: "2-digit",
|
|
3568
|
+
});
|
|
3569
|
+
const dateStr = ts.toLocaleDateString("en-GB", {
|
|
3570
|
+
month: "short",
|
|
3571
|
+
day: "numeric",
|
|
3572
|
+
});
|
|
2983
3573
|
const p = entry.payload || {};
|
|
2984
3574
|
|
|
2985
3575
|
console.log(`### [${p.agent || "unknown"}] ${dateStr} ${timeStr}\n`);
|
|
@@ -3008,7 +3598,7 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3008
3598
|
* @param {string} message
|
|
3009
3599
|
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
3010
3600
|
*/
|
|
3011
|
-
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs } = {}) {
|
|
3601
|
+
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
3012
3602
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3013
3603
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3014
3604
|
|
|
@@ -3020,20 +3610,31 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3020
3610
|
}
|
|
3021
3611
|
|
|
3022
3612
|
/** @type {string} */
|
|
3023
|
-
const activeSession = sessionExists
|
|
3613
|
+
const activeSession = sessionExists
|
|
3614
|
+
? /** @type {string} */ (session)
|
|
3615
|
+
: await cmdStart(agent, session, { yolo });
|
|
3024
3616
|
|
|
3025
3617
|
tmuxSendLiteral(activeSession, message);
|
|
3026
3618
|
await sleep(50);
|
|
3027
3619
|
tmuxSend(activeSession, "Enter");
|
|
3028
3620
|
|
|
3029
|
-
if (noWait)
|
|
3621
|
+
if (noWait) {
|
|
3622
|
+
const parsed = parseSessionName(activeSession);
|
|
3623
|
+
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3624
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
3625
|
+
console.log(`Sent to: ${shortId}
|
|
3626
|
+
|
|
3627
|
+
e.g.
|
|
3628
|
+
${cli} status --session=${shortId}
|
|
3629
|
+
${cli} output --session=${shortId}`);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3030
3632
|
|
|
3031
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3032
3633
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3033
3634
|
|
|
3034
3635
|
const { state, screen } = useAutoApprove
|
|
3035
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3036
|
-
: await
|
|
3636
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3637
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3037
3638
|
|
|
3038
3639
|
if (state === State.RATE_LIMITED) {
|
|
3039
3640
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3041,14 +3642,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3041
3642
|
}
|
|
3042
3643
|
|
|
3043
3644
|
if (state === State.CONFIRMING) {
|
|
3044
|
-
console.log(`CONFIRM: ${
|
|
3645
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3045
3646
|
process.exit(3);
|
|
3046
3647
|
}
|
|
3047
|
-
|
|
3048
|
-
const output = agent.getResponse(activeSession, screen);
|
|
3049
|
-
if (output) {
|
|
3050
|
-
console.log(output);
|
|
3051
|
-
}
|
|
3052
3648
|
}
|
|
3053
3649
|
|
|
3054
3650
|
/**
|
|
@@ -3063,9 +3659,10 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3063
3659
|
}
|
|
3064
3660
|
|
|
3065
3661
|
const before = tmuxCapture(session);
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3662
|
+
const beforeState = agent.getState(before);
|
|
3663
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3664
|
+
console.log(`Already ${beforeState}`);
|
|
3665
|
+
return;
|
|
3069
3666
|
}
|
|
3070
3667
|
|
|
3071
3668
|
tmuxSend(session, agent.approveKey);
|
|
@@ -3080,7 +3677,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3080
3677
|
}
|
|
3081
3678
|
|
|
3082
3679
|
if (state === State.CONFIRMING) {
|
|
3083
|
-
console.log(`CONFIRM: ${
|
|
3680
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3084
3681
|
process.exit(3);
|
|
3085
3682
|
}
|
|
3086
3683
|
|
|
@@ -3099,6 +3696,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3099
3696
|
process.exit(1);
|
|
3100
3697
|
}
|
|
3101
3698
|
|
|
3699
|
+
const before = tmuxCapture(session);
|
|
3700
|
+
const beforeState = agent.getState(before);
|
|
3701
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3702
|
+
console.log(`Already ${beforeState}`);
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3102
3706
|
tmuxSend(session, agent.rejectKey);
|
|
3103
3707
|
|
|
3104
3708
|
if (!wait) return;
|
|
@@ -3121,7 +3725,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3121
3725
|
* @param {string | null | undefined} customInstructions
|
|
3122
3726
|
* @param {{wait?: boolean, yolo?: boolean, fresh?: boolean, timeoutMs?: number}} [options]
|
|
3123
3727
|
*/
|
|
3124
|
-
async function cmdReview(
|
|
3728
|
+
async function cmdReview(
|
|
3729
|
+
agent,
|
|
3730
|
+
session,
|
|
3731
|
+
option,
|
|
3732
|
+
customInstructions,
|
|
3733
|
+
{ wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3734
|
+
) {
|
|
3125
3735
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3126
3736
|
|
|
3127
3737
|
// Reset conversation if --fresh and session exists
|
|
@@ -3147,7 +3757,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3147
3757
|
|
|
3148
3758
|
// AX_REVIEW_MODE=exec: bypass /review command, send instructions directly
|
|
3149
3759
|
if (process.env.AX_REVIEW_MODE === "exec" && option === "custom" && customInstructions) {
|
|
3150
|
-
return cmdAsk(agent, session, customInstructions, {
|
|
3760
|
+
return cmdAsk(agent, session, customInstructions, {
|
|
3761
|
+
noWait: !wait,
|
|
3762
|
+
yolo,
|
|
3763
|
+
timeoutMs,
|
|
3764
|
+
});
|
|
3151
3765
|
}
|
|
3152
3766
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3153
3767
|
|
|
@@ -3159,7 +3773,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3159
3773
|
}
|
|
3160
3774
|
|
|
3161
3775
|
/** @type {string} */
|
|
3162
|
-
const activeSession = sessionExists
|
|
3776
|
+
const activeSession = sessionExists
|
|
3777
|
+
? /** @type {string} */ (session)
|
|
3778
|
+
: await cmdStart(agent, session, { yolo });
|
|
3163
3779
|
|
|
3164
3780
|
tmuxSendLiteral(activeSession, "/review");
|
|
3165
3781
|
await sleep(50);
|
|
@@ -3181,12 +3797,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3181
3797
|
|
|
3182
3798
|
if (!wait) return;
|
|
3183
3799
|
|
|
3184
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3185
3800
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3186
3801
|
|
|
3187
3802
|
const { state, screen } = useAutoApprove
|
|
3188
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3189
|
-
: await
|
|
3803
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3804
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3190
3805
|
|
|
3191
3806
|
if (state === State.RATE_LIMITED) {
|
|
3192
3807
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3194,12 +3809,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3194
3809
|
}
|
|
3195
3810
|
|
|
3196
3811
|
if (state === State.CONFIRMING) {
|
|
3197
|
-
console.log(`CONFIRM: ${
|
|
3812
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3198
3813
|
process.exit(3);
|
|
3199
3814
|
}
|
|
3200
|
-
|
|
3201
|
-
const response = agent.getResponse(activeSession, screen);
|
|
3202
|
-
console.log(response || "");
|
|
3203
3815
|
}
|
|
3204
3816
|
|
|
3205
3817
|
/**
|
|
@@ -3230,7 +3842,7 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3230
3842
|
}
|
|
3231
3843
|
|
|
3232
3844
|
if (state === State.CONFIRMING) {
|
|
3233
|
-
console.log(`CONFIRM: ${
|
|
3845
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3234
3846
|
process.exit(3);
|
|
3235
3847
|
}
|
|
3236
3848
|
|
|
@@ -3242,6 +3854,8 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3242
3854
|
const output = agent.getResponse(session, screen, index);
|
|
3243
3855
|
if (output) {
|
|
3244
3856
|
console.log(output);
|
|
3857
|
+
} else {
|
|
3858
|
+
console.log("READY_NO_CONTENT");
|
|
3245
3859
|
}
|
|
3246
3860
|
}
|
|
3247
3861
|
|
|
@@ -3264,7 +3878,7 @@ function cmdStatus(agent, session) {
|
|
|
3264
3878
|
}
|
|
3265
3879
|
|
|
3266
3880
|
if (state === State.CONFIRMING) {
|
|
3267
|
-
console.log(`CONFIRM: ${
|
|
3881
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3268
3882
|
process.exit(3);
|
|
3269
3883
|
}
|
|
3270
3884
|
|
|
@@ -3272,6 +3886,10 @@ function cmdStatus(agent, session) {
|
|
|
3272
3886
|
console.log("THINKING");
|
|
3273
3887
|
process.exit(4);
|
|
3274
3888
|
}
|
|
3889
|
+
|
|
3890
|
+
// READY (or STARTING/UPDATE_PROMPT which are transient)
|
|
3891
|
+
console.log("READY");
|
|
3892
|
+
process.exit(0);
|
|
3275
3893
|
}
|
|
3276
3894
|
|
|
3277
3895
|
/**
|
|
@@ -3385,7 +4003,7 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3385
4003
|
}
|
|
3386
4004
|
|
|
3387
4005
|
if (state === State.CONFIRMING) {
|
|
3388
|
-
console.log(`CONFIRM: ${
|
|
4006
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3389
4007
|
process.exit(3);
|
|
3390
4008
|
}
|
|
3391
4009
|
|
|
@@ -3420,23 +4038,30 @@ function getAgentFromInvocation() {
|
|
|
3420
4038
|
*/
|
|
3421
4039
|
function printHelp(agent, cliName) {
|
|
3422
4040
|
const name = cliName;
|
|
3423
|
-
const backendName = agent.
|
|
4041
|
+
const backendName = agent.displayName;
|
|
3424
4042
|
const hasReview = !!agent.reviewOptions;
|
|
3425
4043
|
|
|
3426
4044
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
3427
4045
|
|
|
4046
|
+
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4047
|
+
|
|
3428
4048
|
Commands:
|
|
3429
4049
|
agents List all running agents with state and log paths
|
|
4050
|
+
target Show default target session for current tool
|
|
3430
4051
|
attach [SESSION] Attach to agent session interactively
|
|
3431
4052
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3432
4053
|
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
3433
4054
|
summon [name] Summon archangels (all, or by name)
|
|
3434
4055
|
recall [name] Recall archangels (all, or by name)
|
|
3435
|
-
kill Kill sessions
|
|
4056
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
3436
4057
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3437
4058
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3438
|
-
debug Show raw screen output and detected state${
|
|
3439
|
-
|
|
4059
|
+
debug Show raw screen output and detected state${
|
|
4060
|
+
hasReview
|
|
4061
|
+
? `
|
|
4062
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
4063
|
+
: ""
|
|
4064
|
+
}
|
|
3440
4065
|
select N Select menu option N
|
|
3441
4066
|
approve Approve pending action (send 'y')
|
|
3442
4067
|
reject Reject pending action (send 'n')
|
|
@@ -3448,37 +4073,41 @@ Commands:
|
|
|
3448
4073
|
Flags:
|
|
3449
4074
|
--tool=NAME Use specific agent (codex, claude)
|
|
3450
4075
|
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
3451
|
-
--wait Wait for response (for
|
|
3452
|
-
--no-wait
|
|
3453
|
-
--timeout=N Set timeout in seconds (default:
|
|
4076
|
+
--wait Wait for response (default for messages; required for approve/reject)
|
|
4077
|
+
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4078
|
+
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
3454
4079
|
--yolo Skip all confirmations (dangerous)
|
|
3455
4080
|
--fresh Reset conversation before review
|
|
4081
|
+
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4082
|
+
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
3456
4083
|
|
|
3457
4084
|
Environment:
|
|
3458
|
-
AX_DEFAULT_TOOL
|
|
3459
|
-
${agent.envVar}
|
|
3460
|
-
AX_CLAUDE_CONFIG_DIR
|
|
3461
|
-
AX_CODEX_CONFIG_DIR
|
|
3462
|
-
AX_REVIEW_MODE=exec
|
|
3463
|
-
AX_DEBUG=1
|
|
4085
|
+
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
4086
|
+
${agent.envVar} Override default session name
|
|
4087
|
+
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
4088
|
+
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
4089
|
+
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
4090
|
+
AX_DEBUG=1 Enable debug logging
|
|
3464
4091
|
|
|
3465
4092
|
Examples:
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
4093
|
+
${name} "explain this codebase"
|
|
4094
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4095
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4096
|
+
${name} review uncommitted --wait
|
|
4097
|
+
${name} approve --wait
|
|
4098
|
+
${name} kill # Kill agents in current project
|
|
4099
|
+
${name} kill --all # Kill all agents across all projects
|
|
4100
|
+
${name} kill --session=NAME # Kill specific session
|
|
4101
|
+
${name} send "1[Enter]" # Recovery: select option 1 and press Enter
|
|
4102
|
+
${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
|
|
4103
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
4104
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
4105
|
+
${name} recall # Recall all archangels
|
|
4106
|
+
${name} recall reviewer # Recall one by name
|
|
4107
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4108
|
+
|
|
4109
|
+
Note: Reviews and complex tasks may take several minutes.
|
|
4110
|
+
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
3482
4111
|
}
|
|
3483
4112
|
|
|
3484
4113
|
async function main() {
|
|
@@ -3493,38 +4122,32 @@ async function main() {
|
|
|
3493
4122
|
const args = process.argv.slice(2);
|
|
3494
4123
|
const cliName = path.basename(process.argv[1], ".js");
|
|
3495
4124
|
|
|
3496
|
-
|
|
4125
|
+
// Parse all flags and positionals in one place
|
|
4126
|
+
const { flags, positionals } = parseCliArgs(args);
|
|
4127
|
+
|
|
4128
|
+
if (flags.version) {
|
|
3497
4129
|
console.log(VERSION);
|
|
3498
4130
|
process.exit(0);
|
|
3499
4131
|
}
|
|
3500
4132
|
|
|
3501
|
-
//
|
|
3502
|
-
const wait =
|
|
3503
|
-
const noWait = args.includes("--no-wait");
|
|
3504
|
-
const yolo = args.includes("--yolo");
|
|
3505
|
-
const fresh = args.includes("--fresh");
|
|
3506
|
-
const reasoning = args.includes("--reasoning");
|
|
3507
|
-
const follow = args.includes("--follow") || args.includes("-f");
|
|
4133
|
+
// Extract flags into local variables for convenience
|
|
4134
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } = flags;
|
|
3508
4135
|
|
|
3509
4136
|
// Agent selection
|
|
3510
4137
|
let agent = getAgentFromInvocation();
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
if (tool === "claude") agent = ClaudeAgent;
|
|
3515
|
-
else if (tool === "codex") agent = CodexAgent;
|
|
4138
|
+
if (flags.tool) {
|
|
4139
|
+
if (flags.tool === "claude") agent = ClaudeAgent;
|
|
4140
|
+
else if (flags.tool === "codex") agent = CodexAgent;
|
|
3516
4141
|
else {
|
|
3517
|
-
console.log(`ERROR: unknown tool '${tool}'`);
|
|
4142
|
+
console.log(`ERROR: unknown tool '${flags.tool}'`);
|
|
3518
4143
|
process.exit(1);
|
|
3519
4144
|
}
|
|
3520
4145
|
}
|
|
3521
4146
|
|
|
3522
4147
|
// Session resolution
|
|
3523
4148
|
let session = agent.getDefaultSession();
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
const val = sessionArg.split("=")[1];
|
|
3527
|
-
if (val === "self") {
|
|
4149
|
+
if (flags.session) {
|
|
4150
|
+
if (flags.session === "self") {
|
|
3528
4151
|
const current = tmuxCurrentSession();
|
|
3529
4152
|
if (!current) {
|
|
3530
4153
|
console.log("ERROR: --session=self requires running inside tmux");
|
|
@@ -3533,99 +4156,93 @@ async function main() {
|
|
|
3533
4156
|
session = current;
|
|
3534
4157
|
} else {
|
|
3535
4158
|
// Resolve partial names, archangel names, and UUID prefixes
|
|
3536
|
-
session = resolveSessionName(
|
|
4159
|
+
session = resolveSessionName(flags.session);
|
|
3537
4160
|
}
|
|
3538
4161
|
}
|
|
3539
4162
|
|
|
3540
|
-
// Timeout
|
|
4163
|
+
// Timeout (convert seconds to milliseconds)
|
|
3541
4164
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
const val = parseInt(timeoutArg.split("=")[1], 10);
|
|
3545
|
-
if (isNaN(val) || val <= 0) {
|
|
4165
|
+
if (flags.timeout !== undefined) {
|
|
4166
|
+
if (isNaN(flags.timeout) || flags.timeout <= 0) {
|
|
3546
4167
|
console.log("ERROR: invalid timeout");
|
|
3547
4168
|
process.exit(1);
|
|
3548
4169
|
}
|
|
3549
|
-
timeoutMs =
|
|
4170
|
+
timeoutMs = flags.timeout * 1000;
|
|
3550
4171
|
}
|
|
3551
4172
|
|
|
3552
4173
|
// Tail (for log command)
|
|
3553
|
-
|
|
3554
|
-
const tailArg = args.find((a) => a.startsWith("--tail="));
|
|
3555
|
-
if (tailArg) {
|
|
3556
|
-
tail = parseInt(tailArg.split("=")[1], 10) || 50;
|
|
3557
|
-
}
|
|
4174
|
+
const tail = flags.tail ?? 50;
|
|
3558
4175
|
|
|
3559
4176
|
// Limit (for mailbox command)
|
|
3560
|
-
|
|
3561
|
-
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
3562
|
-
if (limitArg) {
|
|
3563
|
-
limit = parseInt(limitArg.split("=")[1], 10) || 20;
|
|
3564
|
-
}
|
|
4177
|
+
const limit = flags.limit ?? 20;
|
|
3565
4178
|
|
|
3566
4179
|
// Branch filter (for mailbox command)
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
|
-
// All flag (for mailbox command - show all regardless of age)
|
|
3574
|
-
const all = args.includes("--all");
|
|
3575
|
-
|
|
3576
|
-
// Filter out flags
|
|
3577
|
-
const filteredArgs = args.filter(
|
|
3578
|
-
(a) =>
|
|
3579
|
-
!["--wait", "--no-wait", "--yolo", "--reasoning", "--follow", "-f", "--all"].includes(a) &&
|
|
3580
|
-
!a.startsWith("--timeout") &&
|
|
3581
|
-
!a.startsWith("--session") &&
|
|
3582
|
-
!a.startsWith("--tool") &&
|
|
3583
|
-
!a.startsWith("--tail") &&
|
|
3584
|
-
!a.startsWith("--limit") &&
|
|
3585
|
-
!a.startsWith("--branch")
|
|
3586
|
-
);
|
|
3587
|
-
const cmd = filteredArgs[0];
|
|
4180
|
+
const branch = flags.branch ?? null;
|
|
4181
|
+
|
|
4182
|
+
// Command is first positional
|
|
4183
|
+
const cmd = positionals[0];
|
|
3588
4184
|
|
|
3589
4185
|
// Dispatch commands
|
|
3590
4186
|
if (cmd === "agents") return cmdAgents();
|
|
3591
|
-
if (cmd === "
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
4187
|
+
if (cmd === "target") {
|
|
4188
|
+
const defaultSession = agent.getDefaultSession();
|
|
4189
|
+
if (defaultSession) {
|
|
4190
|
+
console.log(defaultSession);
|
|
4191
|
+
} else {
|
|
4192
|
+
console.log("NO_TARGET");
|
|
4193
|
+
process.exit(1);
|
|
4194
|
+
}
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
if (cmd === "summon") return cmdSummon(positionals[1]);
|
|
4198
|
+
if (cmd === "recall") return cmdRecall(positionals[1]);
|
|
4199
|
+
if (cmd === "archangel") return cmdArchangel(positionals[1]);
|
|
4200
|
+
if (cmd === "kill") return cmdKill(session, { all, orphans, force });
|
|
4201
|
+
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4202
|
+
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
3597
4203
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3598
4204
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3599
4205
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3600
|
-
if (cmd === "review")
|
|
4206
|
+
if (cmd === "review")
|
|
4207
|
+
return cmdReview(agent, session, positionals[1], positionals[2], {
|
|
4208
|
+
wait,
|
|
4209
|
+
fresh,
|
|
4210
|
+
timeoutMs,
|
|
4211
|
+
});
|
|
3601
4212
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3602
4213
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3603
4214
|
if (cmd === "output") {
|
|
3604
|
-
const indexArg =
|
|
4215
|
+
const indexArg = positionals[1];
|
|
3605
4216
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3606
4217
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3607
4218
|
}
|
|
3608
|
-
if (cmd === "send" &&
|
|
4219
|
+
if (cmd === "send" && positionals.length > 1)
|
|
4220
|
+
return cmdSend(session, positionals.slice(1).join(" "));
|
|
3609
4221
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3610
4222
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3611
|
-
if (cmd === "select" &&
|
|
4223
|
+
if (cmd === "select" && positionals[1])
|
|
4224
|
+
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
3612
4225
|
|
|
3613
4226
|
// Default: send message
|
|
3614
|
-
let message =
|
|
4227
|
+
let message = positionals.join(" ");
|
|
3615
4228
|
if (!message && hasStdinData()) {
|
|
3616
4229
|
message = await readStdin();
|
|
3617
4230
|
}
|
|
3618
4231
|
|
|
3619
|
-
if (!message ||
|
|
4232
|
+
if (!message || flags.help) {
|
|
3620
4233
|
printHelp(agent, cliName);
|
|
3621
4234
|
process.exit(0);
|
|
3622
4235
|
}
|
|
3623
4236
|
|
|
3624
|
-
// Detect "please review" and route to custom review mode
|
|
3625
|
-
const reviewMatch = message.match(/^please review\s*(.*)/i);
|
|
4237
|
+
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4238
|
+
const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
|
|
3626
4239
|
if (reviewMatch && agent.reviewOptions) {
|
|
3627
4240
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3628
|
-
return cmdReview(agent, session, "custom", customInstructions, {
|
|
4241
|
+
return cmdReview(agent, session, "custom", customInstructions, {
|
|
4242
|
+
wait: !noWait,
|
|
4243
|
+
yolo,
|
|
4244
|
+
timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
|
|
4245
|
+
});
|
|
3629
4246
|
}
|
|
3630
4247
|
|
|
3631
4248
|
return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
|
|
@@ -3633,16 +4250,21 @@ async function main() {
|
|
|
3633
4250
|
|
|
3634
4251
|
// Run main() only when executed directly (not when imported for testing)
|
|
3635
4252
|
// Use realpathSync to handle symlinks (e.g., axclaude, axcodex bin entries)
|
|
3636
|
-
const isDirectRun =
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
4253
|
+
const isDirectRun =
|
|
4254
|
+
process.argv[1] &&
|
|
4255
|
+
(() => {
|
|
4256
|
+
try {
|
|
4257
|
+
return realpathSync(process.argv[1]) === __filename;
|
|
4258
|
+
} catch {
|
|
4259
|
+
return false;
|
|
4260
|
+
}
|
|
4261
|
+
})();
|
|
3643
4262
|
if (isDirectRun) {
|
|
3644
4263
|
main().catch((err) => {
|
|
3645
4264
|
console.log(`ERROR: ${err.message}`);
|
|
4265
|
+
if (err instanceof TimeoutError && err.session) {
|
|
4266
|
+
console.log(`Hint: Use 'ax debug --session=${err.session}' to see current screen state`);
|
|
4267
|
+
}
|
|
3646
4268
|
process.exit(1);
|
|
3647
4269
|
});
|
|
3648
4270
|
}
|
|
@@ -3652,6 +4274,7 @@ export {
|
|
|
3652
4274
|
parseSessionName,
|
|
3653
4275
|
parseAgentConfig,
|
|
3654
4276
|
parseKeySequence,
|
|
4277
|
+
parseCliArgs,
|
|
3655
4278
|
getClaudeProjectPath,
|
|
3656
4279
|
matchesPattern,
|
|
3657
4280
|
getBaseDir,
|
|
@@ -3663,4 +4286,3 @@ export {
|
|
|
3663
4286
|
detectState,
|
|
3664
4287
|
State,
|
|
3665
4288
|
};
|
|
3666
|
-
|