ax-agents 0.0.1-alpha.5 → 0.0.1-alpha.6
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 +448 -224
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# ax-agents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/luca-giordano-the-fall-of-the-rebel-angels.jpg" alt="The Fall of the Rebel Angels by Luca Giordano" width="250">
|
|
5
|
+
<br><br>
|
|
6
|
+
<strong>A CLI for orchestrating AI coding agents via `tmux`.</strong>
|
|
7
|
+
</p>
|
|
4
8
|
|
|
5
|
-
Running agents in tmux sessions makes it easy to monitor multiple agents, review their work, and interact with them when needed.
|
|
9
|
+
Running agents in `tmux` sessions makes it easy to monitor multiple agents, review their work, and interact with them when needed.
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
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,13 +9,21 @@
|
|
|
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
|
+
} from "node:fs";
|
|
18
27
|
import { randomUUID } from "node:crypto";
|
|
19
28
|
import { fileURLToPath } from "node:url";
|
|
20
29
|
import path from "node:path";
|
|
@@ -110,8 +119,9 @@ const VERSION = packageJson.version;
|
|
|
110
119
|
*/
|
|
111
120
|
|
|
112
121
|
/**
|
|
122
|
+
* @typedef {{matcher: string, hooks: Array<{type: string, command: string, timeout?: number}>}} ClaudeHookEntry
|
|
113
123
|
* @typedef {Object} ClaudeSettings
|
|
114
|
-
* @property {{UserPromptSubmit?:
|
|
124
|
+
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PostToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
115
125
|
*/
|
|
116
126
|
|
|
117
127
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
@@ -221,7 +231,9 @@ function tmuxKill(session) {
|
|
|
221
231
|
*/
|
|
222
232
|
function tmuxNewSession(session, command) {
|
|
223
233
|
// Use spawnSync to avoid command injection via session/command
|
|
224
|
-
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
234
|
+
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
235
|
+
encoding: "utf-8",
|
|
236
|
+
});
|
|
225
237
|
if (result.status !== 0) throw new Error(result.stderr || "tmux new-session failed");
|
|
226
238
|
}
|
|
227
239
|
|
|
@@ -230,7 +242,9 @@ function tmuxNewSession(session, command) {
|
|
|
230
242
|
*/
|
|
231
243
|
function tmuxCurrentSession() {
|
|
232
244
|
if (!process.env.TMUX) return null;
|
|
233
|
-
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
245
|
+
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
246
|
+
encoding: "utf-8",
|
|
247
|
+
});
|
|
234
248
|
if (result.status !== 0) return null;
|
|
235
249
|
return result.stdout.trim();
|
|
236
250
|
}
|
|
@@ -242,9 +256,13 @@ function tmuxCurrentSession() {
|
|
|
242
256
|
*/
|
|
243
257
|
function isYoloSession(session) {
|
|
244
258
|
try {
|
|
245
|
-
const result = spawnSync(
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
const result = spawnSync(
|
|
260
|
+
"tmux",
|
|
261
|
+
["display-message", "-t", session, "-p", "#{pane_start_command}"],
|
|
262
|
+
{
|
|
263
|
+
encoding: "utf-8",
|
|
264
|
+
},
|
|
265
|
+
);
|
|
248
266
|
if (result.status !== 0) return false;
|
|
249
267
|
const cmd = result.stdout.trim();
|
|
250
268
|
return cmd.includes("--dangerously-");
|
|
@@ -264,8 +282,14 @@ const POLL_MS = parseInt(process.env.AX_POLL_MS || "200", 10);
|
|
|
264
282
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.AX_TIMEOUT_MS || "120000", 10);
|
|
265
283
|
const REVIEW_TIMEOUT_MS = parseInt(process.env.AX_REVIEW_TIMEOUT_MS || "900000", 10); // 15 minutes
|
|
266
284
|
const STARTUP_TIMEOUT_MS = parseInt(process.env.AX_STARTUP_TIMEOUT_MS || "30000", 10);
|
|
267
|
-
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
|
|
268
|
-
|
|
285
|
+
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
|
|
286
|
+
process.env.AX_ARCHANGEL_STARTUP_TIMEOUT_MS || "60000",
|
|
287
|
+
10,
|
|
288
|
+
);
|
|
289
|
+
const ARCHANGEL_RESPONSE_TIMEOUT_MS = parseInt(
|
|
290
|
+
process.env.AX_ARCHANGEL_RESPONSE_TIMEOUT_MS || "300000",
|
|
291
|
+
10,
|
|
292
|
+
); // 5 minutes
|
|
269
293
|
const ARCHANGEL_HEALTH_CHECK_MS = parseInt(process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000", 10);
|
|
270
294
|
const STABLE_MS = parseInt(process.env.AX_STABLE_MS || "1000", 10);
|
|
271
295
|
const APPROVE_DELAY_MS = parseInt(process.env.AX_APPROVE_DELAY_MS || "100", 10);
|
|
@@ -304,7 +328,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
304
328
|
function findCallerPid() {
|
|
305
329
|
let pid = process.ppid;
|
|
306
330
|
while (pid > 1) {
|
|
307
|
-
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
331
|
+
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
332
|
+
encoding: "utf-8",
|
|
333
|
+
});
|
|
308
334
|
if (result.status !== 0) break;
|
|
309
335
|
const parts = result.stdout.trim().split(/\s+/);
|
|
310
336
|
const ppid = parseInt(parts[0], 10);
|
|
@@ -360,13 +386,17 @@ function parseSessionName(session) {
|
|
|
360
386
|
const rest = match[2];
|
|
361
387
|
|
|
362
388
|
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
363
|
-
const archangelMatch = rest.match(
|
|
389
|
+
const archangelMatch = rest.match(
|
|
390
|
+
/^archangel-(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
391
|
+
);
|
|
364
392
|
if (archangelMatch) {
|
|
365
393
|
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
366
394
|
}
|
|
367
395
|
|
|
368
396
|
// Partner: {tool}-partner-{uuid}
|
|
369
|
-
const partnerMatch = rest.match(
|
|
397
|
+
const partnerMatch = rest.match(
|
|
398
|
+
/^partner-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
399
|
+
);
|
|
370
400
|
if (partnerMatch) {
|
|
371
401
|
return { tool, uuid: partnerMatch[1] };
|
|
372
402
|
}
|
|
@@ -399,9 +429,13 @@ function getClaudeProjectPath(cwd) {
|
|
|
399
429
|
*/
|
|
400
430
|
function getTmuxSessionCwd(sessionName) {
|
|
401
431
|
try {
|
|
402
|
-
const result = spawnSync(
|
|
403
|
-
|
|
404
|
-
|
|
432
|
+
const result = spawnSync(
|
|
433
|
+
"tmux",
|
|
434
|
+
["display-message", "-t", sessionName, "-p", "#{pane_current_path}"],
|
|
435
|
+
{
|
|
436
|
+
encoding: "utf-8",
|
|
437
|
+
},
|
|
438
|
+
);
|
|
405
439
|
if (result.status === 0) return result.stdout.trim();
|
|
406
440
|
} catch (err) {
|
|
407
441
|
debugError("getTmuxSessionCwd", err);
|
|
@@ -425,7 +459,9 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
425
459
|
if (existsSync(indexPath)) {
|
|
426
460
|
try {
|
|
427
461
|
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
428
|
-
const entry = index.entries?.find(
|
|
462
|
+
const entry = index.entries?.find(
|
|
463
|
+
/** @param {{sessionId: string, fullPath?: string}} e */ (e) => e.sessionId === sessionId,
|
|
464
|
+
);
|
|
429
465
|
if (entry?.fullPath) return entry.fullPath;
|
|
430
466
|
} catch (err) {
|
|
431
467
|
debugError("findClaudeLogPath", err);
|
|
@@ -447,9 +483,13 @@ function findCodexLogPath(sessionName) {
|
|
|
447
483
|
// For Codex, we need to match by timing since we can't control the session ID
|
|
448
484
|
// Get tmux session creation time
|
|
449
485
|
try {
|
|
450
|
-
const result = spawnSync(
|
|
451
|
-
|
|
452
|
-
|
|
486
|
+
const result = spawnSync(
|
|
487
|
+
"tmux",
|
|
488
|
+
["display-message", "-t", sessionName, "-p", "#{session_created}"],
|
|
489
|
+
{
|
|
490
|
+
encoding: "utf-8",
|
|
491
|
+
},
|
|
492
|
+
);
|
|
453
493
|
if (result.status !== 0) return null;
|
|
454
494
|
const createdTs = parseInt(result.stdout.trim(), 10) * 1000; // tmux gives seconds, we need ms
|
|
455
495
|
if (isNaN(createdTs)) return null;
|
|
@@ -483,7 +523,11 @@ function findCodexLogPath(sessionName) {
|
|
|
483
523
|
// Log file should be created shortly after session start
|
|
484
524
|
// Allow small negative diff (-2s) for clock skew, up to 60s for slow starts
|
|
485
525
|
if (diff >= -2000 && diff < 60000) {
|
|
486
|
-
candidates.push({
|
|
526
|
+
candidates.push({
|
|
527
|
+
file,
|
|
528
|
+
diff: Math.abs(diff),
|
|
529
|
+
path: path.join(dayDir, file),
|
|
530
|
+
});
|
|
487
531
|
}
|
|
488
532
|
}
|
|
489
533
|
|
|
@@ -496,7 +540,6 @@ function findCodexLogPath(sessionName) {
|
|
|
496
540
|
}
|
|
497
541
|
}
|
|
498
542
|
|
|
499
|
-
|
|
500
543
|
/**
|
|
501
544
|
* Extract assistant text responses from a JSONL log file.
|
|
502
545
|
* This provides clean text without screen-scraped artifacts.
|
|
@@ -622,7 +665,7 @@ function loadAgentConfigs() {
|
|
|
622
665
|
try {
|
|
623
666
|
const content = readFileSync(path.join(agentsDir, file), "utf-8");
|
|
624
667
|
const config = parseAgentConfig(file, content);
|
|
625
|
-
if (config &&
|
|
668
|
+
if (config && "error" in config) {
|
|
626
669
|
console.error(`ERROR: ${file}: ${config.error}`);
|
|
627
670
|
continue;
|
|
628
671
|
}
|
|
@@ -653,7 +696,9 @@ function parseAgentConfig(filename, content) {
|
|
|
653
696
|
return { error: `Missing frontmatter. File must start with '---'` };
|
|
654
697
|
}
|
|
655
698
|
if (!normalized.includes("\n---\n")) {
|
|
656
|
-
return {
|
|
699
|
+
return {
|
|
700
|
+
error: `Frontmatter not closed. Add '---' on its own line after the YAML block`,
|
|
701
|
+
};
|
|
657
702
|
}
|
|
658
703
|
return { error: `Invalid frontmatter format` };
|
|
659
704
|
}
|
|
@@ -674,9 +719,13 @@ function parseAgentConfig(filename, content) {
|
|
|
674
719
|
const fieldName = line.trim().match(/^(\w+):/)?.[1];
|
|
675
720
|
if (fieldName && !knownFields.includes(fieldName)) {
|
|
676
721
|
// Suggest closest match
|
|
677
|
-
const suggestions = knownFields.filter(
|
|
722
|
+
const suggestions = knownFields.filter(
|
|
723
|
+
(f) => f[0] === fieldName[0] || fieldName.includes(f.slice(0, 3)),
|
|
724
|
+
);
|
|
678
725
|
const hint = suggestions.length > 0 ? ` Did you mean '${suggestions[0]}'?` : "";
|
|
679
|
-
return {
|
|
726
|
+
return {
|
|
727
|
+
error: `Unknown field '${fieldName}'.${hint} Valid fields: ${knownFields.join(", ")}`,
|
|
728
|
+
};
|
|
680
729
|
}
|
|
681
730
|
}
|
|
682
731
|
|
|
@@ -694,7 +743,9 @@ function parseAgentConfig(filename, content) {
|
|
|
694
743
|
const rawValue = intervalMatch[1].trim();
|
|
695
744
|
const parsed = parseInt(rawValue, 10);
|
|
696
745
|
if (isNaN(parsed)) {
|
|
697
|
-
return {
|
|
746
|
+
return {
|
|
747
|
+
error: `Invalid interval '${rawValue}'. Must be a number (seconds)`,
|
|
748
|
+
};
|
|
698
749
|
}
|
|
699
750
|
interval = Math.max(10, Math.min(3600, parsed)); // Clamp to 10s - 1hr
|
|
700
751
|
}
|
|
@@ -706,16 +757,22 @@ function parseAgentConfig(filename, content) {
|
|
|
706
757
|
const rawWatch = watchLine[1].trim();
|
|
707
758
|
// Must be array format
|
|
708
759
|
if (!rawWatch.startsWith("[") || !rawWatch.endsWith("]")) {
|
|
709
|
-
return {
|
|
760
|
+
return {
|
|
761
|
+
error: `Invalid watch format. Must be an array: watch: ["src/**/*.ts"]`,
|
|
762
|
+
};
|
|
710
763
|
}
|
|
711
764
|
const inner = rawWatch.slice(1, -1).trim();
|
|
712
765
|
if (!inner) {
|
|
713
|
-
return {
|
|
766
|
+
return {
|
|
767
|
+
error: `Empty watch array. Add at least one pattern: watch: ["**/*"]`,
|
|
768
|
+
};
|
|
714
769
|
}
|
|
715
770
|
watchPatterns = inner.split(",").map((p) => p.trim().replace(/^["']|["']$/g, ""));
|
|
716
771
|
// Validate patterns aren't empty
|
|
717
772
|
if (watchPatterns.some((p) => !p)) {
|
|
718
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
error: `Invalid watch pattern. Check for trailing commas or empty values`,
|
|
775
|
+
};
|
|
719
776
|
}
|
|
720
777
|
}
|
|
721
778
|
|
|
@@ -831,7 +888,9 @@ function gcMailbox(maxAgeHours = 24) {
|
|
|
831
888
|
/** @returns {string} */
|
|
832
889
|
function getCurrentBranch() {
|
|
833
890
|
try {
|
|
834
|
-
return execSync("git branch --show-current 2>/dev/null", {
|
|
891
|
+
return execSync("git branch --show-current 2>/dev/null", {
|
|
892
|
+
encoding: "utf-8",
|
|
893
|
+
}).trim();
|
|
835
894
|
} catch {
|
|
836
895
|
return "unknown";
|
|
837
896
|
}
|
|
@@ -840,7 +899,9 @@ function getCurrentBranch() {
|
|
|
840
899
|
/** @returns {string} */
|
|
841
900
|
function getCurrentCommit() {
|
|
842
901
|
try {
|
|
843
|
-
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
902
|
+
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
903
|
+
encoding: "utf-8",
|
|
904
|
+
}).trim();
|
|
844
905
|
} catch {
|
|
845
906
|
return "unknown";
|
|
846
907
|
}
|
|
@@ -864,7 +925,9 @@ function getMainBranch() {
|
|
|
864
925
|
/** @returns {string} */
|
|
865
926
|
function getStagedDiff() {
|
|
866
927
|
try {
|
|
867
|
-
return execSync("git diff --cached 2>/dev/null", {
|
|
928
|
+
return execSync("git diff --cached 2>/dev/null", {
|
|
929
|
+
encoding: "utf-8",
|
|
930
|
+
}).trim();
|
|
868
931
|
} catch {
|
|
869
932
|
return "";
|
|
870
933
|
}
|
|
@@ -889,20 +952,18 @@ function getRecentCommitsDiff(hoursAgo = 4) {
|
|
|
889
952
|
const since = `--since="${hoursAgo} hours ago"`;
|
|
890
953
|
|
|
891
954
|
// Get list of commits in range
|
|
892
|
-
const commits = execSync(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
).trim();
|
|
955
|
+
const commits = execSync(`git log ${mainBranch}..HEAD ${since} --oneline 2>/dev/null`, {
|
|
956
|
+
encoding: "utf-8",
|
|
957
|
+
}).trim();
|
|
896
958
|
|
|
897
959
|
if (!commits) return "";
|
|
898
960
|
|
|
899
961
|
// Get diff for those commits
|
|
900
962
|
const firstCommit = commits.split("\n").filter(Boolean).pop()?.split(" ")[0];
|
|
901
963
|
if (!firstCommit) return "";
|
|
902
|
-
return execSync(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
).trim();
|
|
964
|
+
return execSync(`git diff ${firstCommit}^..HEAD 2>/dev/null`, {
|
|
965
|
+
encoding: "utf-8",
|
|
966
|
+
}).trim();
|
|
906
967
|
} catch {
|
|
907
968
|
return "";
|
|
908
969
|
}
|
|
@@ -917,7 +978,10 @@ function truncateDiff(diff, maxLines = 200) {
|
|
|
917
978
|
if (!diff) return "";
|
|
918
979
|
const lines = diff.split("\n");
|
|
919
980
|
if (lines.length <= maxLines) return diff;
|
|
920
|
-
return
|
|
981
|
+
return (
|
|
982
|
+
lines.slice(0, maxLines).join("\n") +
|
|
983
|
+
`\n\n... (truncated, ${lines.length - maxLines} more lines)`
|
|
984
|
+
);
|
|
921
985
|
}
|
|
922
986
|
|
|
923
987
|
/**
|
|
@@ -1002,18 +1066,23 @@ function findCurrentClaudeSession() {
|
|
|
1002
1066
|
const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
|
|
1003
1067
|
if (existsSync(claudeProjectDir)) {
|
|
1004
1068
|
try {
|
|
1005
|
-
const files = readdirSync(claudeProjectDir).filter(f => f.endsWith(".jsonl"));
|
|
1069
|
+
const files = readdirSync(claudeProjectDir).filter((f) => f.endsWith(".jsonl"));
|
|
1006
1070
|
for (const file of files) {
|
|
1007
1071
|
const uuid = file.replace(".jsonl", "");
|
|
1008
1072
|
// Skip if we already have this from tmux sessions
|
|
1009
|
-
if (candidates.some(c => c.uuid === uuid)) continue;
|
|
1073
|
+
if (candidates.some((c) => c.uuid === uuid)) continue;
|
|
1010
1074
|
|
|
1011
1075
|
const logPath = path.join(claudeProjectDir, file);
|
|
1012
1076
|
try {
|
|
1013
1077
|
const stat = statSync(logPath);
|
|
1014
1078
|
// Only consider logs modified in the last hour (active sessions)
|
|
1015
1079
|
if (Date.now() - stat.mtimeMs < MAILBOX_MAX_AGE_MS) {
|
|
1016
|
-
candidates.push({
|
|
1080
|
+
candidates.push({
|
|
1081
|
+
session: null,
|
|
1082
|
+
uuid,
|
|
1083
|
+
mtime: stat.mtimeMs,
|
|
1084
|
+
logPath,
|
|
1085
|
+
});
|
|
1017
1086
|
}
|
|
1018
1087
|
} catch (err) {
|
|
1019
1088
|
debugError("findCurrentClaudeSession:logStat", err);
|
|
@@ -1085,7 +1154,9 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1085
1154
|
if (typeof c === "string" && c.length > 10) {
|
|
1086
1155
|
entries.push({ type: "user", text: c });
|
|
1087
1156
|
} else if (Array.isArray(c)) {
|
|
1088
|
-
const text = c.find(
|
|
1157
|
+
const text = c.find(
|
|
1158
|
+
/** @param {{type: string, text?: string}} x */ (x) => x.type === "text",
|
|
1159
|
+
)?.text;
|
|
1089
1160
|
if (text && text.length > 10) {
|
|
1090
1161
|
entries.push({ type: "user", text });
|
|
1091
1162
|
}
|
|
@@ -1093,7 +1164,10 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1093
1164
|
} else if (entry.type === "assistant") {
|
|
1094
1165
|
/** @type {{type: string, text?: string}[]} */
|
|
1095
1166
|
const parts = entry.message?.content || [];
|
|
1096
|
-
const text = parts
|
|
1167
|
+
const text = parts
|
|
1168
|
+
.filter((p) => p.type === "text")
|
|
1169
|
+
.map((p) => p.text || "")
|
|
1170
|
+
.join("\n");
|
|
1097
1171
|
// Only include assistant responses with meaningful text
|
|
1098
1172
|
if (text && text.length > 20) {
|
|
1099
1173
|
entries.push({ type: "assistant", text });
|
|
@@ -1105,7 +1179,7 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1105
1179
|
}
|
|
1106
1180
|
|
|
1107
1181
|
// Format recent conversation
|
|
1108
|
-
const formatted = entries.slice(-maxEntries).map(e => {
|
|
1182
|
+
const formatted = entries.slice(-maxEntries).map((e) => {
|
|
1109
1183
|
const preview = e.text.slice(0, 500).replace(/\n/g, " ");
|
|
1110
1184
|
return `**${e.type === "user" ? "User" : "Assistant"}**: ${preview}`;
|
|
1111
1185
|
});
|
|
@@ -1148,10 +1222,16 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1148
1222
|
|
|
1149
1223
|
// Parse all entries
|
|
1150
1224
|
/** @type {any[]} */
|
|
1151
|
-
const entries = lines
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1225
|
+
const entries = lines
|
|
1226
|
+
.map((line, idx) => {
|
|
1227
|
+
try {
|
|
1228
|
+
return { idx, ...JSON.parse(line) };
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
debugError("extractFileEditContext:parse", err);
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
})
|
|
1234
|
+
.filter(Boolean);
|
|
1155
1235
|
|
|
1156
1236
|
// Find Write/Edit tool calls for this file (scan backwards, want most recent)
|
|
1157
1237
|
/** @type {any} */
|
|
@@ -1164,9 +1244,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1164
1244
|
|
|
1165
1245
|
/** @type {any[]} */
|
|
1166
1246
|
const msgContent = entry.message?.content || [];
|
|
1167
|
-
const toolCalls = msgContent.filter(
|
|
1168
|
-
(
|
|
1169
|
-
|
|
1247
|
+
const toolCalls = msgContent.filter(
|
|
1248
|
+
(/** @type {any} */ c) =>
|
|
1249
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1250
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1170
1251
|
);
|
|
1171
1252
|
|
|
1172
1253
|
for (const tc of toolCalls) {
|
|
@@ -1217,8 +1298,9 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1217
1298
|
|
|
1218
1299
|
/** @type {any[]} */
|
|
1219
1300
|
const msgContent = entry.message?.content || [];
|
|
1220
|
-
const readCalls = msgContent.filter(
|
|
1221
|
-
(
|
|
1301
|
+
const readCalls = msgContent.filter(
|
|
1302
|
+
(/** @type {any} */ c) =>
|
|
1303
|
+
(c.type === "tool_use" || c.type === "tool_call") && c.name === "Read",
|
|
1222
1304
|
);
|
|
1223
1305
|
|
|
1224
1306
|
for (const rc of readCalls) {
|
|
@@ -1233,9 +1315,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1233
1315
|
if (entry.type !== "assistant") continue;
|
|
1234
1316
|
/** @type {any[]} */
|
|
1235
1317
|
const msgContent = entry.message?.content || [];
|
|
1236
|
-
const edits = msgContent.filter(
|
|
1237
|
-
(
|
|
1238
|
-
|
|
1318
|
+
const edits = msgContent.filter(
|
|
1319
|
+
(/** @type {any} */ c) =>
|
|
1320
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1321
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1239
1322
|
);
|
|
1240
1323
|
for (const e of edits) {
|
|
1241
1324
|
const input = e.input || e.arguments || {};
|
|
@@ -1250,11 +1333,11 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1250
1333
|
toolCall: {
|
|
1251
1334
|
name: editEntry.toolCall.name,
|
|
1252
1335
|
input: editEntry.toolCall.input || editEntry.toolCall.arguments,
|
|
1253
|
-
id: editEntry.toolCall.id
|
|
1336
|
+
id: editEntry.toolCall.id,
|
|
1254
1337
|
},
|
|
1255
1338
|
subsequentErrors,
|
|
1256
1339
|
readsBefore: [...new Set(readsBefore)].slice(0, 10),
|
|
1257
|
-
editSequence
|
|
1340
|
+
editSequence,
|
|
1258
1341
|
};
|
|
1259
1342
|
}
|
|
1260
1343
|
|
|
@@ -1348,10 +1431,11 @@ function watchForChanges(patterns, callback) {
|
|
|
1348
1431
|
}
|
|
1349
1432
|
}
|
|
1350
1433
|
|
|
1351
|
-
return () => {
|
|
1434
|
+
return () => {
|
|
1435
|
+
for (const w of watchers) w.close();
|
|
1436
|
+
};
|
|
1352
1437
|
}
|
|
1353
1438
|
|
|
1354
|
-
|
|
1355
1439
|
// =============================================================================
|
|
1356
1440
|
// State
|
|
1357
1441
|
// =============================================================================
|
|
@@ -1429,7 +1513,7 @@ function detectState(screen, config) {
|
|
|
1429
1513
|
// Check if any line has the prompt followed by pasted content indicator
|
|
1430
1514
|
const linesArray = lastLines.split("\n");
|
|
1431
1515
|
const promptWithPaste = linesArray.some(
|
|
1432
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text")
|
|
1516
|
+
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
1433
1517
|
);
|
|
1434
1518
|
if (!promptWithPaste) {
|
|
1435
1519
|
return State.READY;
|
|
@@ -1640,7 +1724,12 @@ class Agent {
|
|
|
1640
1724
|
if (/^(run|execute|create|delete|modify|write)/i.test(line)) return line;
|
|
1641
1725
|
}
|
|
1642
1726
|
|
|
1643
|
-
return
|
|
1727
|
+
return (
|
|
1728
|
+
lines
|
|
1729
|
+
.filter((l) => l && !l.match(/^[╭╮╰╯│─]+$/))
|
|
1730
|
+
.slice(0, 2)
|
|
1731
|
+
.join(" | ") || "action"
|
|
1732
|
+
);
|
|
1644
1733
|
}
|
|
1645
1734
|
|
|
1646
1735
|
/**
|
|
@@ -1716,7 +1805,9 @@ class Agent {
|
|
|
1716
1805
|
|
|
1717
1806
|
// Fallback: extract after last prompt
|
|
1718
1807
|
if (filtered.length === 0) {
|
|
1719
|
-
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
1808
|
+
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
1809
|
+
l.startsWith(this.promptSymbol),
|
|
1810
|
+
);
|
|
1720
1811
|
if (lastPromptIdx >= 0 && lastPromptIdx < lines.length - 1) {
|
|
1721
1812
|
const afterPrompt = lines
|
|
1722
1813
|
.slice(lastPromptIdx + 1)
|
|
@@ -1730,14 +1821,14 @@ class Agent {
|
|
|
1730
1821
|
// This handles the case where Claude finished and shows a new empty prompt
|
|
1731
1822
|
if (lastPromptIdx >= 0) {
|
|
1732
1823
|
const lastPromptLine = lines[lastPromptIdx];
|
|
1733
|
-
const isEmptyPrompt =
|
|
1734
|
-
|
|
1824
|
+
const isEmptyPrompt =
|
|
1825
|
+
lastPromptLine.trim() === this.promptSymbol || lastPromptLine.match(/^❯\s*$/);
|
|
1735
1826
|
if (isEmptyPrompt) {
|
|
1736
1827
|
// Find the previous prompt (user's input) and extract content between
|
|
1737
1828
|
// Note: [Pasted text is Claude's truncated output indicator, NOT a prompt
|
|
1738
|
-
const prevPromptIdx = lines
|
|
1739
|
-
(
|
|
1740
|
-
|
|
1829
|
+
const prevPromptIdx = lines
|
|
1830
|
+
.slice(0, lastPromptIdx)
|
|
1831
|
+
.findLastIndex((/** @type {string} */ l) => l.startsWith(this.promptSymbol));
|
|
1741
1832
|
if (prevPromptIdx >= 0) {
|
|
1742
1833
|
const betweenPrompts = lines
|
|
1743
1834
|
.slice(prevPromptIdx + 1, lastPromptIdx)
|
|
@@ -1758,23 +1849,25 @@ class Agent {
|
|
|
1758
1849
|
* @returns {string}
|
|
1759
1850
|
*/
|
|
1760
1851
|
cleanResponse(response) {
|
|
1761
|
-
return
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1852
|
+
return (
|
|
1853
|
+
response
|
|
1854
|
+
// Remove tool call lines (Search, Read, Grep, etc.)
|
|
1855
|
+
.replace(/^[⏺•]\s*(Search|Read|Grep|Glob|Write|Edit|Bash)\([^)]*\).*$/gm, "")
|
|
1856
|
+
// Remove tool result lines
|
|
1857
|
+
.replace(/^⎿\s+.*$/gm, "")
|
|
1858
|
+
// Remove "Sautéed for Xs" timing lines
|
|
1859
|
+
.replace(/^✻\s+Sautéed for.*$/gm, "")
|
|
1860
|
+
// Remove expand hints
|
|
1861
|
+
.replace(/\(ctrl\+o to expand\)/g, "")
|
|
1862
|
+
// Clean up multiple blank lines
|
|
1863
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1864
|
+
// Original cleanup
|
|
1865
|
+
.replace(/^[•⏺-]\s*/, "")
|
|
1866
|
+
.replace(/^\*\*(.+)\*\*/, "$1")
|
|
1867
|
+
.replace(/\n /g, "\n")
|
|
1868
|
+
.replace(/─+\s*$/, "")
|
|
1869
|
+
.trim()
|
|
1870
|
+
);
|
|
1778
1871
|
}
|
|
1779
1872
|
|
|
1780
1873
|
/**
|
|
@@ -1857,9 +1950,18 @@ const ClaudeAgent = new Agent({
|
|
|
1857
1950
|
],
|
|
1858
1951
|
updatePromptPatterns: null,
|
|
1859
1952
|
responseMarkers: ["⏺", "•", "- ", "**"],
|
|
1860
|
-
chromePatterns: [
|
|
1953
|
+
chromePatterns: [
|
|
1954
|
+
"↵ send",
|
|
1955
|
+
"Esc to cancel",
|
|
1956
|
+
"shortcuts",
|
|
1957
|
+
"for more options",
|
|
1958
|
+
"docs.anthropic.com",
|
|
1959
|
+
"⏵⏵",
|
|
1960
|
+
"bypass permissions",
|
|
1961
|
+
"shift+Tab to cycle",
|
|
1962
|
+
],
|
|
1861
1963
|
reviewOptions: null,
|
|
1862
|
-
safeAllowedTools: "Bash(git:*) Read Glob Grep",
|
|
1964
|
+
safeAllowedTools: "Bash(git:*) Read Glob Grep", // Default: auto-approve read-only tools
|
|
1863
1965
|
envVar: "AX_SESSION",
|
|
1864
1966
|
approveKey: "1",
|
|
1865
1967
|
rejectKey: "Escape",
|
|
@@ -1883,7 +1985,11 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1883
1985
|
const initialState = agent.getState(initialScreen);
|
|
1884
1986
|
|
|
1885
1987
|
// Already in terminal state
|
|
1886
|
-
if (
|
|
1988
|
+
if (
|
|
1989
|
+
initialState === State.RATE_LIMITED ||
|
|
1990
|
+
initialState === State.CONFIRMING ||
|
|
1991
|
+
initialState === State.READY
|
|
1992
|
+
) {
|
|
1887
1993
|
return { state: initialState, screen: initialScreen };
|
|
1888
1994
|
}
|
|
1889
1995
|
|
|
@@ -2063,11 +2169,11 @@ function cmdAgents() {
|
|
|
2063
2169
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2064
2170
|
|
|
2065
2171
|
console.log(
|
|
2066
|
-
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)} LOG
|
|
2172
|
+
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)} LOG`,
|
|
2067
2173
|
);
|
|
2068
2174
|
for (const a of agents) {
|
|
2069
2175
|
console.log(
|
|
2070
|
-
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.log}
|
|
2176
|
+
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.log}`,
|
|
2071
2177
|
);
|
|
2072
2178
|
}
|
|
2073
2179
|
}
|
|
@@ -2114,7 +2220,9 @@ function startArchangel(config, parentSession = null) {
|
|
|
2114
2220
|
env,
|
|
2115
2221
|
});
|
|
2116
2222
|
child.unref();
|
|
2117
|
-
console.log(
|
|
2223
|
+
console.log(
|
|
2224
|
+
`Summoning: ${config.name} (pid ${child.pid})${parentSession ? ` [parent: ${parentSession.session}]` : ""}`,
|
|
2225
|
+
);
|
|
2118
2226
|
}
|
|
2119
2227
|
|
|
2120
2228
|
// =============================================================================
|
|
@@ -2150,7 +2258,9 @@ async function cmdArchangel(agentName) {
|
|
|
2150
2258
|
// Check agent CLI is installed before trying to start
|
|
2151
2259
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
2152
2260
|
if (cliCheck.status !== 0) {
|
|
2153
|
-
console.error(
|
|
2261
|
+
console.error(
|
|
2262
|
+
`[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`,
|
|
2263
|
+
);
|
|
2154
2264
|
process.exit(1);
|
|
2155
2265
|
}
|
|
2156
2266
|
|
|
@@ -2212,7 +2322,7 @@ async function cmdArchangel(agentName) {
|
|
|
2212
2322
|
isProcessing = true;
|
|
2213
2323
|
|
|
2214
2324
|
const files = [...changedFiles];
|
|
2215
|
-
changedFiles = new Set();
|
|
2325
|
+
changedFiles = new Set(); // atomic swap to avoid losing changes during processing
|
|
2216
2326
|
|
|
2217
2327
|
try {
|
|
2218
2328
|
// Get parent session log path for JSONL extraction
|
|
@@ -2221,7 +2331,8 @@ async function cmdArchangel(agentName) {
|
|
|
2221
2331
|
|
|
2222
2332
|
// Build file-specific context from JSONL
|
|
2223
2333
|
const fileContexts = [];
|
|
2224
|
-
for (const file of files.slice(0, 5)) {
|
|
2334
|
+
for (const file of files.slice(0, 5)) {
|
|
2335
|
+
// Limit to 5 files
|
|
2225
2336
|
const ctx = extractFileEditContext(logPath, file);
|
|
2226
2337
|
if (ctx) {
|
|
2227
2338
|
fileContexts.push({ file, ...ctx });
|
|
@@ -2248,26 +2359,34 @@ async function cmdArchangel(agentName) {
|
|
|
2248
2359
|
}
|
|
2249
2360
|
|
|
2250
2361
|
if (ctx.readsBefore.length > 0) {
|
|
2251
|
-
const reads = ctx.readsBefore.map(f => f.split("/").pop()).join(", ");
|
|
2362
|
+
const reads = ctx.readsBefore.map((f) => f.split("/").pop()).join(", ");
|
|
2252
2363
|
prompt += `**Files read before:** ${reads}\n`;
|
|
2253
2364
|
}
|
|
2254
2365
|
}
|
|
2255
2366
|
|
|
2256
2367
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
2257
2368
|
|
|
2258
|
-
const gitContext = buildGitContext(
|
|
2369
|
+
const gitContext = buildGitContext(
|
|
2370
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2371
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2372
|
+
);
|
|
2259
2373
|
if (gitContext) {
|
|
2260
2374
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2261
2375
|
}
|
|
2262
2376
|
|
|
2263
|
-
prompt +=
|
|
2377
|
+
prompt +=
|
|
2378
|
+
'\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
2379
|
} else {
|
|
2265
2380
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2266
2381
|
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
2267
|
-
const gitContext = buildGitContext(
|
|
2382
|
+
const gitContext = buildGitContext(
|
|
2383
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2384
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2385
|
+
);
|
|
2268
2386
|
|
|
2269
2387
|
if (parentContext) {
|
|
2270
|
-
prompt +=
|
|
2388
|
+
prompt +=
|
|
2389
|
+
"\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
|
|
2271
2390
|
}
|
|
2272
2391
|
|
|
2273
2392
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
@@ -2276,10 +2395,10 @@ async function cmdArchangel(agentName) {
|
|
|
2276
2395
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2277
2396
|
}
|
|
2278
2397
|
|
|
2279
|
-
prompt +=
|
|
2398
|
+
prompt +=
|
|
2399
|
+
'\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
2400
|
}
|
|
2281
2401
|
|
|
2282
|
-
|
|
2283
2402
|
// Check session still exists
|
|
2284
2403
|
if (!tmuxHasSession(sessionName)) {
|
|
2285
2404
|
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
@@ -2308,22 +2427,30 @@ async function cmdArchangel(agentName) {
|
|
|
2308
2427
|
await sleep(100); // Ensure Enter is processed
|
|
2309
2428
|
|
|
2310
2429
|
// Wait for response
|
|
2311
|
-
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2430
|
+
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2431
|
+
agent,
|
|
2432
|
+
sessionName,
|
|
2433
|
+
ARCHANGEL_RESPONSE_TIMEOUT_MS,
|
|
2434
|
+
);
|
|
2312
2435
|
|
|
2313
2436
|
if (endState === State.RATE_LIMITED) {
|
|
2314
2437
|
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2315
2438
|
process.exit(2);
|
|
2316
2439
|
}
|
|
2317
2440
|
|
|
2318
|
-
|
|
2319
2441
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2320
2442
|
|
|
2321
2443
|
// Sanity check: skip garbage responses (screen scraping artifacts)
|
|
2322
|
-
const isGarbage =
|
|
2444
|
+
const isGarbage =
|
|
2445
|
+
cleanedResponse.includes("[Pasted text") ||
|
|
2323
2446
|
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2324
2447
|
cleanedResponse.length < 20;
|
|
2325
2448
|
|
|
2326
|
-
if (
|
|
2449
|
+
if (
|
|
2450
|
+
cleanedResponse &&
|
|
2451
|
+
!isGarbage &&
|
|
2452
|
+
!cleanedResponse.toLowerCase().includes("no issues found")
|
|
2453
|
+
) {
|
|
2327
2454
|
writeToMailbox({
|
|
2328
2455
|
agent: /** @type {string} */ (agentName),
|
|
2329
2456
|
session: sessionName,
|
|
@@ -2345,7 +2472,10 @@ async function cmdArchangel(agentName) {
|
|
|
2345
2472
|
|
|
2346
2473
|
function scheduleProcessChanges() {
|
|
2347
2474
|
processChanges().catch((err) => {
|
|
2348
|
-
console.error(
|
|
2475
|
+
console.error(
|
|
2476
|
+
`[archangel:${agentName}] Unhandled error:`,
|
|
2477
|
+
err instanceof Error ? err.message : err,
|
|
2478
|
+
);
|
|
2349
2479
|
});
|
|
2350
2480
|
}
|
|
2351
2481
|
|
|
@@ -2499,7 +2629,7 @@ async function cmdRecall(name = null) {
|
|
|
2499
2629
|
}
|
|
2500
2630
|
|
|
2501
2631
|
// Version of the hook script template - bump when making changes
|
|
2502
|
-
const HOOK_SCRIPT_VERSION = "
|
|
2632
|
+
const HOOK_SCRIPT_VERSION = "4";
|
|
2503
2633
|
|
|
2504
2634
|
function ensureMailboxHookScript() {
|
|
2505
2635
|
const hooksDir = HOOKS_DIR;
|
|
@@ -2522,24 +2652,49 @@ ${versionMarker}
|
|
|
2522
2652
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2523
2653
|
import { dirname, join } from "node:path";
|
|
2524
2654
|
import { fileURLToPath } from "node:url";
|
|
2655
|
+
import { createHash } from "node:crypto";
|
|
2525
2656
|
|
|
2526
2657
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2527
2658
|
const AI_DIR = join(__dirname, "..");
|
|
2528
2659
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
2529
2660
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
2530
|
-
const LAST_SEEN = join(AI_DIR, "mailbox-last-seen");
|
|
2531
2661
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2532
2662
|
|
|
2663
|
+
// Read hook input from stdin
|
|
2664
|
+
let hookInput = {};
|
|
2665
|
+
try {
|
|
2666
|
+
const stdinData = readFileSync(0, "utf-8").trim();
|
|
2667
|
+
if (stdinData) hookInput = JSON.parse(stdinData);
|
|
2668
|
+
} catch (err) {
|
|
2669
|
+
if (DEBUG) console.error("[hook] stdin parse:", err.message);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
const sessionId = hookInput.session_id || "";
|
|
2673
|
+
const hookEvent = hookInput.hook_event_name || "";
|
|
2674
|
+
|
|
2675
|
+
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
2676
|
+
|
|
2677
|
+
// NO-OP for archangel or partner sessions
|
|
2678
|
+
if (sessionId.includes("-archangel-") || sessionId.includes("-partner-")) {
|
|
2679
|
+
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
2680
|
+
process.exit(0);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// Per-session last-seen tracking (single JSON file, self-cleaning)
|
|
2684
|
+
const sessionHash = sessionId ? createHash("md5").update(sessionId).digest("hex").slice(0, 8) : "default";
|
|
2685
|
+
const LAST_SEEN_FILE = join(AI_DIR, "mailbox-last-seen.json");
|
|
2686
|
+
|
|
2533
2687
|
if (!existsSync(MAILBOX)) process.exit(0);
|
|
2534
2688
|
|
|
2535
|
-
let
|
|
2689
|
+
let lastSeenMap = {};
|
|
2536
2690
|
try {
|
|
2537
|
-
if (existsSync(
|
|
2538
|
-
|
|
2691
|
+
if (existsSync(LAST_SEEN_FILE)) {
|
|
2692
|
+
lastSeenMap = JSON.parse(readFileSync(LAST_SEEN_FILE, "utf-8"));
|
|
2539
2693
|
}
|
|
2540
2694
|
} catch (err) {
|
|
2541
2695
|
if (DEBUG) console.error("[hook] readLastSeen:", err.message);
|
|
2542
2696
|
}
|
|
2697
|
+
const lastSeen = lastSeenMap[sessionHash] || 0;
|
|
2543
2698
|
|
|
2544
2699
|
const now = Date.now();
|
|
2545
2700
|
const lines = readFileSync(MAILBOX, "utf-8").trim().split("\\n").filter(Boolean);
|
|
@@ -2561,21 +2716,39 @@ for (const line of lines) {
|
|
|
2561
2716
|
}
|
|
2562
2717
|
|
|
2563
2718
|
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
2719
|
const sessionPrefixes = new Set();
|
|
2720
|
+
let messageLines = [];
|
|
2721
|
+
messageLines.push("## Background Agents");
|
|
2722
|
+
messageLines.push("");
|
|
2723
|
+
messageLines.push("Background agents watching your files found:");
|
|
2724
|
+
messageLines.push("");
|
|
2569
2725
|
for (const { agent, sessionPrefix, message } of relevant) {
|
|
2570
2726
|
if (sessionPrefix) sessionPrefixes.add(sessionPrefix);
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2727
|
+
messageLines.push("**[" + agent + "]**");
|
|
2728
|
+
messageLines.push("");
|
|
2729
|
+
messageLines.push(message);
|
|
2730
|
+
messageLines.push("");
|
|
2575
2731
|
}
|
|
2576
2732
|
const sessionList = [...sessionPrefixes].map(s => "\\\`./ax.js log " + s + "\\\`").join(" or ");
|
|
2577
|
-
|
|
2578
|
-
|
|
2733
|
+
messageLines.push("> For more context: \\\`./ax.js mailbox\\\`" + (sessionList ? " or " + sessionList : ""));
|
|
2734
|
+
|
|
2735
|
+
const formattedMessage = messageLines.join("\\n");
|
|
2736
|
+
|
|
2737
|
+
// For Stop hook, return blocking JSON to force acknowledgment
|
|
2738
|
+
if (hookEvent === "Stop") {
|
|
2739
|
+
console.log(JSON.stringify({ decision: "block", reason: formattedMessage }));
|
|
2740
|
+
} else {
|
|
2741
|
+
// For other hooks, just output the context
|
|
2742
|
+
console.log(formattedMessage);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// Update last-seen and prune entries older than 24 hours
|
|
2746
|
+
const PRUNE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
2747
|
+
lastSeenMap[sessionHash] = now;
|
|
2748
|
+
for (const key of Object.keys(lastSeenMap)) {
|
|
2749
|
+
if (now - lastSeenMap[key] > PRUNE_AGE_MS) delete lastSeenMap[key];
|
|
2750
|
+
}
|
|
2751
|
+
writeFileSync(LAST_SEEN_FILE, JSON.stringify(lastSeenMap));
|
|
2579
2752
|
}
|
|
2580
2753
|
|
|
2581
2754
|
process.exit(0);
|
|
@@ -2590,18 +2763,9 @@ process.exit(0);
|
|
|
2590
2763
|
console.log(`\nTo enable manually, add to .claude/settings.json:\n`);
|
|
2591
2764
|
console.log(`{
|
|
2592
2765
|
"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
|
-
]
|
|
2766
|
+
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
2767
|
+
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
2768
|
+
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }]
|
|
2605
2769
|
}
|
|
2606
2770
|
}`);
|
|
2607
2771
|
}
|
|
@@ -2611,6 +2775,7 @@ function ensureClaudeHookConfig() {
|
|
|
2611
2775
|
const settingsDir = ".claude";
|
|
2612
2776
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
2613
2777
|
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
2778
|
+
const hookEvents = ["UserPromptSubmit", "PostToolUse", "Stop"];
|
|
2614
2779
|
|
|
2615
2780
|
try {
|
|
2616
2781
|
/** @type {ClaudeSettings} */
|
|
@@ -2629,33 +2794,41 @@ function ensureClaudeHookConfig() {
|
|
|
2629
2794
|
|
|
2630
2795
|
// Ensure hooks structure exists
|
|
2631
2796
|
if (!settings.hooks) settings.hooks = {};
|
|
2632
|
-
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
2633
2797
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
(
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2798
|
+
let anyAdded = false;
|
|
2799
|
+
|
|
2800
|
+
for (const eventName of hookEvents) {
|
|
2801
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
2802
|
+
|
|
2803
|
+
// Check if our hook is already configured for this event
|
|
2804
|
+
const hookExists = settings.hooks[eventName].some(
|
|
2805
|
+
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
2806
|
+
(entry) =>
|
|
2807
|
+
entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand),
|
|
2808
|
+
);
|
|
2809
|
+
|
|
2810
|
+
if (!hookExists) {
|
|
2811
|
+
// Add the hook for this event
|
|
2812
|
+
settings.hooks[eventName].push({
|
|
2813
|
+
matcher: "",
|
|
2814
|
+
hooks: [
|
|
2815
|
+
{
|
|
2816
|
+
type: "command",
|
|
2817
|
+
command: hookCommand,
|
|
2818
|
+
timeout: 5,
|
|
2819
|
+
},
|
|
2820
|
+
],
|
|
2821
|
+
});
|
|
2822
|
+
anyAdded = true;
|
|
2823
|
+
}
|
|
2642
2824
|
}
|
|
2643
2825
|
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
hooks:
|
|
2648
|
-
|
|
2649
|
-
type: "command",
|
|
2650
|
-
command: hookCommand,
|
|
2651
|
-
timeout: 5,
|
|
2652
|
-
},
|
|
2653
|
-
],
|
|
2654
|
-
});
|
|
2826
|
+
if (anyAdded) {
|
|
2827
|
+
// Write settings
|
|
2828
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2829
|
+
console.log(`Configured hooks in: ${settingsPath}`);
|
|
2830
|
+
}
|
|
2655
2831
|
|
|
2656
|
-
// Write settings
|
|
2657
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2658
|
-
console.log(`Configured hook in: ${settingsPath}`);
|
|
2659
2832
|
return true;
|
|
2660
2833
|
} catch {
|
|
2661
2834
|
// If we can't configure automatically, return false so manual instructions are shown
|
|
@@ -2727,7 +2900,9 @@ function cmdAttach(session) {
|
|
|
2727
2900
|
}
|
|
2728
2901
|
|
|
2729
2902
|
// Hand over to tmux attach
|
|
2730
|
-
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
2903
|
+
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
2904
|
+
stdio: "inherit",
|
|
2905
|
+
});
|
|
2731
2906
|
process.exit(result.status || 0);
|
|
2732
2907
|
}
|
|
2733
2908
|
|
|
@@ -2786,13 +2961,15 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2786
2961
|
|
|
2787
2962
|
if (newLines.length === 0) return;
|
|
2788
2963
|
|
|
2789
|
-
const entries = newLines
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2964
|
+
const entries = newLines
|
|
2965
|
+
.map((line) => {
|
|
2966
|
+
try {
|
|
2967
|
+
return JSON.parse(line);
|
|
2968
|
+
} catch {
|
|
2969
|
+
return null;
|
|
2970
|
+
}
|
|
2971
|
+
})
|
|
2972
|
+
.filter(Boolean);
|
|
2796
2973
|
|
|
2797
2974
|
const output = [];
|
|
2798
2975
|
if (isInitial) {
|
|
@@ -2805,7 +2982,10 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2805
2982
|
const ts = entry.timestamp || entry.ts || entry.createdAt;
|
|
2806
2983
|
if (ts && ts !== lastTimestamp) {
|
|
2807
2984
|
const date = new Date(ts);
|
|
2808
|
-
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
2985
|
+
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
2986
|
+
hour: "2-digit",
|
|
2987
|
+
minute: "2-digit",
|
|
2988
|
+
});
|
|
2809
2989
|
if (formatted.isUserMessage) {
|
|
2810
2990
|
output.push(`\n### ${timeStr}\n`);
|
|
2811
2991
|
}
|
|
@@ -2855,7 +3035,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2855
3035
|
if (type === "user" || type === "human") {
|
|
2856
3036
|
const text = extractTextContent(content);
|
|
2857
3037
|
if (text) {
|
|
2858
|
-
return {
|
|
3038
|
+
return {
|
|
3039
|
+
text: `**User**: ${truncate(text, TRUNCATE_USER_LEN)}\n`,
|
|
3040
|
+
isUserMessage: true,
|
|
3041
|
+
};
|
|
2859
3042
|
}
|
|
2860
3043
|
}
|
|
2861
3044
|
|
|
@@ -2871,10 +3054,12 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2871
3054
|
// Extract tool calls (compressed)
|
|
2872
3055
|
const tools = extractToolCalls(content);
|
|
2873
3056
|
if (tools.length > 0) {
|
|
2874
|
-
const toolSummary = tools
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3057
|
+
const toolSummary = tools
|
|
3058
|
+
.map((t) => {
|
|
3059
|
+
if (t.error) return `${t.name}(${t.target}) ✗`;
|
|
3060
|
+
return `${t.name}(${t.target})`;
|
|
3061
|
+
})
|
|
3062
|
+
.join(", ");
|
|
2878
3063
|
parts.push(`> ${toolSummary}\n`);
|
|
2879
3064
|
}
|
|
2880
3065
|
|
|
@@ -2896,7 +3081,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2896
3081
|
const error = entry.error || entry.is_error;
|
|
2897
3082
|
if (error) {
|
|
2898
3083
|
const name = entry.tool_name || entry.name || "tool";
|
|
2899
|
-
return {
|
|
3084
|
+
return {
|
|
3085
|
+
text: `> ${name} ✗ (${truncate(String(error), 100)})\n`,
|
|
3086
|
+
isUserMessage: false,
|
|
3087
|
+
};
|
|
2900
3088
|
}
|
|
2901
3089
|
}
|
|
2902
3090
|
|
|
@@ -2933,7 +3121,8 @@ function extractToolCalls(content) {
|
|
|
2933
3121
|
const name = c.name || c.tool || "tool";
|
|
2934
3122
|
const input = c.input || c.arguments || {};
|
|
2935
3123
|
// Extract a reasonable target from the input
|
|
2936
|
-
const target =
|
|
3124
|
+
const target =
|
|
3125
|
+
input.file_path || input.path || input.command?.slice(0, 30) || input.pattern || "";
|
|
2937
3126
|
const shortTarget = target.split("/").pop() || target.slice(0, 20);
|
|
2938
3127
|
return { name, target: shortTarget, error: c.error };
|
|
2939
3128
|
});
|
|
@@ -2978,8 +3167,14 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
2978
3167
|
|
|
2979
3168
|
for (const entry of entries) {
|
|
2980
3169
|
const ts = new Date(entry.timestamp);
|
|
2981
|
-
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
2982
|
-
|
|
3170
|
+
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3171
|
+
hour: "2-digit",
|
|
3172
|
+
minute: "2-digit",
|
|
3173
|
+
});
|
|
3174
|
+
const dateStr = ts.toLocaleDateString("en-GB", {
|
|
3175
|
+
month: "short",
|
|
3176
|
+
day: "numeric",
|
|
3177
|
+
});
|
|
2983
3178
|
const p = entry.payload || {};
|
|
2984
3179
|
|
|
2985
3180
|
console.log(`### [${p.agent || "unknown"}] ${dateStr} ${timeStr}\n`);
|
|
@@ -3020,7 +3215,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3020
3215
|
}
|
|
3021
3216
|
|
|
3022
3217
|
/** @type {string} */
|
|
3023
|
-
const activeSession = sessionExists
|
|
3218
|
+
const activeSession = sessionExists
|
|
3219
|
+
? /** @type {string} */ (session)
|
|
3220
|
+
: await cmdStart(agent, session, { yolo });
|
|
3024
3221
|
|
|
3025
3222
|
tmuxSendLiteral(activeSession, message);
|
|
3026
3223
|
await sleep(50);
|
|
@@ -3121,7 +3318,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3121
3318
|
* @param {string | null | undefined} customInstructions
|
|
3122
3319
|
* @param {{wait?: boolean, yolo?: boolean, fresh?: boolean, timeoutMs?: number}} [options]
|
|
3123
3320
|
*/
|
|
3124
|
-
async function cmdReview(
|
|
3321
|
+
async function cmdReview(
|
|
3322
|
+
agent,
|
|
3323
|
+
session,
|
|
3324
|
+
option,
|
|
3325
|
+
customInstructions,
|
|
3326
|
+
{ wait = true, yolo = true, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3327
|
+
) {
|
|
3125
3328
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3126
3329
|
|
|
3127
3330
|
// Reset conversation if --fresh and session exists
|
|
@@ -3147,7 +3350,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3147
3350
|
|
|
3148
3351
|
// AX_REVIEW_MODE=exec: bypass /review command, send instructions directly
|
|
3149
3352
|
if (process.env.AX_REVIEW_MODE === "exec" && option === "custom" && customInstructions) {
|
|
3150
|
-
return cmdAsk(agent, session, customInstructions, {
|
|
3353
|
+
return cmdAsk(agent, session, customInstructions, {
|
|
3354
|
+
noWait: !wait,
|
|
3355
|
+
yolo,
|
|
3356
|
+
timeoutMs,
|
|
3357
|
+
});
|
|
3151
3358
|
}
|
|
3152
3359
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3153
3360
|
|
|
@@ -3159,7 +3366,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3159
3366
|
}
|
|
3160
3367
|
|
|
3161
3368
|
/** @type {string} */
|
|
3162
|
-
const activeSession = sessionExists
|
|
3369
|
+
const activeSession = sessionExists
|
|
3370
|
+
? /** @type {string} */ (session)
|
|
3371
|
+
: await cmdStart(agent, session, { yolo });
|
|
3163
3372
|
|
|
3164
3373
|
tmuxSendLiteral(activeSession, "/review");
|
|
3165
3374
|
await sleep(50);
|
|
@@ -3420,7 +3629,7 @@ function getAgentFromInvocation() {
|
|
|
3420
3629
|
*/
|
|
3421
3630
|
function printHelp(agent, cliName) {
|
|
3422
3631
|
const name = cliName;
|
|
3423
|
-
const backendName = agent.name === "codex" ? "
|
|
3632
|
+
const backendName = agent.name === "codex" ? "Codex" : "Claude";
|
|
3424
3633
|
const hasReview = !!agent.reviewOptions;
|
|
3425
3634
|
|
|
3426
3635
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
@@ -3435,8 +3644,12 @@ Commands:
|
|
|
3435
3644
|
kill Kill sessions in current project (--all for all, --session=NAME for one)
|
|
3436
3645
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3437
3646
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3438
|
-
debug Show raw screen output and detected state${
|
|
3439
|
-
|
|
3647
|
+
debug Show raw screen output and detected state${
|
|
3648
|
+
hasReview
|
|
3649
|
+
? `
|
|
3650
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
3651
|
+
: ""
|
|
3652
|
+
}
|
|
3440
3653
|
select N Select menu option N
|
|
3441
3654
|
approve Approve pending action (send 'y')
|
|
3442
3655
|
reject Reject pending action (send 'n')
|
|
@@ -3455,30 +3668,28 @@ Flags:
|
|
|
3455
3668
|
--fresh Reset conversation before review
|
|
3456
3669
|
|
|
3457
3670
|
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
|
|
3671
|
+
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
3672
|
+
${agent.envVar} Override default session name
|
|
3673
|
+
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
3674
|
+
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
3675
|
+
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
3676
|
+
AX_DEBUG=1 Enable debug logging
|
|
3464
3677
|
|
|
3465
3678
|
Examples:
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
./${name}.js recall reviewer # Recall one by name
|
|
3481
|
-
./${name}.js agents # List all agents (shows TYPE=archangel)`);
|
|
3679
|
+
${name} "explain this codebase"
|
|
3680
|
+
${name} "please review the error handling" # Auto custom review
|
|
3681
|
+
${name} review uncommitted --wait
|
|
3682
|
+
${name} approve --wait
|
|
3683
|
+
${name} kill # Kill agents in current project
|
|
3684
|
+
${name} kill --all # Kill all agents across all projects
|
|
3685
|
+
${name} kill --session=NAME # Kill specific session
|
|
3686
|
+
${name} send "1[Enter]" # Recovery: select option 1 and press Enter
|
|
3687
|
+
${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
|
|
3688
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
3689
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
3690
|
+
${name} recall # Recall all archangels
|
|
3691
|
+
${name} recall reviewer # Recall one by name
|
|
3692
|
+
${name} agents # List all agents (shows TYPE=archangel)`);
|
|
3482
3693
|
}
|
|
3483
3694
|
|
|
3484
3695
|
async function main() {
|
|
@@ -3582,7 +3793,7 @@ async function main() {
|
|
|
3582
3793
|
!a.startsWith("--tool") &&
|
|
3583
3794
|
!a.startsWith("--tail") &&
|
|
3584
3795
|
!a.startsWith("--limit") &&
|
|
3585
|
-
!a.startsWith("--branch")
|
|
3796
|
+
!a.startsWith("--branch"),
|
|
3586
3797
|
);
|
|
3587
3798
|
const cmd = filteredArgs[0];
|
|
3588
3799
|
|
|
@@ -3597,7 +3808,13 @@ async function main() {
|
|
|
3597
3808
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3598
3809
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3599
3810
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3600
|
-
if (cmd === "review")
|
|
3811
|
+
if (cmd === "review")
|
|
3812
|
+
return cmdReview(agent, session, filteredArgs[1], filteredArgs[2], {
|
|
3813
|
+
wait,
|
|
3814
|
+
yolo,
|
|
3815
|
+
fresh,
|
|
3816
|
+
timeoutMs,
|
|
3817
|
+
});
|
|
3601
3818
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3602
3819
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3603
3820
|
if (cmd === "output") {
|
|
@@ -3605,10 +3822,12 @@ async function main() {
|
|
|
3605
3822
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3606
3823
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3607
3824
|
}
|
|
3608
|
-
if (cmd === "send" && filteredArgs.length > 1)
|
|
3825
|
+
if (cmd === "send" && filteredArgs.length > 1)
|
|
3826
|
+
return cmdSend(session, filteredArgs.slice(1).join(" "));
|
|
3609
3827
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3610
3828
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3611
|
-
if (cmd === "select" && filteredArgs[1])
|
|
3829
|
+
if (cmd === "select" && filteredArgs[1])
|
|
3830
|
+
return cmdSelect(agent, session, filteredArgs[1], { wait, timeoutMs });
|
|
3612
3831
|
|
|
3613
3832
|
// Default: send message
|
|
3614
3833
|
let message = filteredArgs.join(" ");
|
|
@@ -3625,7 +3844,11 @@ async function main() {
|
|
|
3625
3844
|
const reviewMatch = message.match(/^please review\s*(.*)/i);
|
|
3626
3845
|
if (reviewMatch && agent.reviewOptions) {
|
|
3627
3846
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3628
|
-
return cmdReview(agent, session, "custom", customInstructions, {
|
|
3847
|
+
return cmdReview(agent, session, "custom", customInstructions, {
|
|
3848
|
+
wait: !noWait,
|
|
3849
|
+
yolo,
|
|
3850
|
+
timeoutMs,
|
|
3851
|
+
});
|
|
3629
3852
|
}
|
|
3630
3853
|
|
|
3631
3854
|
return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
|
|
@@ -3633,13 +3856,15 @@ async function main() {
|
|
|
3633
3856
|
|
|
3634
3857
|
// Run main() only when executed directly (not when imported for testing)
|
|
3635
3858
|
// Use realpathSync to handle symlinks (e.g., axclaude, axcodex bin entries)
|
|
3636
|
-
const isDirectRun =
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3859
|
+
const isDirectRun =
|
|
3860
|
+
process.argv[1] &&
|
|
3861
|
+
(() => {
|
|
3862
|
+
try {
|
|
3863
|
+
return realpathSync(process.argv[1]) === __filename;
|
|
3864
|
+
} catch {
|
|
3865
|
+
return false;
|
|
3866
|
+
}
|
|
3867
|
+
})();
|
|
3643
3868
|
if (isDirectRun) {
|
|
3644
3869
|
main().catch((err) => {
|
|
3645
3870
|
console.log(`ERROR: ${err.message}`);
|
|
@@ -3663,4 +3888,3 @@ export {
|
|
|
3663
3888
|
detectState,
|
|
3664
3889
|
State,
|
|
3665
3890
|
};
|
|
3666
|
-
|