ax-agents 0.1.0 → 0.1.2
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 +124 -20
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -131,7 +131,7 @@ const VERSION = packageJson.version;
|
|
|
131
131
|
/**
|
|
132
132
|
* @typedef {{matcher: string, hooks: Array<{type: string, command: string, timeout?: number}>}} ClaudeHookEntry
|
|
133
133
|
* @typedef {Object} ClaudeSettings
|
|
134
|
-
* @property {{UserPromptSubmit?: ClaudeHookEntry[],
|
|
134
|
+
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PreToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
135
135
|
*/
|
|
136
136
|
|
|
137
137
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
@@ -977,11 +977,12 @@ function tailJsonl(logPath, fromOffset) {
|
|
|
977
977
|
*/
|
|
978
978
|
|
|
979
979
|
/**
|
|
980
|
-
* Format a JSONL entry for streaming display.
|
|
980
|
+
* Format a Claude Code JSONL log entry for streaming display.
|
|
981
|
+
* Claude format: {type: "assistant", message: {content: [...]}}
|
|
981
982
|
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
982
983
|
* @returns {string | null}
|
|
983
984
|
*/
|
|
984
|
-
function
|
|
985
|
+
function formatClaudeLogEntry(entry) {
|
|
985
986
|
// Skip tool_result entries (they can be very verbose)
|
|
986
987
|
if (entry.type === "tool_result") return null;
|
|
987
988
|
|
|
@@ -1012,6 +1013,59 @@ function formatEntry(entry) {
|
|
|
1012
1013
|
return output.length > 0 ? output.join("\n") : null;
|
|
1013
1014
|
}
|
|
1014
1015
|
|
|
1016
|
+
/**
|
|
1017
|
+
* Format a Codex JSONL log entry for streaming display.
|
|
1018
|
+
* Codex format:
|
|
1019
|
+
* - {type: "response_item", payload: {type: "message", role: "assistant", content: [{type: "output_text", text: "..."}]}}
|
|
1020
|
+
* - {type: "response_item", payload: {type: "function_call", name: "...", arguments: "{...}"}}
|
|
1021
|
+
* - {type: "event_msg", payload: {type: "agent_message", message: "..."}}
|
|
1022
|
+
* @param {{type?: string, payload?: {type?: string, role?: string, name?: string, arguments?: string, message?: string, content?: Array<{type?: string, text?: string}>}}} entry
|
|
1023
|
+
* @returns {string | null}
|
|
1024
|
+
*/
|
|
1025
|
+
function formatCodexLogEntry(entry) {
|
|
1026
|
+
// Skip function_call_output entries (equivalent to tool_result - can be verbose)
|
|
1027
|
+
if (entry.type === "response_item" && entry.payload?.type === "function_call_output") {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Handle function calls
|
|
1032
|
+
if (entry.type === "response_item" && entry.payload?.type === "function_call") {
|
|
1033
|
+
const name = entry.payload.name || "tool";
|
|
1034
|
+
let summary = "";
|
|
1035
|
+
try {
|
|
1036
|
+
const args = JSON.parse(entry.payload.arguments || "{}");
|
|
1037
|
+
if (name === "shell_command" && args.command) {
|
|
1038
|
+
summary = args.command.slice(0, 50);
|
|
1039
|
+
} else {
|
|
1040
|
+
const target = args.file_path || args.path || args.pattern || "";
|
|
1041
|
+
summary = target.split("/").pop() || target.slice(0, 30);
|
|
1042
|
+
}
|
|
1043
|
+
} catch {
|
|
1044
|
+
summary = "...";
|
|
1045
|
+
}
|
|
1046
|
+
return `> ${name}(${summary})`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Handle assistant messages (final response)
|
|
1050
|
+
if (entry.type === "response_item" && entry.payload?.role === "assistant") {
|
|
1051
|
+
const parts = entry.payload.content || [];
|
|
1052
|
+
const output = [];
|
|
1053
|
+
for (const part of parts) {
|
|
1054
|
+
if ((part.type === "output_text" || part.type === "text") && part.text) {
|
|
1055
|
+
output.push(part.text);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return output.length > 0 ? output.join("\n") : null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Handle streaming agent messages
|
|
1062
|
+
if (entry.type === "event_msg" && entry.payload?.type === "agent_message") {
|
|
1063
|
+
return entry.payload.message || null;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1015
1069
|
/**
|
|
1016
1070
|
* Extract pending tool from confirmation screen.
|
|
1017
1071
|
* @param {string} screen
|
|
@@ -2027,19 +2081,21 @@ function detectState(screen, config) {
|
|
|
2027
2081
|
}
|
|
2028
2082
|
}
|
|
2029
2083
|
|
|
2084
|
+
// Check for active work patterns first (agent shows prompt even while working)
|
|
2085
|
+
const activeWorkPatterns = config.activeWorkPatterns || [];
|
|
2086
|
+
if (
|
|
2087
|
+
activeWorkPatterns.some((p) => {
|
|
2088
|
+
if (p instanceof RegExp) return p.test(lastLines);
|
|
2089
|
+
return lastLines.includes(p);
|
|
2090
|
+
})
|
|
2091
|
+
) {
|
|
2092
|
+
return State.THINKING;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2030
2095
|
// Ready - check BEFORE thinking to avoid false positives from timing messages like "✻ Worked for 45s"
|
|
2031
2096
|
// If the prompt symbol is visible, the agent is ready regardless of spinner characters in timing messages
|
|
2032
2097
|
if (lastLines.includes(config.promptSymbol)) {
|
|
2033
|
-
|
|
2034
|
-
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
2035
|
-
const linesArray = lastLines.split("\n");
|
|
2036
|
-
const promptWithPaste = linesArray.some(
|
|
2037
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
2038
|
-
);
|
|
2039
|
-
if (!promptWithPaste) {
|
|
2040
|
-
return State.READY;
|
|
2041
|
-
}
|
|
2042
|
-
// If prompt has pasted content, Claude is still processing - not ready yet
|
|
2098
|
+
return State.READY;
|
|
2043
2099
|
}
|
|
2044
2100
|
|
|
2045
2101
|
// Thinking - spinners (check last lines only)
|
|
@@ -2094,6 +2150,7 @@ function detectState(screen, config) {
|
|
|
2094
2150
|
* @property {string[]} [spinners]
|
|
2095
2151
|
* @property {RegExp} [rateLimitPattern]
|
|
2096
2152
|
* @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
|
|
2153
|
+
* @property {(string | RegExp)[]} [activeWorkPatterns]
|
|
2097
2154
|
* @property {ConfirmPattern[]} [confirmPatterns]
|
|
2098
2155
|
* @property {UpdatePromptPatterns | null} [updatePromptPatterns]
|
|
2099
2156
|
* @property {string[]} [responseMarkers]
|
|
@@ -2105,6 +2162,7 @@ function detectState(screen, config) {
|
|
|
2105
2162
|
* @property {string} [safeAllowedTools]
|
|
2106
2163
|
* @property {string | null} [sessionIdFlag]
|
|
2107
2164
|
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
2165
|
+
* @property {((entry: object) => string | null) | null} [logEntryFormatter]
|
|
2108
2166
|
*/
|
|
2109
2167
|
|
|
2110
2168
|
class Agent {
|
|
@@ -2128,6 +2186,8 @@ class Agent {
|
|
|
2128
2186
|
this.rateLimitPattern = config.rateLimitPattern;
|
|
2129
2187
|
/** @type {(string | RegExp | ((lines: string) => boolean))[]} */
|
|
2130
2188
|
this.thinkingPatterns = config.thinkingPatterns || [];
|
|
2189
|
+
/** @type {(string | RegExp)[]} */
|
|
2190
|
+
this.activeWorkPatterns = config.activeWorkPatterns || [];
|
|
2131
2191
|
/** @type {ConfirmPattern[]} */
|
|
2132
2192
|
this.confirmPatterns = config.confirmPatterns || [];
|
|
2133
2193
|
/** @type {UpdatePromptPatterns | null} */
|
|
@@ -2150,6 +2210,8 @@ class Agent {
|
|
|
2150
2210
|
this.sessionIdFlag = config.sessionIdFlag || null;
|
|
2151
2211
|
/** @type {((sessionName: string) => string | null) | null} */
|
|
2152
2212
|
this.logPathFinder = config.logPathFinder || null;
|
|
2213
|
+
/** @type {((entry: object) => string | null) | null} */
|
|
2214
|
+
this.logEntryFormatter = config.logEntryFormatter || null;
|
|
2153
2215
|
}
|
|
2154
2216
|
|
|
2155
2217
|
/**
|
|
@@ -2289,6 +2351,18 @@ class Agent {
|
|
|
2289
2351
|
return null;
|
|
2290
2352
|
}
|
|
2291
2353
|
|
|
2354
|
+
/**
|
|
2355
|
+
* Format a log entry for streaming display.
|
|
2356
|
+
* @param {object} entry
|
|
2357
|
+
* @returns {string | null}
|
|
2358
|
+
*/
|
|
2359
|
+
formatLogEntry(entry) {
|
|
2360
|
+
if (this.logEntryFormatter) {
|
|
2361
|
+
return this.logEntryFormatter(entry);
|
|
2362
|
+
}
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2292
2366
|
/**
|
|
2293
2367
|
* @param {string} screen
|
|
2294
2368
|
* @returns {string}
|
|
@@ -2299,6 +2373,7 @@ class Agent {
|
|
|
2299
2373
|
spinners: this.spinners,
|
|
2300
2374
|
rateLimitPattern: this.rateLimitPattern,
|
|
2301
2375
|
thinkingPatterns: this.thinkingPatterns,
|
|
2376
|
+
activeWorkPatterns: this.activeWorkPatterns,
|
|
2302
2377
|
confirmPatterns: this.confirmPatterns,
|
|
2303
2378
|
updatePromptPatterns: this.updatePromptPatterns,
|
|
2304
2379
|
});
|
|
@@ -2528,11 +2603,13 @@ const CodexAgent = new Agent({
|
|
|
2528
2603
|
screen: ["Update available"],
|
|
2529
2604
|
lastLines: ["Skip"],
|
|
2530
2605
|
},
|
|
2606
|
+
activeWorkPatterns: ["esc to interrupt"],
|
|
2531
2607
|
responseMarkers: ["•", "- ", "**"],
|
|
2532
2608
|
chromePatterns: ["context left", "for shortcuts"],
|
|
2533
2609
|
reviewOptions: { pr: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
2534
2610
|
envVar: "AX_SESSION",
|
|
2535
2611
|
logPathFinder: findCodexLogPath,
|
|
2612
|
+
logEntryFormatter: formatCodexLogEntry,
|
|
2536
2613
|
});
|
|
2537
2614
|
|
|
2538
2615
|
// =============================================================================
|
|
@@ -2550,6 +2627,7 @@ const ClaudeAgent = new Agent({
|
|
|
2550
2627
|
rateLimitPattern: /rate.?limit/i,
|
|
2551
2628
|
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2552
2629
|
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
2630
|
+
activeWorkPatterns: ["[Pasted text"],
|
|
2553
2631
|
confirmPatterns: [
|
|
2554
2632
|
"Do you want to make this edit",
|
|
2555
2633
|
"Do you want to run this command",
|
|
@@ -2581,6 +2659,7 @@ const ClaudeAgent = new Agent({
|
|
|
2581
2659
|
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2582
2660
|
return null;
|
|
2583
2661
|
},
|
|
2662
|
+
logEntryFormatter: formatClaudeLogEntry,
|
|
2584
2663
|
});
|
|
2585
2664
|
|
|
2586
2665
|
// =============================================================================
|
|
@@ -2718,20 +2797,33 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2718
2797
|
let logPath = agent.findLogPath(session);
|
|
2719
2798
|
let logOffset = logPath && existsSync(logPath) ? statSync(logPath).size : 0;
|
|
2720
2799
|
let printedThinking = false;
|
|
2800
|
+
/** @type {string | null} Track last assistant message to dedupe final response */
|
|
2801
|
+
let lastAssistantMessage = null;
|
|
2721
2802
|
|
|
2722
2803
|
const streamNewEntries = () => {
|
|
2723
2804
|
if (!logPath) {
|
|
2724
2805
|
logPath = agent.findLogPath(session);
|
|
2725
2806
|
if (logPath && existsSync(logPath)) {
|
|
2726
|
-
|
|
2807
|
+
// Read from beginning when file is first discovered
|
|
2808
|
+
// (Claude creates log file when first message is sent)
|
|
2809
|
+
logOffset = 0;
|
|
2727
2810
|
}
|
|
2728
2811
|
}
|
|
2729
2812
|
if (logPath) {
|
|
2730
2813
|
const { entries, newOffset } = tailJsonl(logPath, logOffset);
|
|
2731
2814
|
logOffset = newOffset;
|
|
2732
2815
|
for (const entry of entries) {
|
|
2733
|
-
const formatted =
|
|
2734
|
-
if (formatted)
|
|
2816
|
+
const formatted = agent.formatLogEntry(entry);
|
|
2817
|
+
if (!formatted) continue;
|
|
2818
|
+
|
|
2819
|
+
// Dedupe assistant messages (streaming agent_message vs final response_item)
|
|
2820
|
+
// Tool calls (starting with ">") are always printed
|
|
2821
|
+
if (!formatted.startsWith(">")) {
|
|
2822
|
+
if (formatted === lastAssistantMessage) continue;
|
|
2823
|
+
lastAssistantMessage = formatted;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
console.log(formatted);
|
|
2735
2827
|
}
|
|
2736
2828
|
}
|
|
2737
2829
|
};
|
|
@@ -3420,7 +3512,7 @@ async function cmdRecall(name = null) {
|
|
|
3420
3512
|
}
|
|
3421
3513
|
|
|
3422
3514
|
// Version of the hook script template - bump when making changes
|
|
3423
|
-
const HOOK_SCRIPT_VERSION = "
|
|
3515
|
+
const HOOK_SCRIPT_VERSION = "5";
|
|
3424
3516
|
|
|
3425
3517
|
function ensureMailboxHookScript() {
|
|
3426
3518
|
const hooksDir = HOOKS_DIR;
|
|
@@ -3539,8 +3631,16 @@ if (relevant.length > 0) {
|
|
|
3539
3631
|
// For Stop hook, return blocking JSON to force acknowledgment
|
|
3540
3632
|
if (hookEvent === "Stop") {
|
|
3541
3633
|
console.log(JSON.stringify({ decision: "block", reason: formattedMessage }));
|
|
3634
|
+
} else if (hookEvent === "PreToolUse") {
|
|
3635
|
+
// For PreToolUse, use JSON with hookSpecificOutput to inject into Claude's context
|
|
3636
|
+
console.log(JSON.stringify({
|
|
3637
|
+
hookSpecificOutput: {
|
|
3638
|
+
hookEventName: "PreToolUse",
|
|
3639
|
+
additionalContext: formattedMessage
|
|
3640
|
+
}
|
|
3641
|
+
}));
|
|
3542
3642
|
} else {
|
|
3543
|
-
// For
|
|
3643
|
+
// For UserPromptSubmit, plain text stdout is automatically added to context
|
|
3544
3644
|
console.log(formattedMessage);
|
|
3545
3645
|
}
|
|
3546
3646
|
|
|
@@ -3566,7 +3666,7 @@ process.exit(0);
|
|
|
3566
3666
|
console.log(`{
|
|
3567
3667
|
"hooks": {
|
|
3568
3668
|
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3569
|
-
"
|
|
3669
|
+
"PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3570
3670
|
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }]
|
|
3571
3671
|
}
|
|
3572
3672
|
}`);
|
|
@@ -3577,7 +3677,7 @@ function ensureClaudeHookConfig() {
|
|
|
3577
3677
|
const settingsDir = ".claude";
|
|
3578
3678
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
3579
3679
|
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
3580
|
-
const hookEvents = ["UserPromptSubmit", "
|
|
3680
|
+
const hookEvents = ["UserPromptSubmit", "PreToolUse", "Stop"];
|
|
3581
3681
|
|
|
3582
3682
|
try {
|
|
3583
3683
|
/** @type {ClaudeSettings} */
|
|
@@ -5031,4 +5131,8 @@ export {
|
|
|
5031
5131
|
State,
|
|
5032
5132
|
normalizeAllowedTools,
|
|
5033
5133
|
computePermissionHash,
|
|
5134
|
+
formatClaudeLogEntry,
|
|
5135
|
+
formatCodexLogEntry,
|
|
5136
|
+
CodexAgent,
|
|
5137
|
+
ClaudeAgent,
|
|
5034
5138
|
};
|