ax-agents 0.1.3 → 0.1.4
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 +764 -51
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -134,6 +134,71 @@ const VERSION = packageJson.version;
|
|
|
134
134
|
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PreToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
135
135
|
*/
|
|
136
136
|
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Terminal Stream Types - Abstraction layer for terminal I/O
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Style properties for terminal text (ANSI colors, formatting)
|
|
143
|
+
* @typedef {Object} TerminalStyle
|
|
144
|
+
* @property {string} [fg] - Foreground color (e.g., "red", "green", "#ff0000")
|
|
145
|
+
* @property {string} [bg] - Background color
|
|
146
|
+
* @property {boolean} [bold] - Bold text
|
|
147
|
+
* @property {boolean} [dim] - Dimmed text
|
|
148
|
+
* @property {boolean} [italic] - Italic text
|
|
149
|
+
* @property {boolean} [underline] - Underlined text
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A span of text with optional styling
|
|
154
|
+
* @typedef {Object} TextSpan
|
|
155
|
+
* @property {string} text - The text content
|
|
156
|
+
* @property {TerminalStyle} [style] - Optional style properties
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A line of terminal output, containing styled spans and raw text
|
|
161
|
+
* @typedef {Object} TerminalLine
|
|
162
|
+
* @property {TextSpan[]} spans - Styled text spans
|
|
163
|
+
* @property {string} raw - Raw text content (spans joined, styles stripped)
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Query for matching terminal lines
|
|
168
|
+
* @typedef {Object} MatchQuery
|
|
169
|
+
* @property {string | RegExp} pattern - Pattern to match against raw line text
|
|
170
|
+
* @property {Partial<TerminalStyle>} [style] - Optional style filter (ignored if implementation doesn't support styles)
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Result of a pattern match operation
|
|
175
|
+
* @typedef {Object} MatchResult
|
|
176
|
+
* @property {boolean} matched - Whether a match was found
|
|
177
|
+
* @property {TerminalLine} [line] - The matched line (if matched)
|
|
178
|
+
* @property {number} [lineIndex] - Index of the matched line (if matched)
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Options for reading from a terminal stream
|
|
183
|
+
* @typedef {Object} ReadOptions
|
|
184
|
+
* @property {number} [max] - Maximum number of lines to return
|
|
185
|
+
* @property {number} [timeoutMs] - Timeout in milliseconds
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Options for waiting for a match
|
|
190
|
+
* @typedef {Object} WaitOptions
|
|
191
|
+
* @property {number} [timeoutMs] - Timeout in milliseconds
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Interface for reading terminal output.
|
|
196
|
+
* Implementations: JsonlTerminalStream (Claude logs), ScreenTerminalStream (tmux capture)
|
|
197
|
+
* @typedef {Object} TerminalStream
|
|
198
|
+
* @property {(opts?: ReadOptions) => Promise<TerminalLine[]>} readNext - Read new lines since last read
|
|
199
|
+
* @property {(query: MatchQuery, opts?: WaitOptions) => Promise<MatchResult>} waitForMatch - Wait for a line matching the query
|
|
200
|
+
*/
|
|
201
|
+
|
|
137
202
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
138
203
|
|
|
139
204
|
/**
|
|
@@ -205,11 +270,13 @@ function tmuxHasSession(session) {
|
|
|
205
270
|
/**
|
|
206
271
|
* @param {string} session
|
|
207
272
|
* @param {number} [scrollback]
|
|
273
|
+
* @param {boolean} [withEscapes] - Include ANSI escape sequences (uses -e flag)
|
|
208
274
|
* @returns {string}
|
|
209
275
|
*/
|
|
210
|
-
function tmuxCapture(session, scrollback = 0) {
|
|
276
|
+
function tmuxCapture(session, scrollback = 0, withEscapes = false) {
|
|
211
277
|
try {
|
|
212
278
|
const args = ["capture-pane", "-t", session, "-p"];
|
|
279
|
+
if (withEscapes) args.push("-e"); // Include escape sequences
|
|
213
280
|
if (scrollback) args.push("-S", String(-scrollback));
|
|
214
281
|
return tmux(args);
|
|
215
282
|
} catch (err) {
|
|
@@ -236,6 +303,41 @@ function tmuxSendLiteral(session, text) {
|
|
|
236
303
|
tmux(["send-keys", "-t", session, "-l", text]);
|
|
237
304
|
}
|
|
238
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Paste text into a tmux session using load-buffer + paste-buffer.
|
|
308
|
+
* More reliable than send-keys -l for large text.
|
|
309
|
+
* Uses a named buffer to avoid races with concurrent invocations.
|
|
310
|
+
* @param {string} session
|
|
311
|
+
* @param {string} text
|
|
312
|
+
*/
|
|
313
|
+
function tmuxPasteLiteral(session, text) {
|
|
314
|
+
debug("tmux", `pasteLiteral session=${session}, text=${text.slice(0, 50)}...`);
|
|
315
|
+
// Use unique buffer name per invocation to avoid races (even to same session)
|
|
316
|
+
const bufferName = `ax-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
317
|
+
// Load text into named tmux buffer from stdin
|
|
318
|
+
const loadResult = spawnSync("tmux", ["load-buffer", "-b", bufferName, "-"], {
|
|
319
|
+
input: text,
|
|
320
|
+
encoding: "utf-8",
|
|
321
|
+
});
|
|
322
|
+
if (loadResult.status !== 0) {
|
|
323
|
+
debug("tmux", `load-buffer failed: ${loadResult.stderr}`);
|
|
324
|
+
throw new Error(loadResult.stderr || "tmux load-buffer failed");
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
// Paste buffer into the session
|
|
328
|
+
tmux(["paste-buffer", "-b", bufferName, "-t", session]);
|
|
329
|
+
// Ensure cursor is at end of pasted text
|
|
330
|
+
tmux(["send-keys", "-t", session, "End"]);
|
|
331
|
+
} finally {
|
|
332
|
+
// Clean up the named buffer
|
|
333
|
+
try {
|
|
334
|
+
tmux(["delete-buffer", "-b", bufferName]);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
debugError("tmuxPasteLiteral", err);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
239
341
|
/**
|
|
240
342
|
* @param {string} session
|
|
241
343
|
*/
|
|
@@ -837,7 +939,10 @@ function findCodexLogPath(sessionName) {
|
|
|
837
939
|
}
|
|
838
940
|
// Return the closest match
|
|
839
941
|
candidates.sort((a, b) => a.diff - b.diff);
|
|
840
|
-
debug(
|
|
942
|
+
debug(
|
|
943
|
+
"log",
|
|
944
|
+
`findCodexLogPath: found ${candidates.length} candidates, best: ${candidates[0].path}`,
|
|
945
|
+
);
|
|
841
946
|
return candidates[0].path;
|
|
842
947
|
} catch {
|
|
843
948
|
debug("log", `findCodexLogPath: exception caught`);
|
|
@@ -1026,7 +1131,7 @@ function tailJsonl(logPath, fromOffset) {
|
|
|
1026
1131
|
/**
|
|
1027
1132
|
* Format a Claude Code JSONL log entry for streaming display.
|
|
1028
1133
|
* Claude format: {type: "assistant", message: {content: [...]}}
|
|
1029
|
-
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
1134
|
+
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, thinking?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
1030
1135
|
* @returns {string | null}
|
|
1031
1136
|
*/
|
|
1032
1137
|
function formatClaudeLogEntry(entry) {
|
|
@@ -1068,7 +1173,8 @@ function formatClaudeLogEntry(entry) {
|
|
|
1068
1173
|
* - {type: "response_item", payload: {type: "message", role: "assistant", content: [{type: "output_text", text: "..."}]}}
|
|
1069
1174
|
* - {type: "response_item", payload: {type: "function_call", name: "...", arguments: "{...}"}}
|
|
1070
1175
|
* - {type: "event_msg", payload: {type: "agent_message", message: "..."}}
|
|
1071
|
-
*
|
|
1176
|
+
* - {type: "event_msg", payload: {type: "agent_reasoning", text: "..."}}
|
|
1177
|
+
* @param {{type?: string, payload?: {type?: string, role?: string, name?: string, arguments?: string, message?: string, text?: string, content?: Array<{type?: string, text?: string}>}}} entry
|
|
1072
1178
|
* @returns {string | null}
|
|
1073
1179
|
*/
|
|
1074
1180
|
function formatCodexLogEntry(entry) {
|
|
@@ -1120,6 +1226,587 @@ function formatCodexLogEntry(entry) {
|
|
|
1120
1226
|
return null;
|
|
1121
1227
|
}
|
|
1122
1228
|
|
|
1229
|
+
// =============================================================================
|
|
1230
|
+
// Terminal Stream Primitives - Pure functions for parsing terminal data
|
|
1231
|
+
// =============================================================================
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Parse a JSONL log entry into TerminalLine[].
|
|
1235
|
+
* Wraps formatClaudeLogEntry/formatCodexLogEntry to return structured data.
|
|
1236
|
+
* @param {object} entry - A parsed JSONL entry
|
|
1237
|
+
* @param {'claude' | 'codex'} format - The log format
|
|
1238
|
+
* @returns {TerminalLine[]}
|
|
1239
|
+
*/
|
|
1240
|
+
function parseJsonlEntry(entry, format) {
|
|
1241
|
+
const formatted = format === "claude" ? formatClaudeLogEntry(entry) : formatCodexLogEntry(entry);
|
|
1242
|
+
if (!formatted) return [];
|
|
1243
|
+
|
|
1244
|
+
// Split on newlines and create a TerminalLine for each
|
|
1245
|
+
return formatted.split("\n").map((line) => ({
|
|
1246
|
+
spans: [{ text: line }],
|
|
1247
|
+
raw: line,
|
|
1248
|
+
}));
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Parse raw screen output into TerminalLine[].
|
|
1253
|
+
* Each line becomes a TerminalLine with a single unstyled span.
|
|
1254
|
+
* @param {string} screen - Raw screen content from tmux capture
|
|
1255
|
+
* @returns {TerminalLine[]}
|
|
1256
|
+
*/
|
|
1257
|
+
function parseScreenLines(screen) {
|
|
1258
|
+
if (!screen) return [];
|
|
1259
|
+
return screen.split("\n").map((line) => ({
|
|
1260
|
+
spans: [{ text: line }],
|
|
1261
|
+
raw: line,
|
|
1262
|
+
}));
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* ANSI color code to color name mapping.
|
|
1267
|
+
* @type {Record<string, string>}
|
|
1268
|
+
*/
|
|
1269
|
+
const ANSI_COLORS = {
|
|
1270
|
+
30: "black",
|
|
1271
|
+
31: "red",
|
|
1272
|
+
32: "green",
|
|
1273
|
+
33: "yellow",
|
|
1274
|
+
34: "blue",
|
|
1275
|
+
35: "magenta",
|
|
1276
|
+
36: "cyan",
|
|
1277
|
+
37: "white",
|
|
1278
|
+
90: "bright-black",
|
|
1279
|
+
91: "bright-red",
|
|
1280
|
+
92: "bright-green",
|
|
1281
|
+
93: "bright-yellow",
|
|
1282
|
+
94: "bright-blue",
|
|
1283
|
+
95: "bright-magenta",
|
|
1284
|
+
96: "bright-cyan",
|
|
1285
|
+
97: "bright-white",
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* ANSI background color code to color name mapping.
|
|
1290
|
+
* @type {Record<string, string>}
|
|
1291
|
+
*/
|
|
1292
|
+
const ANSI_BG_COLORS = {
|
|
1293
|
+
40: "black",
|
|
1294
|
+
41: "red",
|
|
1295
|
+
42: "green",
|
|
1296
|
+
43: "yellow",
|
|
1297
|
+
44: "blue",
|
|
1298
|
+
45: "magenta",
|
|
1299
|
+
46: "cyan",
|
|
1300
|
+
47: "white",
|
|
1301
|
+
100: "bright-black",
|
|
1302
|
+
101: "bright-red",
|
|
1303
|
+
102: "bright-green",
|
|
1304
|
+
103: "bright-yellow",
|
|
1305
|
+
104: "bright-blue",
|
|
1306
|
+
105: "bright-magenta",
|
|
1307
|
+
106: "bright-cyan",
|
|
1308
|
+
107: "bright-white",
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Parse ANSI escape sequences from a line of text into styled spans.
|
|
1313
|
+
* @param {string} line - Line containing ANSI escape sequences
|
|
1314
|
+
* @returns {TextSpan[]}
|
|
1315
|
+
*/
|
|
1316
|
+
function parseAnsiLine(line) {
|
|
1317
|
+
if (!line) return [{ text: "" }];
|
|
1318
|
+
|
|
1319
|
+
const spans = [];
|
|
1320
|
+
/** @type {TerminalStyle} */
|
|
1321
|
+
let currentStyle = {};
|
|
1322
|
+
let currentText = "";
|
|
1323
|
+
|
|
1324
|
+
// ANSI escape sequence pattern: ESC [ <params> m
|
|
1325
|
+
// Matches sequences like \x1b[31m (red), \x1b[1;31m (bold red), \x1b[0m (reset)
|
|
1326
|
+
// eslint-disable-next-line no-control-regex
|
|
1327
|
+
const ansiPattern = /\x1b\[([0-9;]*)m/g;
|
|
1328
|
+
let lastIndex = 0;
|
|
1329
|
+
let match;
|
|
1330
|
+
|
|
1331
|
+
while ((match = ansiPattern.exec(line)) !== null) {
|
|
1332
|
+
// Add text before this escape sequence
|
|
1333
|
+
const textBefore = line.slice(lastIndex, match.index);
|
|
1334
|
+
if (textBefore) {
|
|
1335
|
+
currentText += textBefore;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Flush current span if we have text
|
|
1339
|
+
if (currentText) {
|
|
1340
|
+
/** @type {TextSpan} */
|
|
1341
|
+
const span = { text: currentText };
|
|
1342
|
+
if (Object.keys(currentStyle).length > 0) {
|
|
1343
|
+
span.style = { ...currentStyle };
|
|
1344
|
+
}
|
|
1345
|
+
spans.push(span);
|
|
1346
|
+
currentText = "";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Parse SGR (Select Graphic Rendition) parameters
|
|
1350
|
+
// Note: \x1b[m (empty params) is equivalent to \x1b[0m (reset)
|
|
1351
|
+
const params = match[1].split(";").filter(Boolean);
|
|
1352
|
+
if (params.length === 0) {
|
|
1353
|
+
// Empty params means reset (e.g., \x1b[m)
|
|
1354
|
+
currentStyle = {};
|
|
1355
|
+
}
|
|
1356
|
+
for (const param of params) {
|
|
1357
|
+
const code = param;
|
|
1358
|
+
if (code === "0") {
|
|
1359
|
+
// Reset
|
|
1360
|
+
currentStyle = {};
|
|
1361
|
+
} else if (code === "1") {
|
|
1362
|
+
currentStyle.bold = true;
|
|
1363
|
+
} else if (code === "2") {
|
|
1364
|
+
currentStyle.dim = true;
|
|
1365
|
+
} else if (code === "3") {
|
|
1366
|
+
currentStyle.italic = true;
|
|
1367
|
+
} else if (code === "4") {
|
|
1368
|
+
currentStyle.underline = true;
|
|
1369
|
+
} else if (code === "22") {
|
|
1370
|
+
// Normal intensity (neither bold nor dim)
|
|
1371
|
+
delete currentStyle.bold;
|
|
1372
|
+
delete currentStyle.dim;
|
|
1373
|
+
} else if (code === "23") {
|
|
1374
|
+
delete currentStyle.italic;
|
|
1375
|
+
} else if (code === "24") {
|
|
1376
|
+
delete currentStyle.underline;
|
|
1377
|
+
} else if (ANSI_COLORS[code]) {
|
|
1378
|
+
currentStyle.fg = ANSI_COLORS[code];
|
|
1379
|
+
} else if (ANSI_BG_COLORS[code]) {
|
|
1380
|
+
currentStyle.bg = ANSI_BG_COLORS[code];
|
|
1381
|
+
} else if (code === "39") {
|
|
1382
|
+
// Default foreground
|
|
1383
|
+
delete currentStyle.fg;
|
|
1384
|
+
} else if (code === "49") {
|
|
1385
|
+
// Default background
|
|
1386
|
+
delete currentStyle.bg;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
lastIndex = ansiPattern.lastIndex;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Add remaining text
|
|
1394
|
+
const remaining = line.slice(lastIndex);
|
|
1395
|
+
if (remaining) {
|
|
1396
|
+
currentText += remaining;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Flush final span
|
|
1400
|
+
if (currentText || spans.length === 0) {
|
|
1401
|
+
/** @type {TextSpan} */
|
|
1402
|
+
const span = { text: currentText };
|
|
1403
|
+
if (Object.keys(currentStyle).length > 0) {
|
|
1404
|
+
span.style = { ...currentStyle };
|
|
1405
|
+
}
|
|
1406
|
+
spans.push(span);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
return spans;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Parse raw screen output with ANSI codes into styled TerminalLine[].
|
|
1414
|
+
* @param {string} screen - Screen content with ANSI escape codes
|
|
1415
|
+
* @returns {TerminalLine[]}
|
|
1416
|
+
*/
|
|
1417
|
+
function parseStyledScreenLines(screen) {
|
|
1418
|
+
if (!screen) return [];
|
|
1419
|
+
return screen.split("\n").map((line) => {
|
|
1420
|
+
const spans = parseAnsiLine(line);
|
|
1421
|
+
// Raw text is spans joined without styles
|
|
1422
|
+
const raw = spans.map((s) => s.text).join("");
|
|
1423
|
+
return { spans, raw };
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Find a line matching the given query.
|
|
1429
|
+
* Style filters are ignored when lines don't have style information.
|
|
1430
|
+
* @param {TerminalLine[]} lines - Lines to search
|
|
1431
|
+
* @param {MatchQuery} query - Query with pattern and optional style filter
|
|
1432
|
+
* @returns {MatchResult}
|
|
1433
|
+
*/
|
|
1434
|
+
function findMatch(lines, query) {
|
|
1435
|
+
const { pattern, style } = query;
|
|
1436
|
+
|
|
1437
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1438
|
+
const line = lines[i];
|
|
1439
|
+
const text = line.raw;
|
|
1440
|
+
|
|
1441
|
+
// Check pattern match
|
|
1442
|
+
const patternMatches =
|
|
1443
|
+
typeof pattern === "string" ? text.includes(pattern) : pattern.test(text);
|
|
1444
|
+
|
|
1445
|
+
if (!patternMatches) continue;
|
|
1446
|
+
|
|
1447
|
+
// If no style filter requested, we have a match
|
|
1448
|
+
if (!style) {
|
|
1449
|
+
return { matched: true, line, lineIndex: i };
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Check style match (if line has styled spans)
|
|
1453
|
+
// Style filter is silently ignored if implementation doesn't provide styles
|
|
1454
|
+
const hasStyledSpans = line.spans.some((span) => span.style);
|
|
1455
|
+
if (!hasStyledSpans) {
|
|
1456
|
+
// No style info available - pattern match is enough
|
|
1457
|
+
return { matched: true, line, lineIndex: i };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Check if any span matches both pattern and style
|
|
1461
|
+
const styleMatches = line.spans.some((span) => {
|
|
1462
|
+
if (!span.style) return false;
|
|
1463
|
+
const spanMatchesPattern =
|
|
1464
|
+
typeof pattern === "string" ? span.text.includes(pattern) : pattern.test(span.text);
|
|
1465
|
+
if (!spanMatchesPattern) return false;
|
|
1466
|
+
|
|
1467
|
+
// Check each requested style property
|
|
1468
|
+
const spanStyle = /** @type {Record<string, unknown>} */ (span.style);
|
|
1469
|
+
for (const [key, value] of Object.entries(style)) {
|
|
1470
|
+
if (spanStyle[key] !== value) return false;
|
|
1471
|
+
}
|
|
1472
|
+
return true;
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
if (styleMatches) {
|
|
1476
|
+
return { matched: true, line, lineIndex: i };
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return { matched: false };
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// =============================================================================
|
|
1484
|
+
// Terminal Stream Implementations
|
|
1485
|
+
// =============================================================================
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Terminal stream that reads from JSONL log files (Claude/Codex logs).
|
|
1489
|
+
* Implements TerminalStream interface.
|
|
1490
|
+
* @implements {TerminalStream}
|
|
1491
|
+
*/
|
|
1492
|
+
class JsonlTerminalStream {
|
|
1493
|
+
/** @type {() => string | null} */
|
|
1494
|
+
logPathFinder;
|
|
1495
|
+
/** @type {'claude' | 'codex'} */
|
|
1496
|
+
format;
|
|
1497
|
+
/** @type {string | null} */
|
|
1498
|
+
logPath;
|
|
1499
|
+
/** @type {number} */
|
|
1500
|
+
offset;
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* @param {() => string | null} logPathFinder - Function that returns current log path (may change during session)
|
|
1504
|
+
* @param {'claude' | 'codex'} format - Log format for parsing entries
|
|
1505
|
+
*/
|
|
1506
|
+
constructor(logPathFinder, format) {
|
|
1507
|
+
this.logPathFinder = logPathFinder;
|
|
1508
|
+
this.format = format;
|
|
1509
|
+
this.logPath = null;
|
|
1510
|
+
this.offset = 0;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Read new lines since last read.
|
|
1515
|
+
* @param {ReadOptions} [opts]
|
|
1516
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1517
|
+
*/
|
|
1518
|
+
async readNext(opts = {}) {
|
|
1519
|
+
// Check for new/changed log path
|
|
1520
|
+
const currentLogPath = this.logPathFinder();
|
|
1521
|
+
if (currentLogPath && currentLogPath !== this.logPath) {
|
|
1522
|
+
this.logPath = currentLogPath;
|
|
1523
|
+
// Read from beginning when file is first discovered or changed
|
|
1524
|
+
if (existsSync(this.logPath)) {
|
|
1525
|
+
this.offset = 0;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (!this.logPath) {
|
|
1530
|
+
return [];
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const { entries, newOffset } = tailJsonl(this.logPath, this.offset);
|
|
1534
|
+
this.offset = newOffset;
|
|
1535
|
+
|
|
1536
|
+
const lines = [];
|
|
1537
|
+
for (const entry of entries) {
|
|
1538
|
+
const entryLines = parseJsonlEntry(entry, this.format);
|
|
1539
|
+
lines.push(...entryLines);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (opts.max && lines.length > opts.max) {
|
|
1543
|
+
return lines.slice(0, opts.max);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return lines;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Wait for a line matching the query.
|
|
1551
|
+
* @param {MatchQuery} query
|
|
1552
|
+
* @param {WaitOptions} [opts]
|
|
1553
|
+
* @returns {Promise<MatchResult>}
|
|
1554
|
+
*/
|
|
1555
|
+
async waitForMatch(query, opts = {}) {
|
|
1556
|
+
const timeoutMs = opts.timeoutMs || 30000;
|
|
1557
|
+
const pollInterval = 100;
|
|
1558
|
+
const deadline = Date.now() + timeoutMs;
|
|
1559
|
+
|
|
1560
|
+
while (Date.now() < deadline) {
|
|
1561
|
+
const lines = await this.readNext();
|
|
1562
|
+
if (lines.length > 0) {
|
|
1563
|
+
const result = findMatch(lines, query);
|
|
1564
|
+
if (result.matched) {
|
|
1565
|
+
return result;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
await sleep(pollInterval);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
return { matched: false };
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* Terminal stream that reads from tmux screen capture.
|
|
1577
|
+
* Implements TerminalStream interface.
|
|
1578
|
+
* @implements {TerminalStream}
|
|
1579
|
+
*/
|
|
1580
|
+
class ScreenTerminalStream {
|
|
1581
|
+
/**
|
|
1582
|
+
* @param {string} session - tmux session name
|
|
1583
|
+
* @param {number} [scrollback] - Number of scrollback lines to capture
|
|
1584
|
+
*/
|
|
1585
|
+
constructor(session, scrollback = 0) {
|
|
1586
|
+
this.session = session;
|
|
1587
|
+
this.scrollback = scrollback;
|
|
1588
|
+
this.lastScreen = "";
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Read current screen lines (returns all visible lines on each call).
|
|
1593
|
+
* Note: Unlike JsonlTerminalStream, this returns the full screen each time.
|
|
1594
|
+
* @param {ReadOptions} [opts]
|
|
1595
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1596
|
+
*/
|
|
1597
|
+
async readNext(opts = {}) {
|
|
1598
|
+
const screen = tmuxCapture(this.session, this.scrollback);
|
|
1599
|
+
this.lastScreen = screen;
|
|
1600
|
+
|
|
1601
|
+
const lines = parseScreenLines(screen);
|
|
1602
|
+
|
|
1603
|
+
if (opts.max && lines.length > opts.max) {
|
|
1604
|
+
return lines.slice(-opts.max); // Return last N lines for screen capture
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return lines;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Wait for a line matching the query.
|
|
1612
|
+
* @param {MatchQuery} query
|
|
1613
|
+
* @param {WaitOptions} [opts]
|
|
1614
|
+
* @returns {Promise<MatchResult>}
|
|
1615
|
+
*/
|
|
1616
|
+
async waitForMatch(query, opts = {}) {
|
|
1617
|
+
const timeoutMs = opts.timeoutMs || 30000;
|
|
1618
|
+
const pollInterval = 100;
|
|
1619
|
+
const deadline = Date.now() + timeoutMs;
|
|
1620
|
+
|
|
1621
|
+
while (Date.now() < deadline) {
|
|
1622
|
+
const lines = await this.readNext();
|
|
1623
|
+
const result = findMatch(lines, query);
|
|
1624
|
+
if (result.matched) {
|
|
1625
|
+
return result;
|
|
1626
|
+
}
|
|
1627
|
+
await sleep(pollInterval);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
return { matched: false };
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Get the last captured screen (raw string).
|
|
1635
|
+
* Useful for compatibility with existing code that needs raw screen.
|
|
1636
|
+
* @returns {string}
|
|
1637
|
+
*/
|
|
1638
|
+
getLastScreen() {
|
|
1639
|
+
return this.lastScreen;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Terminal stream that reads from tmux screen capture with ANSI styling.
|
|
1645
|
+
* Uses `tmux capture-pane -e` to capture escape sequences.
|
|
1646
|
+
* Implements TerminalStream interface.
|
|
1647
|
+
* @implements {TerminalStream}
|
|
1648
|
+
*/
|
|
1649
|
+
class StyledScreenTerminalStream {
|
|
1650
|
+
/**
|
|
1651
|
+
* @param {string} session - tmux session name
|
|
1652
|
+
* @param {number} [scrollback] - Number of scrollback lines to capture
|
|
1653
|
+
*/
|
|
1654
|
+
constructor(session, scrollback = 0) {
|
|
1655
|
+
this.session = session;
|
|
1656
|
+
this.scrollback = scrollback;
|
|
1657
|
+
this.lastScreen = "";
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Read current screen lines with ANSI styling parsed.
|
|
1662
|
+
* @param {ReadOptions} [opts]
|
|
1663
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1664
|
+
*/
|
|
1665
|
+
async readNext(opts = {}) {
|
|
1666
|
+
const screen = tmuxCapture(this.session, this.scrollback, true); // withEscapes=true
|
|
1667
|
+
this.lastScreen = screen;
|
|
1668
|
+
|
|
1669
|
+
const lines = parseStyledScreenLines(screen);
|
|
1670
|
+
|
|
1671
|
+
if (opts.max && lines.length > opts.max) {
|
|
1672
|
+
return lines.slice(-opts.max); // Return last N lines for screen capture
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return lines;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Wait for a line matching the query (supports style-aware matching).
|
|
1680
|
+
* @param {MatchQuery} query
|
|
1681
|
+
* @param {WaitOptions} [opts]
|
|
1682
|
+
* @returns {Promise<MatchResult>}
|
|
1683
|
+
*/
|
|
1684
|
+
async waitForMatch(query, opts = {}) {
|
|
1685
|
+
const timeoutMs = opts.timeoutMs || 30000;
|
|
1686
|
+
const pollInterval = 100;
|
|
1687
|
+
const deadline = Date.now() + timeoutMs;
|
|
1688
|
+
|
|
1689
|
+
while (Date.now() < deadline) {
|
|
1690
|
+
const lines = await this.readNext();
|
|
1691
|
+
const result = findMatch(lines, query);
|
|
1692
|
+
if (result.matched) {
|
|
1693
|
+
return result;
|
|
1694
|
+
}
|
|
1695
|
+
await sleep(pollInterval);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
return { matched: false };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* Get the last captured screen (raw string with ANSI codes).
|
|
1703
|
+
* @returns {string}
|
|
1704
|
+
*/
|
|
1705
|
+
getLastScreen() {
|
|
1706
|
+
return this.lastScreen;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Fake terminal stream for testing.
|
|
1712
|
+
* Implements TerminalStream interface.
|
|
1713
|
+
* @implements {TerminalStream}
|
|
1714
|
+
*/
|
|
1715
|
+
class FakeTerminalStream {
|
|
1716
|
+
/**
|
|
1717
|
+
* @param {TerminalLine[]} lines - Initial lines to provide
|
|
1718
|
+
*/
|
|
1719
|
+
constructor(lines = []) {
|
|
1720
|
+
this.lines = [...lines];
|
|
1721
|
+
this.readCount = 0;
|
|
1722
|
+
/** @type {TerminalLine[][]} */
|
|
1723
|
+
this.pendingLines = [];
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* Queue lines to be returned on subsequent readNext calls.
|
|
1728
|
+
* @param {TerminalLine[]} lines
|
|
1729
|
+
*/
|
|
1730
|
+
queueLines(lines) {
|
|
1731
|
+
this.pendingLines.push(lines);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Add more lines to the current buffer (simulates new output).
|
|
1736
|
+
* @param {TerminalLine[]} lines
|
|
1737
|
+
*/
|
|
1738
|
+
addLines(lines) {
|
|
1739
|
+
this.lines.push(...lines);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Read new lines since last read.
|
|
1744
|
+
* First call returns initial lines, subsequent calls return queued lines.
|
|
1745
|
+
* @param {ReadOptions} [opts]
|
|
1746
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1747
|
+
*/
|
|
1748
|
+
async readNext(opts = {}) {
|
|
1749
|
+
this.readCount++;
|
|
1750
|
+
|
|
1751
|
+
/** @type {TerminalLine[]} */
|
|
1752
|
+
let result = [];
|
|
1753
|
+
if (this.readCount === 1) {
|
|
1754
|
+
result = this.lines;
|
|
1755
|
+
} else if (this.pendingLines.length > 0) {
|
|
1756
|
+
result = this.pendingLines.shift() || [];
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (opts.max && result.length > opts.max) {
|
|
1760
|
+
return result.slice(0, opts.max);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
return result;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
/**
|
|
1767
|
+
* Wait for a line matching the query.
|
|
1768
|
+
* Immediately checks available lines without polling.
|
|
1769
|
+
* @param {MatchQuery} query
|
|
1770
|
+
* @param {WaitOptions} [_opts]
|
|
1771
|
+
* @returns {Promise<MatchResult>}
|
|
1772
|
+
*/
|
|
1773
|
+
async waitForMatch(query, _opts = {}) {
|
|
1774
|
+
// Check initial lines
|
|
1775
|
+
const result = findMatch(this.lines, query);
|
|
1776
|
+
if (result.matched) {
|
|
1777
|
+
return result;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Check all pending lines
|
|
1781
|
+
for (const pendingBatch of this.pendingLines) {
|
|
1782
|
+
const batchResult = findMatch(pendingBatch, query);
|
|
1783
|
+
if (batchResult.matched) {
|
|
1784
|
+
return batchResult;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
return { matched: false };
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Create a TerminalLine from a raw string (helper for tests).
|
|
1793
|
+
* @param {string} raw
|
|
1794
|
+
* @returns {TerminalLine}
|
|
1795
|
+
*/
|
|
1796
|
+
static line(raw) {
|
|
1797
|
+
return { spans: [{ text: raw }], raw };
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Create multiple TerminalLines from raw strings (helper for tests).
|
|
1802
|
+
* @param {string[]} raws
|
|
1803
|
+
* @returns {TerminalLine[]}
|
|
1804
|
+
*/
|
|
1805
|
+
static lines(raws) {
|
|
1806
|
+
return raws.map((raw) => FakeTerminalStream.line(raw));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1123
1810
|
/**
|
|
1124
1811
|
* Extract pending tool from confirmation screen.
|
|
1125
1812
|
* @param {string} screen
|
|
@@ -2108,6 +2795,7 @@ const State = {
|
|
|
2108
2795
|
* @param {string[]} [config.spinners] - Spinner characters indicating thinking
|
|
2109
2796
|
* @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
|
|
2110
2797
|
* @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
2798
|
+
* @param {(string | RegExp)[]} [config.activeWorkPatterns] - Patterns indicating active work (beats ready)
|
|
2111
2799
|
* @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
|
|
2112
2800
|
* @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
|
|
2113
2801
|
* @returns {string} The detected state
|
|
@@ -2250,7 +2938,6 @@ function detectState(screen, config) {
|
|
|
2250
2938
|
* @property {string} [safeAllowedTools]
|
|
2251
2939
|
* @property {string | null} [sessionIdFlag]
|
|
2252
2940
|
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
2253
|
-
* @property {((entry: object) => string | null) | null} [logEntryFormatter]
|
|
2254
2941
|
*/
|
|
2255
2942
|
|
|
2256
2943
|
class Agent {
|
|
@@ -2298,8 +2985,6 @@ class Agent {
|
|
|
2298
2985
|
this.sessionIdFlag = config.sessionIdFlag || null;
|
|
2299
2986
|
/** @type {((sessionName: string) => string | null) | null} */
|
|
2300
2987
|
this.logPathFinder = config.logPathFinder || null;
|
|
2301
|
-
/** @type {((entry: object) => string | null) | null} */
|
|
2302
|
-
this.logEntryFormatter = config.logEntryFormatter || null;
|
|
2303
2988
|
}
|
|
2304
2989
|
|
|
2305
2990
|
/**
|
|
@@ -2447,15 +3132,32 @@ class Agent {
|
|
|
2447
3132
|
}
|
|
2448
3133
|
|
|
2449
3134
|
/**
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
*
|
|
3135
|
+
* Create a terminal stream for reading agent output.
|
|
3136
|
+
* Returns JsonlTerminalStream for agents with log file support,
|
|
3137
|
+
* otherwise falls back to ScreenTerminalStream.
|
|
3138
|
+
* @param {string} sessionName
|
|
3139
|
+
* @returns {TerminalStream}
|
|
2453
3140
|
*/
|
|
2454
|
-
|
|
2455
|
-
if
|
|
2456
|
-
|
|
3141
|
+
createStream(sessionName) {
|
|
3142
|
+
// Prefer JSONL stream if agent has log path finder
|
|
3143
|
+
if (this.logPathFinder) {
|
|
3144
|
+
/** @type {'claude' | 'codex'} */
|
|
3145
|
+
const format = this.name === "claude" ? "claude" : "codex";
|
|
3146
|
+
return new JsonlTerminalStream(() => this.findLogPath(sessionName), format);
|
|
2457
3147
|
}
|
|
2458
|
-
|
|
3148
|
+
// Fall back to screen capture
|
|
3149
|
+
return new ScreenTerminalStream(sessionName);
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
/**
|
|
3153
|
+
* Create a styled terminal stream with ANSI color support.
|
|
3154
|
+
* Only uses screen capture (JSONL doesn't have style info).
|
|
3155
|
+
* @param {string} sessionName
|
|
3156
|
+
* @param {number} [scrollback]
|
|
3157
|
+
* @returns {StyledScreenTerminalStream}
|
|
3158
|
+
*/
|
|
3159
|
+
createStyledStream(sessionName, scrollback = 0) {
|
|
3160
|
+
return new StyledScreenTerminalStream(sessionName, scrollback);
|
|
2459
3161
|
}
|
|
2460
3162
|
|
|
2461
3163
|
/**
|
|
@@ -2704,7 +3406,6 @@ const CodexAgent = new Agent({
|
|
|
2704
3406
|
reviewOptions: { branch: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
2705
3407
|
envVar: "AX_SESSION",
|
|
2706
3408
|
logPathFinder: findCodexLogPath,
|
|
2707
|
-
logEntryFormatter: formatCodexLogEntry,
|
|
2708
3409
|
});
|
|
2709
3410
|
|
|
2710
3411
|
// =============================================================================
|
|
@@ -2754,7 +3455,6 @@ const ClaudeAgent = new Agent({
|
|
|
2754
3455
|
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2755
3456
|
return null;
|
|
2756
3457
|
},
|
|
2757
|
-
logEntryFormatter: formatClaudeLogEntry,
|
|
2758
3458
|
});
|
|
2759
3459
|
|
|
2760
3460
|
// =============================================================================
|
|
@@ -2906,16 +3606,18 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2906
3606
|
|
|
2907
3607
|
/**
|
|
2908
3608
|
* Wait for agent response with streaming output to console.
|
|
3609
|
+
* Uses TerminalStream abstraction for reading agent output.
|
|
2909
3610
|
* @param {Agent} agent
|
|
2910
3611
|
* @param {string} session
|
|
2911
3612
|
* @param {number} [timeoutMs]
|
|
2912
3613
|
* @returns {Promise<{state: string, screen: string}>}
|
|
2913
3614
|
*/
|
|
2914
3615
|
async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2915
|
-
|
|
2916
|
-
|
|
3616
|
+
// Create terminal stream for this agent/session
|
|
3617
|
+
const stream = agent.createStream(session);
|
|
2917
3618
|
let printedThinking = false;
|
|
2918
|
-
debug("stream", `start:
|
|
3619
|
+
debug("stream", `start: using ${stream.constructor.name}`);
|
|
3620
|
+
|
|
2919
3621
|
// Sliding window for deduplication - only dedupe recent messages
|
|
2920
3622
|
// This catches Codex's duplicate log entries (A,B,A,B pattern) while
|
|
2921
3623
|
// allowing legitimate repeated messages across turns
|
|
@@ -2923,41 +3625,30 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2923
3625
|
const recentMessages = [];
|
|
2924
3626
|
const DEDUPE_WINDOW = 10;
|
|
2925
3627
|
|
|
2926
|
-
const
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
// Read from beginning when file is first discovered
|
|
2931
|
-
// (Claude creates log file when first message is sent)
|
|
2932
|
-
debug("stream", `log file discovered: ${logPath}`);
|
|
2933
|
-
logOffset = 0;
|
|
2934
|
-
}
|
|
3628
|
+
const streamNewLines = async () => {
|
|
3629
|
+
const lines = await stream.readNext();
|
|
3630
|
+
if (lines.length > 0) {
|
|
3631
|
+
debug("stream", `read ${lines.length} lines`);
|
|
2935
3632
|
}
|
|
2936
|
-
if (logPath) {
|
|
2937
|
-
const { entries, newOffset } = tailJsonl(logPath, logOffset);
|
|
2938
|
-
if (entries.length > 0) {
|
|
2939
|
-
debug("stream", `read ${entries.length} entries, offset ${logOffset} -> ${newOffset}`);
|
|
2940
|
-
}
|
|
2941
|
-
logOffset = newOffset;
|
|
2942
|
-
for (const entry of entries) {
|
|
2943
|
-
const formatted = agent.formatLogEntry(entry);
|
|
2944
|
-
if (!formatted) continue;
|
|
2945
|
-
|
|
2946
|
-
// Dedupe messages within sliding window (Codex logs can contain duplicates)
|
|
2947
|
-
// Tool calls (starting with ">") are always printed
|
|
2948
|
-
if (!formatted.startsWith(">")) {
|
|
2949
|
-
if (recentMessages.includes(formatted)) continue;
|
|
2950
|
-
recentMessages.push(formatted);
|
|
2951
|
-
if (recentMessages.length > DEDUPE_WINDOW) recentMessages.shift();
|
|
2952
|
-
}
|
|
2953
3633
|
|
|
2954
|
-
|
|
3634
|
+
for (const line of lines) {
|
|
3635
|
+
const text = line.raw;
|
|
3636
|
+
if (!text) continue;
|
|
3637
|
+
|
|
3638
|
+
// Dedupe messages within sliding window (Codex logs can contain duplicates)
|
|
3639
|
+
// Tool calls (starting with ">") are always printed
|
|
3640
|
+
if (!text.startsWith(">")) {
|
|
3641
|
+
if (recentMessages.includes(text)) continue;
|
|
3642
|
+
recentMessages.push(text);
|
|
3643
|
+
if (recentMessages.length > DEDUPE_WINDOW) recentMessages.shift();
|
|
2955
3644
|
}
|
|
3645
|
+
|
|
3646
|
+
console.log(text);
|
|
2956
3647
|
}
|
|
2957
3648
|
};
|
|
2958
3649
|
|
|
2959
3650
|
return pollForResponse(agent, session, timeoutMs, {
|
|
2960
|
-
onPoll: () =>
|
|
3651
|
+
onPoll: () => streamNewLines(),
|
|
2961
3652
|
onStateChange: (state, lastState, screen) => {
|
|
2962
3653
|
if (state === State.THINKING && !printedThinking) {
|
|
2963
3654
|
console.log("[THINKING]");
|
|
@@ -2970,7 +3661,7 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2970
3661
|
printedThinking = false;
|
|
2971
3662
|
}
|
|
2972
3663
|
},
|
|
2973
|
-
onReady: () =>
|
|
3664
|
+
onReady: () => streamNewLines(),
|
|
2974
3665
|
});
|
|
2975
3666
|
}
|
|
2976
3667
|
|
|
@@ -4519,8 +5210,13 @@ async function cmdAsk(
|
|
|
4519
5210
|
? /** @type {string} */ (session)
|
|
4520
5211
|
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
4521
5212
|
|
|
4522
|
-
|
|
4523
|
-
|
|
5213
|
+
if (sessionExists) {
|
|
5214
|
+
await waitUntilReady(agent, activeSession, timeoutMs);
|
|
5215
|
+
tmuxSend(activeSession, "C-u"); // Clear any stale input
|
|
5216
|
+
await sleep(50);
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
tmuxPasteLiteral(activeSession, message);
|
|
4524
5220
|
tmuxSend(activeSession, "Enter");
|
|
4525
5221
|
|
|
4526
5222
|
if (noWait) {
|
|
@@ -4708,6 +5404,12 @@ async function cmdReview(
|
|
|
4708
5404
|
? /** @type {string} */ (session)
|
|
4709
5405
|
: await cmdStart(agent, session, { yolo });
|
|
4710
5406
|
|
|
5407
|
+
if (sessionExists) {
|
|
5408
|
+
await waitUntilReady(agent, activeSession, timeoutMs);
|
|
5409
|
+
tmuxSend(activeSession, "C-u"); // Clear any stale input
|
|
5410
|
+
await sleep(50);
|
|
5411
|
+
}
|
|
5412
|
+
|
|
4711
5413
|
debug("review", `Codex path: sending /review command`);
|
|
4712
5414
|
tmuxSendLiteral(activeSession, "/review");
|
|
4713
5415
|
await sleep(50);
|
|
@@ -5189,7 +5891,7 @@ async function main() {
|
|
|
5189
5891
|
const cmd = positionals[0];
|
|
5190
5892
|
|
|
5191
5893
|
// Dispatch commands
|
|
5192
|
-
if (cmd === "agents") return cmdAgents();
|
|
5894
|
+
if (cmd === "agents" || cmd === "list") return cmdAgents();
|
|
5193
5895
|
if (cmd === "target") {
|
|
5194
5896
|
const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
5195
5897
|
if (defaultSession) {
|
|
@@ -5318,6 +6020,17 @@ export {
|
|
|
5318
6020
|
computePermissionHash,
|
|
5319
6021
|
formatClaudeLogEntry,
|
|
5320
6022
|
formatCodexLogEntry,
|
|
6023
|
+
// Terminal stream primitives
|
|
6024
|
+
parseJsonlEntry,
|
|
6025
|
+
parseScreenLines,
|
|
6026
|
+
parseAnsiLine,
|
|
6027
|
+
parseStyledScreenLines,
|
|
6028
|
+
findMatch,
|
|
6029
|
+
// Terminal stream implementations
|
|
6030
|
+
JsonlTerminalStream,
|
|
6031
|
+
ScreenTerminalStream,
|
|
6032
|
+
StyledScreenTerminalStream,
|
|
6033
|
+
FakeTerminalStream,
|
|
5321
6034
|
CodexAgent,
|
|
5322
6035
|
ClaudeAgent,
|
|
5323
6036
|
};
|