ax-agents 0.0.1-alpha.6 → 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/ax.js +556 -158
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -23,11 +23,15 @@ import {
|
|
|
23
23
|
renameSync,
|
|
24
24
|
realpathSync,
|
|
25
25
|
watch,
|
|
26
|
+
openSync,
|
|
27
|
+
readSync,
|
|
28
|
+
closeSync,
|
|
26
29
|
} from "node:fs";
|
|
27
30
|
import { randomUUID } from "node:crypto";
|
|
28
31
|
import { fileURLToPath } from "node:url";
|
|
29
32
|
import path from "node:path";
|
|
30
33
|
import os from "node:os";
|
|
34
|
+
import { parseArgs } from "node:util";
|
|
31
35
|
|
|
32
36
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
37
|
const __dirname = path.dirname(__filename);
|
|
@@ -302,6 +306,21 @@ const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
|
|
|
302
306
|
const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
303
307
|
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
304
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
|
+
|
|
305
324
|
/**
|
|
306
325
|
* @param {string} session
|
|
307
326
|
* @param {(screen: string) => boolean} predicate
|
|
@@ -315,7 +334,7 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
315
334
|
if (predicate(screen)) return screen;
|
|
316
335
|
await sleep(POLL_MS);
|
|
317
336
|
}
|
|
318
|
-
throw new
|
|
337
|
+
throw new TimeoutError(session);
|
|
319
338
|
}
|
|
320
339
|
|
|
321
340
|
// =============================================================================
|
|
@@ -343,6 +362,38 @@ function findCallerPid() {
|
|
|
343
362
|
return null;
|
|
344
363
|
}
|
|
345
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
|
+
|
|
346
397
|
// =============================================================================
|
|
347
398
|
// Helpers - stdin
|
|
348
399
|
// =============================================================================
|
|
@@ -371,6 +422,86 @@ async function readStdin() {
|
|
|
371
422
|
}
|
|
372
423
|
|
|
373
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
|
+
|
|
374
505
|
// Helpers - session tracking
|
|
375
506
|
// =============================================================================
|
|
376
507
|
|
|
@@ -585,6 +716,130 @@ function getAssistantText(logPath, index = 0) {
|
|
|
585
716
|
}
|
|
586
717
|
}
|
|
587
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
|
+
|
|
588
843
|
/**
|
|
589
844
|
* @returns {string[]}
|
|
590
845
|
*/
|
|
@@ -1457,7 +1712,7 @@ const State = {
|
|
|
1457
1712
|
* @param {string} config.promptSymbol - Symbol indicating ready state
|
|
1458
1713
|
* @param {string[]} [config.spinners] - Spinner characters indicating thinking
|
|
1459
1714
|
* @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
|
|
1460
|
-
* @param {string[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1715
|
+
* @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1461
1716
|
* @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
|
|
1462
1717
|
* @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
|
|
1463
1718
|
* @returns {string} The detected state
|
|
@@ -1475,14 +1730,33 @@ function detectState(screen, config) {
|
|
|
1475
1730
|
return State.RATE_LIMITED;
|
|
1476
1731
|
}
|
|
1477
1732
|
|
|
1478
|
-
//
|
|
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")
|
|
1479
1747
|
const spinners = config.spinners || [];
|
|
1480
|
-
if (spinners.some((s) =>
|
|
1748
|
+
if (spinners.some((s) => lastLines.includes(s))) {
|
|
1481
1749
|
return State.THINKING;
|
|
1482
1750
|
}
|
|
1483
|
-
// Thinking - text patterns (last lines)
|
|
1751
|
+
// Thinking - text patterns (last lines) - supports strings, regexes, and functions
|
|
1484
1752
|
const thinkingPatterns = config.thinkingPatterns || [];
|
|
1485
|
-
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
|
+
) {
|
|
1486
1760
|
return State.THINKING;
|
|
1487
1761
|
}
|
|
1488
1762
|
|
|
@@ -1494,19 +1768,6 @@ function detectState(screen, config) {
|
|
|
1494
1768
|
}
|
|
1495
1769
|
}
|
|
1496
1770
|
|
|
1497
|
-
// Confirming - check recent lines (not full screen to avoid history false positives)
|
|
1498
|
-
const confirmPatterns = config.confirmPatterns || [];
|
|
1499
|
-
for (const pattern of confirmPatterns) {
|
|
1500
|
-
if (typeof pattern === "function") {
|
|
1501
|
-
// Functions check lastLines first (most specific), then recentLines
|
|
1502
|
-
if (pattern(lastLines)) return State.CONFIRMING;
|
|
1503
|
-
if (pattern(recentLines)) return State.CONFIRMING;
|
|
1504
|
-
} else {
|
|
1505
|
-
// String patterns check recentLines (bounded range)
|
|
1506
|
-
if (recentLines.includes(pattern)) return State.CONFIRMING;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
1771
|
// Ready - only if prompt symbol is visible AND not followed by pasted content
|
|
1511
1772
|
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
1512
1773
|
if (lastLines.includes(config.promptSymbol)) {
|
|
@@ -1541,12 +1802,13 @@ function detectState(screen, config) {
|
|
|
1541
1802
|
/**
|
|
1542
1803
|
* @typedef {Object} AgentConfigInput
|
|
1543
1804
|
* @property {string} name
|
|
1805
|
+
* @property {string} displayName
|
|
1544
1806
|
* @property {string} startCommand
|
|
1545
1807
|
* @property {string} yoloCommand
|
|
1546
1808
|
* @property {string} promptSymbol
|
|
1547
1809
|
* @property {string[]} [spinners]
|
|
1548
1810
|
* @property {RegExp} [rateLimitPattern]
|
|
1549
|
-
* @property {string[]} [thinkingPatterns]
|
|
1811
|
+
* @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
|
|
1550
1812
|
* @property {ConfirmPattern[]} [confirmPatterns]
|
|
1551
1813
|
* @property {UpdatePromptPatterns | null} [updatePromptPatterns]
|
|
1552
1814
|
* @property {string[]} [responseMarkers]
|
|
@@ -1556,6 +1818,8 @@ function detectState(screen, config) {
|
|
|
1556
1818
|
* @property {string} [approveKey]
|
|
1557
1819
|
* @property {string} [rejectKey]
|
|
1558
1820
|
* @property {string} [safeAllowedTools]
|
|
1821
|
+
* @property {string | null} [sessionIdFlag]
|
|
1822
|
+
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
1559
1823
|
*/
|
|
1560
1824
|
|
|
1561
1825
|
class Agent {
|
|
@@ -1566,6 +1830,8 @@ class Agent {
|
|
|
1566
1830
|
/** @type {string} */
|
|
1567
1831
|
this.name = config.name;
|
|
1568
1832
|
/** @type {string} */
|
|
1833
|
+
this.displayName = config.displayName;
|
|
1834
|
+
/** @type {string} */
|
|
1569
1835
|
this.startCommand = config.startCommand;
|
|
1570
1836
|
/** @type {string} */
|
|
1571
1837
|
this.yoloCommand = config.yoloCommand;
|
|
@@ -1575,7 +1841,7 @@ class Agent {
|
|
|
1575
1841
|
this.spinners = config.spinners || [];
|
|
1576
1842
|
/** @type {RegExp | undefined} */
|
|
1577
1843
|
this.rateLimitPattern = config.rateLimitPattern;
|
|
1578
|
-
/** @type {string[]} */
|
|
1844
|
+
/** @type {(string | RegExp | ((lines: string) => boolean))[]} */
|
|
1579
1845
|
this.thinkingPatterns = config.thinkingPatterns || [];
|
|
1580
1846
|
/** @type {ConfirmPattern[]} */
|
|
1581
1847
|
this.confirmPatterns = config.confirmPatterns || [];
|
|
@@ -1595,6 +1861,10 @@ class Agent {
|
|
|
1595
1861
|
this.rejectKey = config.rejectKey || "n";
|
|
1596
1862
|
/** @type {string | undefined} */
|
|
1597
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;
|
|
1598
1868
|
}
|
|
1599
1869
|
|
|
1600
1870
|
/**
|
|
@@ -1612,11 +1882,11 @@ class Agent {
|
|
|
1612
1882
|
} else {
|
|
1613
1883
|
base = this.startCommand;
|
|
1614
1884
|
}
|
|
1615
|
-
//
|
|
1616
|
-
if (this.
|
|
1885
|
+
// Some agents support session ID flags for deterministic session tracking
|
|
1886
|
+
if (this.sessionIdFlag && sessionName) {
|
|
1617
1887
|
const parsed = parseSessionName(sessionName);
|
|
1618
1888
|
if (parsed?.uuid) {
|
|
1619
|
-
return `${base}
|
|
1889
|
+
return `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
|
|
1620
1890
|
}
|
|
1621
1891
|
}
|
|
1622
1892
|
return base;
|
|
@@ -1674,13 +1944,8 @@ class Agent {
|
|
|
1674
1944
|
* @returns {string | null}
|
|
1675
1945
|
*/
|
|
1676
1946
|
findLogPath(sessionName) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const uuid = parsed?.uuid;
|
|
1680
|
-
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
1681
|
-
}
|
|
1682
|
-
if (this.name === "codex") {
|
|
1683
|
-
return findCodexLogPath(sessionName);
|
|
1947
|
+
if (this.logPathFinder) {
|
|
1948
|
+
return this.logPathFinder(sessionName);
|
|
1684
1949
|
}
|
|
1685
1950
|
return null;
|
|
1686
1951
|
}
|
|
@@ -1908,6 +2173,7 @@ class Agent {
|
|
|
1908
2173
|
|
|
1909
2174
|
const CodexAgent = new Agent({
|
|
1910
2175
|
name: "codex",
|
|
2176
|
+
displayName: "Codex",
|
|
1911
2177
|
startCommand: "codex --sandbox read-only",
|
|
1912
2178
|
yoloCommand: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
1913
2179
|
promptSymbol: "›",
|
|
@@ -1927,6 +2193,7 @@ const CodexAgent = new Agent({
|
|
|
1927
2193
|
chromePatterns: ["context left", "for shortcuts"],
|
|
1928
2194
|
reviewOptions: { pr: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
1929
2195
|
envVar: "AX_SESSION",
|
|
2196
|
+
logPathFinder: findCodexLogPath,
|
|
1930
2197
|
});
|
|
1931
2198
|
|
|
1932
2199
|
// =============================================================================
|
|
@@ -1935,12 +2202,15 @@ const CodexAgent = new Agent({
|
|
|
1935
2202
|
|
|
1936
2203
|
const ClaudeAgent = new Agent({
|
|
1937
2204
|
name: "claude",
|
|
2205
|
+
displayName: "Claude",
|
|
1938
2206
|
startCommand: "claude",
|
|
1939
2207
|
yoloCommand: "claude --dangerously-skip-permissions",
|
|
1940
2208
|
promptSymbol: "❯",
|
|
1941
|
-
|
|
2209
|
+
// Claude Code spinners: ·✢✳✶✻✽ (from cli.js source)
|
|
2210
|
+
spinners: ["·", "✢", "✳", "✶", "✻", "✽"],
|
|
1942
2211
|
rateLimitPattern: /rate.?limit/i,
|
|
1943
|
-
|
|
2212
|
+
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2213
|
+
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
1944
2214
|
confirmPatterns: [
|
|
1945
2215
|
"Do you want to make this edit",
|
|
1946
2216
|
"Do you want to run this command",
|
|
@@ -1965,6 +2235,13 @@ const ClaudeAgent = new Agent({
|
|
|
1965
2235
|
envVar: "AX_SESSION",
|
|
1966
2236
|
approveKey: "1",
|
|
1967
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
|
+
},
|
|
1968
2245
|
});
|
|
1969
2246
|
|
|
1970
2247
|
// =============================================================================
|
|
@@ -2002,30 +2279,38 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2002
2279
|
return { state, screen };
|
|
2003
2280
|
}
|
|
2004
2281
|
}
|
|
2005
|
-
throw new
|
|
2282
|
+
throw new TimeoutError(session);
|
|
2006
2283
|
}
|
|
2007
2284
|
|
|
2008
2285
|
/**
|
|
2009
|
-
*
|
|
2010
|
-
* Waits for screen activity before considering the response complete.
|
|
2286
|
+
* Core polling loop for waiting on agent responses.
|
|
2011
2287
|
* @param {Agent} agent
|
|
2012
2288
|
* @param {string} session
|
|
2013
|
-
* @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]
|
|
2014
2291
|
* @returns {Promise<{state: string, screen: string}>}
|
|
2015
2292
|
*/
|
|
2016
|
-
async function
|
|
2293
|
+
async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
2294
|
+
const { onPoll, onStateChange, onReady } = hooks;
|
|
2017
2295
|
const start = Date.now();
|
|
2018
2296
|
const initialScreen = tmuxCapture(session);
|
|
2019
2297
|
|
|
2020
2298
|
let lastScreen = initialScreen;
|
|
2299
|
+
let lastState = null;
|
|
2021
2300
|
let stableAt = null;
|
|
2022
2301
|
let sawActivity = false;
|
|
2023
2302
|
|
|
2024
2303
|
while (Date.now() - start < timeoutMs) {
|
|
2025
|
-
await sleep(POLL_MS);
|
|
2026
2304
|
const screen = tmuxCapture(session);
|
|
2027
2305
|
const state = agent.getState(screen);
|
|
2028
2306
|
|
|
2307
|
+
if (onPoll) onPoll(screen, state);
|
|
2308
|
+
|
|
2309
|
+
if (state !== lastState) {
|
|
2310
|
+
if (onStateChange) onStateChange(state, lastState, screen);
|
|
2311
|
+
lastState = state;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2029
2314
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING) {
|
|
2030
2315
|
return { state, screen };
|
|
2031
2316
|
}
|
|
@@ -2040,6 +2325,7 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2040
2325
|
|
|
2041
2326
|
if (sawActivity && stableAt && Date.now() - stableAt >= STABLE_MS) {
|
|
2042
2327
|
if (state === State.READY) {
|
|
2328
|
+
if (onReady) onReady(screen);
|
|
2043
2329
|
return { state, screen };
|
|
2044
2330
|
}
|
|
2045
2331
|
}
|
|
@@ -2047,26 +2333,86 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2047
2333
|
if (state === State.THINKING) {
|
|
2048
2334
|
sawActivity = true;
|
|
2049
2335
|
}
|
|
2336
|
+
|
|
2337
|
+
await sleep(POLL_MS);
|
|
2050
2338
|
}
|
|
2051
|
-
throw new
|
|
2339
|
+
throw new TimeoutError(session);
|
|
2052
2340
|
}
|
|
2053
2341
|
|
|
2054
2342
|
/**
|
|
2055
|
-
*
|
|
2056
|
-
* Used by callers to implement yolo mode on sessions not started with native --yolo.
|
|
2343
|
+
* Wait for agent response without streaming output.
|
|
2057
2344
|
* @param {Agent} agent
|
|
2058
2345
|
* @param {string} session
|
|
2059
2346
|
* @param {number} [timeoutMs]
|
|
2060
2347
|
* @returns {Promise<{state: string, screen: string}>}
|
|
2061
2348
|
*/
|
|
2062
|
-
async function
|
|
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.
|
|
2355
|
+
* @param {Agent} agent
|
|
2356
|
+
* @param {string} session
|
|
2357
|
+
* @param {number} [timeoutMs]
|
|
2358
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2359
|
+
*/
|
|
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) {
|
|
2063
2409
|
const deadline = Date.now() + timeoutMs;
|
|
2064
2410
|
|
|
2065
2411
|
while (Date.now() < deadline) {
|
|
2066
2412
|
const remaining = deadline - Date.now();
|
|
2067
2413
|
if (remaining <= 0) break;
|
|
2068
2414
|
|
|
2069
|
-
const { state, screen } = await
|
|
2415
|
+
const { state, screen } = await waitFn(agent, session, remaining);
|
|
2070
2416
|
|
|
2071
2417
|
if (state === State.RATE_LIMITED || state === State.READY) {
|
|
2072
2418
|
return { state, screen };
|
|
@@ -2078,11 +2424,10 @@ async function autoApproveLoop(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2078
2424
|
continue;
|
|
2079
2425
|
}
|
|
2080
2426
|
|
|
2081
|
-
// Unexpected state - log and continue polling
|
|
2082
2427
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2083
2428
|
}
|
|
2084
2429
|
|
|
2085
|
-
throw new
|
|
2430
|
+
throw new TimeoutError(session);
|
|
2086
2431
|
}
|
|
2087
2432
|
|
|
2088
2433
|
/**
|
|
@@ -2141,9 +2486,22 @@ function cmdAgents() {
|
|
|
2141
2486
|
|
|
2142
2487
|
if (agentSessions.length === 0) {
|
|
2143
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
|
+
}
|
|
2144
2498
|
return;
|
|
2145
2499
|
}
|
|
2146
2500
|
|
|
2501
|
+
// Get default session for each agent type
|
|
2502
|
+
const claudeDefault = ClaudeAgent.getDefaultSession();
|
|
2503
|
+
const codexDefault = CodexAgent.getDefaultSession();
|
|
2504
|
+
|
|
2147
2505
|
// Get info for each agent
|
|
2148
2506
|
const agents = agentSessions.map((session) => {
|
|
2149
2507
|
const parsed = /** @type {ParsedSession} */ (parseSessionName(session));
|
|
@@ -2152,30 +2510,45 @@ function cmdAgents() {
|
|
|
2152
2510
|
const state = agent.getState(screen);
|
|
2153
2511
|
const logPath = agent.findLogPath(session);
|
|
2154
2512
|
const type = parsed.archangelName ? "archangel" : "-";
|
|
2513
|
+
const isDefault =
|
|
2514
|
+
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
2515
|
+
(parsed.tool === "codex" && session === codexDefault);
|
|
2155
2516
|
|
|
2156
2517
|
return {
|
|
2157
2518
|
session,
|
|
2158
2519
|
tool: parsed.tool,
|
|
2159
2520
|
state: state || "unknown",
|
|
2521
|
+
target: isDefault ? "*" : "",
|
|
2160
2522
|
type,
|
|
2161
2523
|
log: logPath || "-",
|
|
2162
2524
|
};
|
|
2163
2525
|
});
|
|
2164
2526
|
|
|
2165
|
-
// Print table
|
|
2527
|
+
// Print sessions table
|
|
2166
2528
|
const maxSession = Math.max(7, ...agents.map((a) => a.session.length));
|
|
2167
2529
|
const maxTool = Math.max(4, ...agents.map((a) => a.tool.length));
|
|
2168
2530
|
const maxState = Math.max(5, ...agents.map((a) => a.state.length));
|
|
2531
|
+
const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
|
|
2169
2532
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2170
2533
|
|
|
2171
2534
|
console.log(
|
|
2172
|
-
`${"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`,
|
|
2173
2536
|
);
|
|
2174
2537
|
for (const a of agents) {
|
|
2175
2538
|
console.log(
|
|
2176
|
-
`${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}`,
|
|
2177
2540
|
);
|
|
2178
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
|
+
}
|
|
2179
2552
|
}
|
|
2180
2553
|
|
|
2181
2554
|
// =============================================================================
|
|
@@ -2838,9 +3211,31 @@ function ensureClaudeHookConfig() {
|
|
|
2838
3211
|
|
|
2839
3212
|
/**
|
|
2840
3213
|
* @param {string | null | undefined} session
|
|
2841
|
-
* @param {{all?: boolean}} [options]
|
|
3214
|
+
* @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
|
|
2842
3215
|
*/
|
|
2843
|
-
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
|
+
|
|
2844
3239
|
// If specific session provided, kill just that one
|
|
2845
3240
|
if (session) {
|
|
2846
3241
|
if (!tmuxHasSession(session)) {
|
|
@@ -3203,7 +3598,7 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3203
3598
|
* @param {string} message
|
|
3204
3599
|
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
3205
3600
|
*/
|
|
3206
|
-
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 } = {}) {
|
|
3207
3602
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3208
3603
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3209
3604
|
|
|
@@ -3223,14 +3618,23 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3223
3618
|
await sleep(50);
|
|
3224
3619
|
tmuxSend(activeSession, "Enter");
|
|
3225
3620
|
|
|
3226
|
-
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
|
+
}
|
|
3227
3632
|
|
|
3228
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3229
3633
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3230
3634
|
|
|
3231
3635
|
const { state, screen } = useAutoApprove
|
|
3232
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3233
|
-
: await
|
|
3636
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3637
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3234
3638
|
|
|
3235
3639
|
if (state === State.RATE_LIMITED) {
|
|
3236
3640
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3238,14 +3642,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3238
3642
|
}
|
|
3239
3643
|
|
|
3240
3644
|
if (state === State.CONFIRMING) {
|
|
3241
|
-
console.log(`CONFIRM: ${
|
|
3645
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3242
3646
|
process.exit(3);
|
|
3243
3647
|
}
|
|
3244
|
-
|
|
3245
|
-
const output = agent.getResponse(activeSession, screen);
|
|
3246
|
-
if (output) {
|
|
3247
|
-
console.log(output);
|
|
3248
|
-
}
|
|
3249
3648
|
}
|
|
3250
3649
|
|
|
3251
3650
|
/**
|
|
@@ -3260,9 +3659,10 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3260
3659
|
}
|
|
3261
3660
|
|
|
3262
3661
|
const before = tmuxCapture(session);
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3662
|
+
const beforeState = agent.getState(before);
|
|
3663
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3664
|
+
console.log(`Already ${beforeState}`);
|
|
3665
|
+
return;
|
|
3266
3666
|
}
|
|
3267
3667
|
|
|
3268
3668
|
tmuxSend(session, agent.approveKey);
|
|
@@ -3277,7 +3677,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3277
3677
|
}
|
|
3278
3678
|
|
|
3279
3679
|
if (state === State.CONFIRMING) {
|
|
3280
|
-
console.log(`CONFIRM: ${
|
|
3680
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3281
3681
|
process.exit(3);
|
|
3282
3682
|
}
|
|
3283
3683
|
|
|
@@ -3296,6 +3696,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3296
3696
|
process.exit(1);
|
|
3297
3697
|
}
|
|
3298
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
|
+
|
|
3299
3706
|
tmuxSend(session, agent.rejectKey);
|
|
3300
3707
|
|
|
3301
3708
|
if (!wait) return;
|
|
@@ -3323,7 +3730,7 @@ async function cmdReview(
|
|
|
3323
3730
|
session,
|
|
3324
3731
|
option,
|
|
3325
3732
|
customInstructions,
|
|
3326
|
-
{ wait = true, yolo =
|
|
3733
|
+
{ wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3327
3734
|
) {
|
|
3328
3735
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3329
3736
|
|
|
@@ -3390,12 +3797,11 @@ async function cmdReview(
|
|
|
3390
3797
|
|
|
3391
3798
|
if (!wait) return;
|
|
3392
3799
|
|
|
3393
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3394
3800
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3395
3801
|
|
|
3396
3802
|
const { state, screen } = useAutoApprove
|
|
3397
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3398
|
-
: await
|
|
3803
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3804
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3399
3805
|
|
|
3400
3806
|
if (state === State.RATE_LIMITED) {
|
|
3401
3807
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3403,12 +3809,9 @@ async function cmdReview(
|
|
|
3403
3809
|
}
|
|
3404
3810
|
|
|
3405
3811
|
if (state === State.CONFIRMING) {
|
|
3406
|
-
console.log(`CONFIRM: ${
|
|
3812
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3407
3813
|
process.exit(3);
|
|
3408
3814
|
}
|
|
3409
|
-
|
|
3410
|
-
const response = agent.getResponse(activeSession, screen);
|
|
3411
|
-
console.log(response || "");
|
|
3412
3815
|
}
|
|
3413
3816
|
|
|
3414
3817
|
/**
|
|
@@ -3439,7 +3842,7 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3439
3842
|
}
|
|
3440
3843
|
|
|
3441
3844
|
if (state === State.CONFIRMING) {
|
|
3442
|
-
console.log(`CONFIRM: ${
|
|
3845
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3443
3846
|
process.exit(3);
|
|
3444
3847
|
}
|
|
3445
3848
|
|
|
@@ -3451,6 +3854,8 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3451
3854
|
const output = agent.getResponse(session, screen, index);
|
|
3452
3855
|
if (output) {
|
|
3453
3856
|
console.log(output);
|
|
3857
|
+
} else {
|
|
3858
|
+
console.log("READY_NO_CONTENT");
|
|
3454
3859
|
}
|
|
3455
3860
|
}
|
|
3456
3861
|
|
|
@@ -3473,7 +3878,7 @@ function cmdStatus(agent, session) {
|
|
|
3473
3878
|
}
|
|
3474
3879
|
|
|
3475
3880
|
if (state === State.CONFIRMING) {
|
|
3476
|
-
console.log(`CONFIRM: ${
|
|
3881
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3477
3882
|
process.exit(3);
|
|
3478
3883
|
}
|
|
3479
3884
|
|
|
@@ -3481,6 +3886,10 @@ function cmdStatus(agent, session) {
|
|
|
3481
3886
|
console.log("THINKING");
|
|
3482
3887
|
process.exit(4);
|
|
3483
3888
|
}
|
|
3889
|
+
|
|
3890
|
+
// READY (or STARTING/UPDATE_PROMPT which are transient)
|
|
3891
|
+
console.log("READY");
|
|
3892
|
+
process.exit(0);
|
|
3484
3893
|
}
|
|
3485
3894
|
|
|
3486
3895
|
/**
|
|
@@ -3594,7 +4003,7 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3594
4003
|
}
|
|
3595
4004
|
|
|
3596
4005
|
if (state === State.CONFIRMING) {
|
|
3597
|
-
console.log(`CONFIRM: ${
|
|
4006
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3598
4007
|
process.exit(3);
|
|
3599
4008
|
}
|
|
3600
4009
|
|
|
@@ -3629,19 +4038,22 @@ function getAgentFromInvocation() {
|
|
|
3629
4038
|
*/
|
|
3630
4039
|
function printHelp(agent, cliName) {
|
|
3631
4040
|
const name = cliName;
|
|
3632
|
-
const backendName = agent.
|
|
4041
|
+
const backendName = agent.displayName;
|
|
3633
4042
|
const hasReview = !!agent.reviewOptions;
|
|
3634
4043
|
|
|
3635
4044
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
3636
4045
|
|
|
4046
|
+
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4047
|
+
|
|
3637
4048
|
Commands:
|
|
3638
4049
|
agents List all running agents with state and log paths
|
|
4050
|
+
target Show default target session for current tool
|
|
3639
4051
|
attach [SESSION] Attach to agent session interactively
|
|
3640
4052
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3641
4053
|
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
3642
4054
|
summon [name] Summon archangels (all, or by name)
|
|
3643
4055
|
recall [name] Recall archangels (all, or by name)
|
|
3644
|
-
kill Kill sessions
|
|
4056
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
3645
4057
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3646
4058
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3647
4059
|
debug Show raw screen output and detected state${
|
|
@@ -3661,11 +4073,13 @@ Commands:
|
|
|
3661
4073
|
Flags:
|
|
3662
4074
|
--tool=NAME Use specific agent (codex, claude)
|
|
3663
4075
|
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
3664
|
-
--wait Wait for response (for
|
|
3665
|
-
--no-wait
|
|
3666
|
-
--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})
|
|
3667
4079
|
--yolo Skip all confirmations (dangerous)
|
|
3668
4080
|
--fresh Reset conversation before review
|
|
4081
|
+
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4082
|
+
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
3669
4083
|
|
|
3670
4084
|
Environment:
|
|
3671
4085
|
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
@@ -3677,7 +4091,8 @@ Environment:
|
|
|
3677
4091
|
|
|
3678
4092
|
Examples:
|
|
3679
4093
|
${name} "explain this codebase"
|
|
3680
|
-
${name} "
|
|
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)
|
|
3681
4096
|
${name} review uncommitted --wait
|
|
3682
4097
|
${name} approve --wait
|
|
3683
4098
|
${name} kill # Kill agents in current project
|
|
@@ -3689,7 +4104,10 @@ Examples:
|
|
|
3689
4104
|
${name} summon reviewer # Summon by name (creates config if new)
|
|
3690
4105
|
${name} recall # Recall all archangels
|
|
3691
4106
|
${name} recall reviewer # Recall one by name
|
|
3692
|
-
${name} agents # List all agents (shows TYPE=archangel)
|
|
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).`);
|
|
3693
4111
|
}
|
|
3694
4112
|
|
|
3695
4113
|
async function main() {
|
|
@@ -3704,38 +4122,32 @@ async function main() {
|
|
|
3704
4122
|
const args = process.argv.slice(2);
|
|
3705
4123
|
const cliName = path.basename(process.argv[1], ".js");
|
|
3706
4124
|
|
|
3707
|
-
|
|
4125
|
+
// Parse all flags and positionals in one place
|
|
4126
|
+
const { flags, positionals } = parseCliArgs(args);
|
|
4127
|
+
|
|
4128
|
+
if (flags.version) {
|
|
3708
4129
|
console.log(VERSION);
|
|
3709
4130
|
process.exit(0);
|
|
3710
4131
|
}
|
|
3711
4132
|
|
|
3712
|
-
//
|
|
3713
|
-
const wait =
|
|
3714
|
-
const noWait = args.includes("--no-wait");
|
|
3715
|
-
const yolo = args.includes("--yolo");
|
|
3716
|
-
const fresh = args.includes("--fresh");
|
|
3717
|
-
const reasoning = args.includes("--reasoning");
|
|
3718
|
-
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;
|
|
3719
4135
|
|
|
3720
4136
|
// Agent selection
|
|
3721
4137
|
let agent = getAgentFromInvocation();
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
if (tool === "claude") agent = ClaudeAgent;
|
|
3726
|
-
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;
|
|
3727
4141
|
else {
|
|
3728
|
-
console.log(`ERROR: unknown tool '${tool}'`);
|
|
4142
|
+
console.log(`ERROR: unknown tool '${flags.tool}'`);
|
|
3729
4143
|
process.exit(1);
|
|
3730
4144
|
}
|
|
3731
4145
|
}
|
|
3732
4146
|
|
|
3733
4147
|
// Session resolution
|
|
3734
4148
|
let session = agent.getDefaultSession();
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
const val = sessionArg.split("=")[1];
|
|
3738
|
-
if (val === "self") {
|
|
4149
|
+
if (flags.session) {
|
|
4150
|
+
if (flags.session === "self") {
|
|
3739
4151
|
const current = tmuxCurrentSession();
|
|
3740
4152
|
if (!current) {
|
|
3741
4153
|
console.log("ERROR: --session=self requires running inside tmux");
|
|
@@ -3744,110 +4156,92 @@ async function main() {
|
|
|
3744
4156
|
session = current;
|
|
3745
4157
|
} else {
|
|
3746
4158
|
// Resolve partial names, archangel names, and UUID prefixes
|
|
3747
|
-
session = resolveSessionName(
|
|
4159
|
+
session = resolveSessionName(flags.session);
|
|
3748
4160
|
}
|
|
3749
4161
|
}
|
|
3750
4162
|
|
|
3751
|
-
// Timeout
|
|
4163
|
+
// Timeout (convert seconds to milliseconds)
|
|
3752
4164
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
const val = parseInt(timeoutArg.split("=")[1], 10);
|
|
3756
|
-
if (isNaN(val) || val <= 0) {
|
|
4165
|
+
if (flags.timeout !== undefined) {
|
|
4166
|
+
if (isNaN(flags.timeout) || flags.timeout <= 0) {
|
|
3757
4167
|
console.log("ERROR: invalid timeout");
|
|
3758
4168
|
process.exit(1);
|
|
3759
4169
|
}
|
|
3760
|
-
timeoutMs =
|
|
4170
|
+
timeoutMs = flags.timeout * 1000;
|
|
3761
4171
|
}
|
|
3762
4172
|
|
|
3763
4173
|
// Tail (for log command)
|
|
3764
|
-
|
|
3765
|
-
const tailArg = args.find((a) => a.startsWith("--tail="));
|
|
3766
|
-
if (tailArg) {
|
|
3767
|
-
tail = parseInt(tailArg.split("=")[1], 10) || 50;
|
|
3768
|
-
}
|
|
4174
|
+
const tail = flags.tail ?? 50;
|
|
3769
4175
|
|
|
3770
4176
|
// Limit (for mailbox command)
|
|
3771
|
-
|
|
3772
|
-
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
3773
|
-
if (limitArg) {
|
|
3774
|
-
limit = parseInt(limitArg.split("=")[1], 10) || 20;
|
|
3775
|
-
}
|
|
4177
|
+
const limit = flags.limit ?? 20;
|
|
3776
4178
|
|
|
3777
4179
|
// Branch filter (for mailbox command)
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
}
|
|
3783
|
-
|
|
3784
|
-
// All flag (for mailbox command - show all regardless of age)
|
|
3785
|
-
const all = args.includes("--all");
|
|
3786
|
-
|
|
3787
|
-
// Filter out flags
|
|
3788
|
-
const filteredArgs = args.filter(
|
|
3789
|
-
(a) =>
|
|
3790
|
-
!["--wait", "--no-wait", "--yolo", "--reasoning", "--follow", "-f", "--all"].includes(a) &&
|
|
3791
|
-
!a.startsWith("--timeout") &&
|
|
3792
|
-
!a.startsWith("--session") &&
|
|
3793
|
-
!a.startsWith("--tool") &&
|
|
3794
|
-
!a.startsWith("--tail") &&
|
|
3795
|
-
!a.startsWith("--limit") &&
|
|
3796
|
-
!a.startsWith("--branch"),
|
|
3797
|
-
);
|
|
3798
|
-
const cmd = filteredArgs[0];
|
|
4180
|
+
const branch = flags.branch ?? null;
|
|
4181
|
+
|
|
4182
|
+
// Command is first positional
|
|
4183
|
+
const cmd = positionals[0];
|
|
3799
4184
|
|
|
3800
4185
|
// Dispatch commands
|
|
3801
4186
|
if (cmd === "agents") return cmdAgents();
|
|
3802
|
-
if (cmd === "
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
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 });
|
|
3808
4203
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3809
4204
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3810
4205
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3811
4206
|
if (cmd === "review")
|
|
3812
|
-
return cmdReview(agent, session,
|
|
4207
|
+
return cmdReview(agent, session, positionals[1], positionals[2], {
|
|
3813
4208
|
wait,
|
|
3814
|
-
yolo,
|
|
3815
4209
|
fresh,
|
|
3816
4210
|
timeoutMs,
|
|
3817
4211
|
});
|
|
3818
4212
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3819
4213
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3820
4214
|
if (cmd === "output") {
|
|
3821
|
-
const indexArg =
|
|
4215
|
+
const indexArg = positionals[1];
|
|
3822
4216
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3823
4217
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3824
4218
|
}
|
|
3825
|
-
if (cmd === "send" &&
|
|
3826
|
-
return cmdSend(session,
|
|
4219
|
+
if (cmd === "send" && positionals.length > 1)
|
|
4220
|
+
return cmdSend(session, positionals.slice(1).join(" "));
|
|
3827
4221
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3828
4222
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3829
|
-
if (cmd === "select" &&
|
|
3830
|
-
return cmdSelect(agent, session,
|
|
4223
|
+
if (cmd === "select" && positionals[1])
|
|
4224
|
+
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
3831
4225
|
|
|
3832
4226
|
// Default: send message
|
|
3833
|
-
let message =
|
|
4227
|
+
let message = positionals.join(" ");
|
|
3834
4228
|
if (!message && hasStdinData()) {
|
|
3835
4229
|
message = await readStdin();
|
|
3836
4230
|
}
|
|
3837
4231
|
|
|
3838
|
-
if (!message ||
|
|
4232
|
+
if (!message || flags.help) {
|
|
3839
4233
|
printHelp(agent, cliName);
|
|
3840
4234
|
process.exit(0);
|
|
3841
4235
|
}
|
|
3842
4236
|
|
|
3843
|
-
// Detect "please review" and route to custom review mode
|
|
3844
|
-
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);
|
|
3845
4239
|
if (reviewMatch && agent.reviewOptions) {
|
|
3846
4240
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3847
4241
|
return cmdReview(agent, session, "custom", customInstructions, {
|
|
3848
4242
|
wait: !noWait,
|
|
3849
4243
|
yolo,
|
|
3850
|
-
timeoutMs,
|
|
4244
|
+
timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
|
|
3851
4245
|
});
|
|
3852
4246
|
}
|
|
3853
4247
|
|
|
@@ -3868,6 +4262,9 @@ const isDirectRun =
|
|
|
3868
4262
|
if (isDirectRun) {
|
|
3869
4263
|
main().catch((err) => {
|
|
3870
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
|
+
}
|
|
3871
4268
|
process.exit(1);
|
|
3872
4269
|
});
|
|
3873
4270
|
}
|
|
@@ -3877,6 +4274,7 @@ export {
|
|
|
3877
4274
|
parseSessionName,
|
|
3878
4275
|
parseAgentConfig,
|
|
3879
4276
|
parseKeySequence,
|
|
4277
|
+
parseCliArgs,
|
|
3880
4278
|
getClaudeProjectPath,
|
|
3881
4279
|
matchesPattern,
|
|
3882
4280
|
getBaseDir,
|