ax-agents 0.1.3 → 0.1.5
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 +1227 -89
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// 2 - rate limited
|
|
10
10
|
// 3 - awaiting confirmation
|
|
11
11
|
// 4 - thinking
|
|
12
|
+
// 5 - iteration complete, more work to do (ax do)
|
|
12
13
|
|
|
13
14
|
import { execSync, spawnSync, spawn } from "node:child_process";
|
|
14
15
|
import {
|
|
@@ -31,7 +32,7 @@ import { randomUUID, createHash } from "node:crypto";
|
|
|
31
32
|
import { fileURLToPath } from "node:url";
|
|
32
33
|
import path from "node:path";
|
|
33
34
|
import os from "node:os";
|
|
34
|
-
import { parseArgs } from "node:util";
|
|
35
|
+
import { parseArgs, styleText } from "node:util";
|
|
35
36
|
|
|
36
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
37
38
|
const __dirname = path.dirname(__filename);
|
|
@@ -134,14 +135,102 @@ const VERSION = packageJson.version;
|
|
|
134
135
|
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PreToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
135
136
|
*/
|
|
136
137
|
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Terminal Stream Types - Abstraction layer for terminal I/O
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Style properties for terminal text (ANSI colors, formatting)
|
|
144
|
+
* @typedef {Object} TerminalStyle
|
|
145
|
+
* @property {string} [fg] - Foreground color (e.g., "red", "green", "#ff0000")
|
|
146
|
+
* @property {string} [bg] - Background color
|
|
147
|
+
* @property {boolean} [bold] - Bold text
|
|
148
|
+
* @property {boolean} [dim] - Dimmed text
|
|
149
|
+
* @property {boolean} [italic] - Italic text
|
|
150
|
+
* @property {boolean} [underline] - Underlined text
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A span of text with optional styling
|
|
155
|
+
* @typedef {Object} TextSpan
|
|
156
|
+
* @property {string} text - The text content
|
|
157
|
+
* @property {TerminalStyle} [style] - Optional style properties
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A line of terminal output, containing styled spans and raw text
|
|
162
|
+
* @typedef {Object} TerminalLine
|
|
163
|
+
* @property {TextSpan[]} spans - Styled text spans
|
|
164
|
+
* @property {string} raw - Raw text content (spans joined, styles stripped)
|
|
165
|
+
* @property {'text' | 'thinking' | 'tool'} [lineType] - Content type for styling
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A segment of log output with type information
|
|
170
|
+
* @typedef {Object} LogSegment
|
|
171
|
+
* @property {'text' | 'thinking' | 'tool'} type - Content type
|
|
172
|
+
* @property {string} content - The text content
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Query for matching terminal lines
|
|
177
|
+
* @typedef {Object} MatchQuery
|
|
178
|
+
* @property {string | RegExp} pattern - Pattern to match against raw line text
|
|
179
|
+
* @property {Partial<TerminalStyle>} [style] - Optional style filter (ignored if implementation doesn't support styles)
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Result of a pattern match operation
|
|
184
|
+
* @typedef {Object} MatchResult
|
|
185
|
+
* @property {boolean} matched - Whether a match was found
|
|
186
|
+
* @property {TerminalLine} [line] - The matched line (if matched)
|
|
187
|
+
* @property {number} [lineIndex] - Index of the matched line (if matched)
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Options for reading from a terminal stream
|
|
192
|
+
* @typedef {Object} ReadOptions
|
|
193
|
+
* @property {number} [max] - Maximum number of lines to return
|
|
194
|
+
* @property {number} [timeoutMs] - Timeout in milliseconds
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Options for waiting for a match
|
|
199
|
+
* @typedef {Object} WaitOptions
|
|
200
|
+
* @property {number} [timeoutMs] - Timeout in milliseconds
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Interface for reading terminal output.
|
|
205
|
+
* Implementations: JsonlTerminalStream (Claude logs), ScreenTerminalStream (tmux capture)
|
|
206
|
+
* @typedef {Object} TerminalStream
|
|
207
|
+
* @property {(opts?: ReadOptions) => Promise<TerminalLine[]>} readNext - Read new lines since last read
|
|
208
|
+
* @property {(query: MatchQuery, opts?: WaitOptions) => Promise<MatchResult>} waitForMatch - Wait for a line matching the query
|
|
209
|
+
*/
|
|
210
|
+
|
|
137
211
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
138
212
|
|
|
213
|
+
// ANSI colour codes for debug output
|
|
214
|
+
const COLORS = {
|
|
215
|
+
reset: "\x1b[0m",
|
|
216
|
+
bright: "\x1b[1m",
|
|
217
|
+
cyan: "\x1b[96m", // Bright cyan
|
|
218
|
+
magenta: "\x1b[95m", // Bright magenta
|
|
219
|
+
yellow: "\x1b[93m", // Bright yellow
|
|
220
|
+
red: "\x1b[91m", // Bright red
|
|
221
|
+
};
|
|
222
|
+
|
|
139
223
|
/**
|
|
140
224
|
* @param {string} context
|
|
141
225
|
* @param {unknown} err
|
|
142
226
|
*/
|
|
143
227
|
function debugError(context, err) {
|
|
144
|
-
if (DEBUG)
|
|
228
|
+
if (DEBUG) {
|
|
229
|
+
const msg = err instanceof Error ? err.message : err;
|
|
230
|
+
console.error(
|
|
231
|
+
`${COLORS.bright}${COLORS.red}[error:${context}]${COLORS.reset} ${COLORS.magenta}${msg}${COLORS.reset}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
145
234
|
}
|
|
146
235
|
|
|
147
236
|
/**
|
|
@@ -150,7 +239,11 @@ function debugError(context, err) {
|
|
|
150
239
|
* @param {string} message - The debug message
|
|
151
240
|
*/
|
|
152
241
|
function debug(tag, message) {
|
|
153
|
-
if (DEBUG)
|
|
242
|
+
if (DEBUG) {
|
|
243
|
+
console.error(
|
|
244
|
+
`${COLORS.bright}${COLORS.cyan}[${tag}]${COLORS.reset} ${COLORS.yellow}${message}${COLORS.reset}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
154
247
|
}
|
|
155
248
|
|
|
156
249
|
// =============================================================================
|
|
@@ -174,6 +267,46 @@ const AI_DIR = path.join(PROJECT_ROOT, ".ai");
|
|
|
174
267
|
const AGENTS_DIR = path.join(AI_DIR, "agents");
|
|
175
268
|
const HOOKS_DIR = path.join(AI_DIR, "hooks");
|
|
176
269
|
const RFP_DIR = path.join(AI_DIR, "rfps");
|
|
270
|
+
const DO_DIR = path.join(AI_DIR, "do");
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get path to progress file for a named do task
|
|
274
|
+
* @param {string} name - Task name (default: "default")
|
|
275
|
+
* @returns {string}
|
|
276
|
+
*/
|
|
277
|
+
function getDoProgressPath(name = "default") {
|
|
278
|
+
const dir = path.join(DO_DIR, name);
|
|
279
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
280
|
+
const filePath = path.join(dir, "progress.txt");
|
|
281
|
+
// Touch the file if it doesn't exist so agent can read it on first iteration
|
|
282
|
+
if (!existsSync(filePath)) writeFileSync(filePath, "");
|
|
283
|
+
return filePath;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Build prompt for do loop with preamble and progress context
|
|
288
|
+
* @param {string} userPrompt
|
|
289
|
+
* @param {string} name
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/
|
|
292
|
+
function buildDoPrompt(userPrompt, name) {
|
|
293
|
+
const progressPath = getDoProgressPath(name);
|
|
294
|
+
const progress = existsSync(progressPath) ? readFileSync(progressPath, "utf-8") : "";
|
|
295
|
+
|
|
296
|
+
const relProgressPath = `.ai/do/${name}/progress.txt`;
|
|
297
|
+
const preamble = DO_PREAMBLE.replace(/\{progressPath\}/g, relProgressPath);
|
|
298
|
+
|
|
299
|
+
return `${preamble}
|
|
300
|
+
|
|
301
|
+
## Progress So Far
|
|
302
|
+
${progress || "(No progress yet)"}
|
|
303
|
+
|
|
304
|
+
## Your Task
|
|
305
|
+
${userPrompt}
|
|
306
|
+
|
|
307
|
+
Remember: Work on ONE thing, update ${relProgressPath}, run tests, commit.
|
|
308
|
+
When ALL tasks are complete, output <promise>COMPLETE</promise>`;
|
|
309
|
+
}
|
|
177
310
|
|
|
178
311
|
// =============================================================================
|
|
179
312
|
// Helpers - tmux
|
|
@@ -205,11 +338,13 @@ function tmuxHasSession(session) {
|
|
|
205
338
|
/**
|
|
206
339
|
* @param {string} session
|
|
207
340
|
* @param {number} [scrollback]
|
|
341
|
+
* @param {boolean} [withEscapes] - Include ANSI escape sequences (uses -e flag)
|
|
208
342
|
* @returns {string}
|
|
209
343
|
*/
|
|
210
|
-
function tmuxCapture(session, scrollback = 0) {
|
|
344
|
+
function tmuxCapture(session, scrollback = 0, withEscapes = false) {
|
|
211
345
|
try {
|
|
212
346
|
const args = ["capture-pane", "-t", session, "-p"];
|
|
347
|
+
if (withEscapes) args.push("-e"); // Include escape sequences
|
|
213
348
|
if (scrollback) args.push("-S", String(-scrollback));
|
|
214
349
|
return tmux(args);
|
|
215
350
|
} catch (err) {
|
|
@@ -236,6 +371,64 @@ function tmuxSendLiteral(session, text) {
|
|
|
236
371
|
tmux(["send-keys", "-t", session, "-l", text]);
|
|
237
372
|
}
|
|
238
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Paste text into a tmux session using load-buffer + paste-buffer.
|
|
376
|
+
* More reliable than send-keys -l for large text.
|
|
377
|
+
* Uses a named buffer to avoid races with concurrent invocations.
|
|
378
|
+
* @param {string} session
|
|
379
|
+
* @param {string} text
|
|
380
|
+
*/
|
|
381
|
+
function tmuxPasteLiteral(session, text) {
|
|
382
|
+
debug("tmux", `pasteLiteral session=${session}, text=${text.slice(0, 50)}...`);
|
|
383
|
+
// Use unique buffer name per invocation to avoid races (even to same session)
|
|
384
|
+
const bufferName = `ax-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
385
|
+
// Load text into named tmux buffer from stdin
|
|
386
|
+
const loadResult = spawnSync("tmux", ["load-buffer", "-b", bufferName, "-"], {
|
|
387
|
+
input: text,
|
|
388
|
+
encoding: "utf-8",
|
|
389
|
+
});
|
|
390
|
+
if (loadResult.status !== 0) {
|
|
391
|
+
debug("tmux", `load-buffer failed: ${loadResult.stderr}`);
|
|
392
|
+
throw new Error(loadResult.stderr || "tmux load-buffer failed");
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
// Paste buffer into the session
|
|
396
|
+
tmux(["paste-buffer", "-b", bufferName, "-t", session]);
|
|
397
|
+
// Move cursor to end of pasted text
|
|
398
|
+
tmux(["send-keys", "-t", session, "End"]);
|
|
399
|
+
} finally {
|
|
400
|
+
// Clean up the named buffer
|
|
401
|
+
try {
|
|
402
|
+
tmux(["delete-buffer", "-b", bufferName]);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
debugError("tmuxPasteLiteral", err);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Paste text and send Enter, waiting for multiline paste indicator if needed.
|
|
411
|
+
* Claude Code shows "[Pasted text #N +M lines]" for multiline input and needs
|
|
412
|
+
* time to process it before accepting Enter.
|
|
413
|
+
* @param {string} session
|
|
414
|
+
* @param {string} text
|
|
415
|
+
*/
|
|
416
|
+
async function tmuxSendText(session, text) {
|
|
417
|
+
const parsed = parseSessionName(session);
|
|
418
|
+
const isClaude = parsed?.tool === "claude";
|
|
419
|
+
const newlineCount = (text.match(/\n/g) || []).length;
|
|
420
|
+
|
|
421
|
+
tmuxPasteLiteral(session, text);
|
|
422
|
+
|
|
423
|
+
// For multiline text in Claude, use adaptive delay based on paste size
|
|
424
|
+
if (isClaude && newlineCount > 0) {
|
|
425
|
+
const delay = Math.min(1500, 50 + 3 * text.length + 20 * newlineCount);
|
|
426
|
+
debug("sendText", `multiline paste (${text.length} chars, ${newlineCount} lines), waiting ${delay}ms`);
|
|
427
|
+
await sleep(delay);
|
|
428
|
+
}
|
|
429
|
+
tmuxSend(session, "Enter");
|
|
430
|
+
}
|
|
431
|
+
|
|
239
432
|
/**
|
|
240
433
|
* @param {string} session
|
|
241
434
|
*/
|
|
@@ -247,6 +440,23 @@ function tmuxKill(session) {
|
|
|
247
440
|
}
|
|
248
441
|
}
|
|
249
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Rename a tmux session.
|
|
445
|
+
* @param {string} oldName
|
|
446
|
+
* @param {string} newName
|
|
447
|
+
* @returns {boolean}
|
|
448
|
+
*/
|
|
449
|
+
function tmuxRenameSession(oldName, newName) {
|
|
450
|
+
try {
|
|
451
|
+
tmux(["rename-session", "-t", oldName, newName]);
|
|
452
|
+
debug("tmux", `renamed session: ${oldName} -> ${newName}`);
|
|
453
|
+
return true;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
debugError("tmuxRenameSession", err);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
250
460
|
/**
|
|
251
461
|
* @param {string} session
|
|
252
462
|
* @param {string} command
|
|
@@ -396,6 +606,24 @@ const RFP_PREAMBLE = `## Guidelines
|
|
|
396
606
|
- Prioritize clarity over brevity.
|
|
397
607
|
- If you have nothing to propose, respond with ONLY "EMPTY_RESPONSE".`;
|
|
398
608
|
|
|
609
|
+
// Note: DO_PREAMBLE is a template - {progressPath} gets replaced at runtime
|
|
610
|
+
const DO_PREAMBLE = `You are an autonomous coding agent in a loop. Each iteration:
|
|
611
|
+
|
|
612
|
+
1. Read {progressPath} to see what's done
|
|
613
|
+
2. Choose the highest priority remaining task
|
|
614
|
+
3. Implement ONE small feature/fix
|
|
615
|
+
4. Run feedback loops (tests, types, lint)
|
|
616
|
+
5. Commit your changes with a clear message
|
|
617
|
+
6. Append to {progressPath} what you did
|
|
618
|
+
7. If ALL tasks are complete, output: <promise>COMPLETE</promise>
|
|
619
|
+
|
|
620
|
+
Guidelines:
|
|
621
|
+
- Work on ONE task per iteration, keep changes small
|
|
622
|
+
- Always run tests before committing - do NOT commit if tests fail
|
|
623
|
+
- Update {progressPath} BEFORE outputting COMPLETE
|
|
624
|
+
- Prioritize risky/architectural work first
|
|
625
|
+
- If stuck, document the blocker in {progressPath}`;
|
|
626
|
+
|
|
399
627
|
/**
|
|
400
628
|
* @param {string} session
|
|
401
629
|
* @param {(screen: string) => boolean} predicate
|
|
@@ -559,6 +787,10 @@ async function readStdinIfNeeded(value) {
|
|
|
559
787
|
* @property {string} [branch]
|
|
560
788
|
* @property {string} [archangels]
|
|
561
789
|
* @property {string} [autoApprove]
|
|
790
|
+
* @property {string} [name]
|
|
791
|
+
* @property {number} [maxLoops]
|
|
792
|
+
* @property {boolean} loop
|
|
793
|
+
* @property {boolean} reset
|
|
562
794
|
*/
|
|
563
795
|
function parseCliArgs(args) {
|
|
564
796
|
const { values, positionals } = parseArgs({
|
|
@@ -577,6 +809,8 @@ function parseCliArgs(args) {
|
|
|
577
809
|
stale: { type: "boolean", default: false },
|
|
578
810
|
version: { type: "boolean", short: "V", default: false },
|
|
579
811
|
help: { type: "boolean", short: "h", default: false },
|
|
812
|
+
loop: { type: "boolean", default: false },
|
|
813
|
+
reset: { type: "boolean", default: false },
|
|
580
814
|
// Value flags
|
|
581
815
|
tool: { type: "string" },
|
|
582
816
|
"auto-approve": { type: "string" },
|
|
@@ -586,6 +820,8 @@ function parseCliArgs(args) {
|
|
|
586
820
|
limit: { type: "string" },
|
|
587
821
|
branch: { type: "string" },
|
|
588
822
|
archangels: { type: "string" },
|
|
823
|
+
name: { type: "string" },
|
|
824
|
+
"max-loops": { type: "string" },
|
|
589
825
|
},
|
|
590
826
|
allowPositionals: true,
|
|
591
827
|
strict: false, // Don't error on unknown flags
|
|
@@ -613,6 +849,10 @@ function parseCliArgs(args) {
|
|
|
613
849
|
branch: /** @type {string | undefined} */ (values.branch),
|
|
614
850
|
archangels: /** @type {string | undefined} */ (values.archangels),
|
|
615
851
|
autoApprove: /** @type {string | undefined} */ (values["auto-approve"]),
|
|
852
|
+
name: /** @type {string | undefined} */ (values.name),
|
|
853
|
+
maxLoops: values["max-loops"] !== undefined ? Number(values["max-loops"]) : undefined,
|
|
854
|
+
loop: Boolean(values.loop),
|
|
855
|
+
reset: Boolean(values.reset),
|
|
616
856
|
},
|
|
617
857
|
positionals,
|
|
618
858
|
};
|
|
@@ -681,6 +921,31 @@ function generateSessionName(tool, { allowedTools = null, yolo = false } = {}) {
|
|
|
681
921
|
return `${tool}-partner-${uuid}`;
|
|
682
922
|
}
|
|
683
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Rebuild a session name with a new UUID, preserving other attributes.
|
|
926
|
+
* @param {string} sessionName - existing session name
|
|
927
|
+
* @param {string} newUuid - new UUID to use
|
|
928
|
+
* @returns {string | null}
|
|
929
|
+
*/
|
|
930
|
+
function rebuildSessionName(sessionName, newUuid) {
|
|
931
|
+
const parsed = parseSessionName(sessionName);
|
|
932
|
+
if (!parsed || !parsed.uuid) return null;
|
|
933
|
+
|
|
934
|
+
// Archangel sessions: {tool}-archangel-{name}-{uuid}
|
|
935
|
+
if (parsed.archangelName) {
|
|
936
|
+
return `${parsed.tool}-archangel-${parsed.archangelName}-${newUuid}`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Partner sessions: {tool}-partner-{uuid}[-p{hash}|-yolo]
|
|
940
|
+
let name = `${parsed.tool}-partner-${newUuid}`;
|
|
941
|
+
if (parsed.yolo) {
|
|
942
|
+
name += "-yolo";
|
|
943
|
+
} else if (parsed.permissionHash) {
|
|
944
|
+
name += `-p${parsed.permissionHash}`;
|
|
945
|
+
}
|
|
946
|
+
return name;
|
|
947
|
+
}
|
|
948
|
+
|
|
684
949
|
/**
|
|
685
950
|
* Quick hash for change detection (not cryptographic).
|
|
686
951
|
* @param {string | null | undefined} str
|
|
@@ -761,6 +1026,42 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
761
1026
|
return null;
|
|
762
1027
|
}
|
|
763
1028
|
|
|
1029
|
+
/**
|
|
1030
|
+
* Find the most recently created Claude session UUID for a project.
|
|
1031
|
+
* @param {string} sessionName - tmux session name (used to get cwd)
|
|
1032
|
+
* @returns {string | null}
|
|
1033
|
+
*/
|
|
1034
|
+
function findNewestClaudeSessionUuid(sessionName) {
|
|
1035
|
+
const cwd = getTmuxSessionCwd(sessionName) || process.cwd();
|
|
1036
|
+
const projectPath = getClaudeProjectPath(cwd);
|
|
1037
|
+
const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
|
|
1038
|
+
const indexPath = path.join(claudeProjectDir, "sessions-index.json");
|
|
1039
|
+
|
|
1040
|
+
if (!existsSync(indexPath)) {
|
|
1041
|
+
debug("log", `findNewestClaudeSessionUuid: no index at ${indexPath}`);
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
1047
|
+
if (!index.entries?.length) return null;
|
|
1048
|
+
|
|
1049
|
+
// Sort by created timestamp (most recent first)
|
|
1050
|
+
const sorted = [...index.entries].sort((a, b) => {
|
|
1051
|
+
const aTime = a.created ? new Date(a.created).getTime() : 0;
|
|
1052
|
+
const bTime = b.created ? new Date(b.created).getTime() : 0;
|
|
1053
|
+
return bTime - aTime;
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
const newest = sorted[0];
|
|
1057
|
+
debug("log", `findNewestClaudeSessionUuid: newest=${newest.sessionId}`);
|
|
1058
|
+
return newest.sessionId;
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
debugError("findNewestClaudeSessionUuid", err);
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
764
1065
|
/**
|
|
765
1066
|
* @param {string} sessionName
|
|
766
1067
|
* @returns {string | null}
|
|
@@ -837,7 +1138,10 @@ function findCodexLogPath(sessionName) {
|
|
|
837
1138
|
}
|
|
838
1139
|
// Return the closest match
|
|
839
1140
|
candidates.sort((a, b) => a.diff - b.diff);
|
|
840
|
-
debug(
|
|
1141
|
+
debug(
|
|
1142
|
+
"log",
|
|
1143
|
+
`findCodexLogPath: found ${candidates.length} candidates, best: ${candidates[0].path}`,
|
|
1144
|
+
);
|
|
841
1145
|
return candidates[0].path;
|
|
842
1146
|
} catch {
|
|
843
1147
|
debug("log", `findCodexLogPath: exception caught`);
|
|
@@ -1026,9 +1330,14 @@ function tailJsonl(logPath, fromOffset) {
|
|
|
1026
1330
|
/**
|
|
1027
1331
|
* Format a Claude Code JSONL log entry for streaming display.
|
|
1028
1332
|
* 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
|
|
1333
|
+
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, thinking?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
1030
1334
|
* @returns {string | null}
|
|
1031
1335
|
*/
|
|
1336
|
+
/**
|
|
1337
|
+
* Format a Claude JSONL log entry for streaming display.
|
|
1338
|
+
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, thinking?: string, name?: string, tool?: string, input?: {command?: string, file_path?: string, path?: string, pattern?: string, description?: string, subagent_type?: string}, arguments?: {command?: string, file_path?: string, path?: string, pattern?: string, description?: string, subagent_type?: string}}>}}} entry
|
|
1339
|
+
* @returns {LogSegment[] | null}
|
|
1340
|
+
*/
|
|
1032
1341
|
function formatClaudeLogEntry(entry) {
|
|
1033
1342
|
// Skip tool_result entries (they can be very verbose)
|
|
1034
1343
|
if (entry.type === "tool_result") return null;
|
|
@@ -1037,29 +1346,34 @@ function formatClaudeLogEntry(entry) {
|
|
|
1037
1346
|
if (entry.type !== "assistant") return null;
|
|
1038
1347
|
|
|
1039
1348
|
const parts = entry.message?.content || [];
|
|
1349
|
+
/** @type {LogSegment[]} */
|
|
1040
1350
|
const output = [];
|
|
1041
1351
|
|
|
1042
1352
|
for (const part of parts) {
|
|
1043
1353
|
if (part.type === "text" && part.text) {
|
|
1044
|
-
output.push(part.text);
|
|
1354
|
+
output.push({ type: "text", content: part.text });
|
|
1045
1355
|
} else if (part.type === "thinking" && part.thinking) {
|
|
1046
1356
|
// Include thinking blocks - extended thinking models put responses here
|
|
1047
|
-
output.push(part.thinking);
|
|
1357
|
+
output.push({ type: "thinking", content: part.thinking });
|
|
1048
1358
|
} else if (part.type === "tool_use" || part.type === "tool_call") {
|
|
1049
1359
|
const name = part.name || part.tool || "tool";
|
|
1050
1360
|
const input = part.input || part.arguments || {};
|
|
1051
1361
|
let summary;
|
|
1052
1362
|
if (name === "Bash" && input.command) {
|
|
1053
1363
|
summary = input.command.slice(0, 50);
|
|
1364
|
+
} else if (name === "Task" && (input.description || input.subagent_type)) {
|
|
1365
|
+
// Task tool: show description or subagent type
|
|
1366
|
+
summary = input.description || input.subagent_type || "";
|
|
1367
|
+
summary = summary.slice(0, 40);
|
|
1054
1368
|
} else {
|
|
1055
1369
|
const target = input.file_path || input.path || input.pattern || "";
|
|
1056
1370
|
summary = target.split("/").pop() || target.slice(0, 30);
|
|
1057
1371
|
}
|
|
1058
|
-
output.push(`> ${name}(${summary})`);
|
|
1372
|
+
output.push({ type: "tool", content: `> ${name}(${summary})` });
|
|
1059
1373
|
}
|
|
1060
1374
|
}
|
|
1061
1375
|
|
|
1062
|
-
return output.length > 0 ? output
|
|
1376
|
+
return output.length > 0 ? output : null;
|
|
1063
1377
|
}
|
|
1064
1378
|
|
|
1065
1379
|
/**
|
|
@@ -1068,8 +1382,9 @@ function formatClaudeLogEntry(entry) {
|
|
|
1068
1382
|
* - {type: "response_item", payload: {type: "message", role: "assistant", content: [{type: "output_text", text: "..."}]}}
|
|
1069
1383
|
* - {type: "response_item", payload: {type: "function_call", name: "...", arguments: "{...}"}}
|
|
1070
1384
|
* - {type: "event_msg", payload: {type: "agent_message", message: "..."}}
|
|
1071
|
-
*
|
|
1072
|
-
* @
|
|
1385
|
+
* - {type: "event_msg", payload: {type: "agent_reasoning", text: "..."}}
|
|
1386
|
+
* @param {{type?: string, payload?: {type?: string, role?: string, name?: string, arguments?: string, message?: string, text?: string, content?: Array<{type?: string, text?: string}>}}} entry
|
|
1387
|
+
* @returns {LogSegment[] | null}
|
|
1073
1388
|
*/
|
|
1074
1389
|
function formatCodexLogEntry(entry) {
|
|
1075
1390
|
// Skip function_call_output entries (equivalent to tool_result - can be verbose)
|
|
@@ -1085,6 +1400,10 @@ function formatCodexLogEntry(entry) {
|
|
|
1085
1400
|
const args = JSON.parse(entry.payload.arguments || "{}");
|
|
1086
1401
|
if (name === "shell_command" && args.command) {
|
|
1087
1402
|
summary = args.command.slice(0, 50);
|
|
1403
|
+
} else if (name === "Task" && (args.description || args.subagent_type)) {
|
|
1404
|
+
// Task tool: show description or subagent type
|
|
1405
|
+
summary = args.description || args.subagent_type || "";
|
|
1406
|
+
summary = summary.slice(0, 40);
|
|
1088
1407
|
} else {
|
|
1089
1408
|
const target = args.file_path || args.path || args.pattern || "";
|
|
1090
1409
|
summary = target.split("/").pop() || target.slice(0, 30);
|
|
@@ -1092,34 +1411,640 @@ function formatCodexLogEntry(entry) {
|
|
|
1092
1411
|
} catch {
|
|
1093
1412
|
summary = "...";
|
|
1094
1413
|
}
|
|
1095
|
-
return `> ${name}(${summary})
|
|
1414
|
+
return [{ type: "tool", content: `> ${name}(${summary})` }];
|
|
1096
1415
|
}
|
|
1097
1416
|
|
|
1098
1417
|
// Handle assistant messages (final response)
|
|
1099
1418
|
if (entry.type === "response_item" && entry.payload?.role === "assistant") {
|
|
1100
1419
|
const parts = entry.payload.content || [];
|
|
1420
|
+
/** @type {LogSegment[]} */
|
|
1101
1421
|
const output = [];
|
|
1102
1422
|
for (const part of parts) {
|
|
1103
1423
|
if ((part.type === "output_text" || part.type === "text") && part.text) {
|
|
1104
|
-
output.push(part.text);
|
|
1424
|
+
output.push({ type: "text", content: part.text });
|
|
1105
1425
|
}
|
|
1106
1426
|
}
|
|
1107
|
-
return output.length > 0 ? output
|
|
1427
|
+
return output.length > 0 ? output : null;
|
|
1108
1428
|
}
|
|
1109
1429
|
|
|
1110
1430
|
// Handle streaming agent messages
|
|
1111
1431
|
if (entry.type === "event_msg" && entry.payload?.type === "agent_message") {
|
|
1112
|
-
|
|
1432
|
+
const message = entry.payload.message;
|
|
1433
|
+
return message ? [{ type: "text", content: message }] : null;
|
|
1113
1434
|
}
|
|
1114
1435
|
|
|
1115
1436
|
// Handle agent reasoning (thinking during review)
|
|
1116
1437
|
if (entry.type === "event_msg" && entry.payload?.type === "agent_reasoning") {
|
|
1117
|
-
|
|
1438
|
+
const text = entry.payload.text;
|
|
1439
|
+
return text ? [{ type: "thinking", content: text }] : null;
|
|
1118
1440
|
}
|
|
1119
1441
|
|
|
1120
1442
|
return null;
|
|
1121
1443
|
}
|
|
1122
1444
|
|
|
1445
|
+
// =============================================================================
|
|
1446
|
+
// Terminal Stream Primitives - Pure functions for parsing terminal data
|
|
1447
|
+
// =============================================================================
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Parse a JSONL log entry into TerminalLine[].
|
|
1451
|
+
* Wraps formatClaudeLogEntry/formatCodexLogEntry to return structured data.
|
|
1452
|
+
* @param {object} entry - A parsed JSONL entry
|
|
1453
|
+
* @param {'claude' | 'codex'} format - The log format
|
|
1454
|
+
* @returns {TerminalLine[]}
|
|
1455
|
+
*/
|
|
1456
|
+
function parseJsonlEntry(entry, format) {
|
|
1457
|
+
const segments = format === "claude" ? formatClaudeLogEntry(entry) : formatCodexLogEntry(entry);
|
|
1458
|
+
if (!segments) return [];
|
|
1459
|
+
|
|
1460
|
+
// Convert segments to TerminalLines, splitting multiline content
|
|
1461
|
+
/** @type {TerminalLine[]} */
|
|
1462
|
+
const lines = [];
|
|
1463
|
+
for (const segment of segments) {
|
|
1464
|
+
const contentLines = segment.content.split("\n");
|
|
1465
|
+
for (const line of contentLines) {
|
|
1466
|
+
lines.push({
|
|
1467
|
+
spans: [{ text: line }],
|
|
1468
|
+
raw: line,
|
|
1469
|
+
lineType: segment.type,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return lines;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Parse raw screen output into TerminalLine[].
|
|
1478
|
+
* Each line becomes a TerminalLine with a single unstyled span.
|
|
1479
|
+
* @param {string} screen - Raw screen content from tmux capture
|
|
1480
|
+
* @returns {TerminalLine[]}
|
|
1481
|
+
*/
|
|
1482
|
+
function parseScreenLines(screen) {
|
|
1483
|
+
if (!screen) return [];
|
|
1484
|
+
return screen.split("\n").map((line) => ({
|
|
1485
|
+
spans: [{ text: line }],
|
|
1486
|
+
raw: line,
|
|
1487
|
+
}));
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* ANSI color code to color name mapping.
|
|
1492
|
+
* @type {Record<string, string>}
|
|
1493
|
+
*/
|
|
1494
|
+
const ANSI_COLORS = {
|
|
1495
|
+
30: "black",
|
|
1496
|
+
31: "red",
|
|
1497
|
+
32: "green",
|
|
1498
|
+
33: "yellow",
|
|
1499
|
+
34: "blue",
|
|
1500
|
+
35: "magenta",
|
|
1501
|
+
36: "cyan",
|
|
1502
|
+
37: "white",
|
|
1503
|
+
90: "bright-black",
|
|
1504
|
+
91: "bright-red",
|
|
1505
|
+
92: "bright-green",
|
|
1506
|
+
93: "bright-yellow",
|
|
1507
|
+
94: "bright-blue",
|
|
1508
|
+
95: "bright-magenta",
|
|
1509
|
+
96: "bright-cyan",
|
|
1510
|
+
97: "bright-white",
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* ANSI background color code to color name mapping.
|
|
1515
|
+
* @type {Record<string, string>}
|
|
1516
|
+
*/
|
|
1517
|
+
const ANSI_BG_COLORS = {
|
|
1518
|
+
40: "black",
|
|
1519
|
+
41: "red",
|
|
1520
|
+
42: "green",
|
|
1521
|
+
43: "yellow",
|
|
1522
|
+
44: "blue",
|
|
1523
|
+
45: "magenta",
|
|
1524
|
+
46: "cyan",
|
|
1525
|
+
47: "white",
|
|
1526
|
+
100: "bright-black",
|
|
1527
|
+
101: "bright-red",
|
|
1528
|
+
102: "bright-green",
|
|
1529
|
+
103: "bright-yellow",
|
|
1530
|
+
104: "bright-blue",
|
|
1531
|
+
105: "bright-magenta",
|
|
1532
|
+
106: "bright-cyan",
|
|
1533
|
+
107: "bright-white",
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Parse ANSI escape sequences from a line of text into styled spans.
|
|
1538
|
+
* @param {string} line - Line containing ANSI escape sequences
|
|
1539
|
+
* @returns {TextSpan[]}
|
|
1540
|
+
*/
|
|
1541
|
+
function parseAnsiLine(line) {
|
|
1542
|
+
if (!line) return [{ text: "" }];
|
|
1543
|
+
|
|
1544
|
+
const spans = [];
|
|
1545
|
+
/** @type {TerminalStyle} */
|
|
1546
|
+
let currentStyle = {};
|
|
1547
|
+
let currentText = "";
|
|
1548
|
+
|
|
1549
|
+
// ANSI escape sequence pattern: ESC [ <params> m
|
|
1550
|
+
// Matches sequences like \x1b[31m (red), \x1b[1;31m (bold red), \x1b[0m (reset)
|
|
1551
|
+
// eslint-disable-next-line no-control-regex
|
|
1552
|
+
const ansiPattern = /\x1b\[([0-9;]*)m/g;
|
|
1553
|
+
let lastIndex = 0;
|
|
1554
|
+
let match;
|
|
1555
|
+
|
|
1556
|
+
while ((match = ansiPattern.exec(line)) !== null) {
|
|
1557
|
+
// Add text before this escape sequence
|
|
1558
|
+
const textBefore = line.slice(lastIndex, match.index);
|
|
1559
|
+
if (textBefore) {
|
|
1560
|
+
currentText += textBefore;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Flush current span if we have text
|
|
1564
|
+
if (currentText) {
|
|
1565
|
+
/** @type {TextSpan} */
|
|
1566
|
+
const span = { text: currentText };
|
|
1567
|
+
if (Object.keys(currentStyle).length > 0) {
|
|
1568
|
+
span.style = { ...currentStyle };
|
|
1569
|
+
}
|
|
1570
|
+
spans.push(span);
|
|
1571
|
+
currentText = "";
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Parse SGR (Select Graphic Rendition) parameters
|
|
1575
|
+
// Note: \x1b[m (empty params) is equivalent to \x1b[0m (reset)
|
|
1576
|
+
const params = match[1].split(";").filter(Boolean);
|
|
1577
|
+
if (params.length === 0) {
|
|
1578
|
+
// Empty params means reset (e.g., \x1b[m)
|
|
1579
|
+
currentStyle = {};
|
|
1580
|
+
}
|
|
1581
|
+
for (const param of params) {
|
|
1582
|
+
const code = param;
|
|
1583
|
+
if (code === "0") {
|
|
1584
|
+
// Reset
|
|
1585
|
+
currentStyle = {};
|
|
1586
|
+
} else if (code === "1") {
|
|
1587
|
+
currentStyle.bold = true;
|
|
1588
|
+
} else if (code === "2") {
|
|
1589
|
+
currentStyle.dim = true;
|
|
1590
|
+
} else if (code === "3") {
|
|
1591
|
+
currentStyle.italic = true;
|
|
1592
|
+
} else if (code === "4") {
|
|
1593
|
+
currentStyle.underline = true;
|
|
1594
|
+
} else if (code === "22") {
|
|
1595
|
+
// Normal intensity (neither bold nor dim)
|
|
1596
|
+
delete currentStyle.bold;
|
|
1597
|
+
delete currentStyle.dim;
|
|
1598
|
+
} else if (code === "23") {
|
|
1599
|
+
delete currentStyle.italic;
|
|
1600
|
+
} else if (code === "24") {
|
|
1601
|
+
delete currentStyle.underline;
|
|
1602
|
+
} else if (ANSI_COLORS[code]) {
|
|
1603
|
+
currentStyle.fg = ANSI_COLORS[code];
|
|
1604
|
+
} else if (ANSI_BG_COLORS[code]) {
|
|
1605
|
+
currentStyle.bg = ANSI_BG_COLORS[code];
|
|
1606
|
+
} else if (code === "39") {
|
|
1607
|
+
// Default foreground
|
|
1608
|
+
delete currentStyle.fg;
|
|
1609
|
+
} else if (code === "49") {
|
|
1610
|
+
// Default background
|
|
1611
|
+
delete currentStyle.bg;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
lastIndex = ansiPattern.lastIndex;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Add remaining text
|
|
1619
|
+
const remaining = line.slice(lastIndex);
|
|
1620
|
+
if (remaining) {
|
|
1621
|
+
currentText += remaining;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Flush final span
|
|
1625
|
+
if (currentText || spans.length === 0) {
|
|
1626
|
+
/** @type {TextSpan} */
|
|
1627
|
+
const span = { text: currentText };
|
|
1628
|
+
if (Object.keys(currentStyle).length > 0) {
|
|
1629
|
+
span.style = { ...currentStyle };
|
|
1630
|
+
}
|
|
1631
|
+
spans.push(span);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
return spans;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Parse raw screen output with ANSI codes into styled TerminalLine[].
|
|
1639
|
+
* @param {string} screen - Screen content with ANSI escape codes
|
|
1640
|
+
* @returns {TerminalLine[]}
|
|
1641
|
+
*/
|
|
1642
|
+
function parseStyledScreenLines(screen) {
|
|
1643
|
+
if (!screen) return [];
|
|
1644
|
+
return screen.split("\n").map((line) => {
|
|
1645
|
+
const spans = parseAnsiLine(line);
|
|
1646
|
+
// Raw text is spans joined without styles
|
|
1647
|
+
const raw = spans.map((s) => s.text).join("");
|
|
1648
|
+
return { spans, raw };
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Find a line matching the given query.
|
|
1654
|
+
* Style filters are ignored when lines don't have style information.
|
|
1655
|
+
* @param {TerminalLine[]} lines - Lines to search
|
|
1656
|
+
* @param {MatchQuery} query - Query with pattern and optional style filter
|
|
1657
|
+
* @returns {MatchResult}
|
|
1658
|
+
*/
|
|
1659
|
+
function findMatch(lines, query) {
|
|
1660
|
+
const { pattern, style } = query;
|
|
1661
|
+
|
|
1662
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1663
|
+
const line = lines[i];
|
|
1664
|
+
const text = line.raw;
|
|
1665
|
+
|
|
1666
|
+
// Check pattern match
|
|
1667
|
+
const patternMatches =
|
|
1668
|
+
typeof pattern === "string" ? text.includes(pattern) : pattern.test(text);
|
|
1669
|
+
|
|
1670
|
+
if (!patternMatches) continue;
|
|
1671
|
+
|
|
1672
|
+
// If no style filter requested, we have a match
|
|
1673
|
+
if (!style) {
|
|
1674
|
+
return { matched: true, line, lineIndex: i };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Check style match (if line has styled spans)
|
|
1678
|
+
// Style filter is silently ignored if implementation doesn't provide styles
|
|
1679
|
+
const hasStyledSpans = line.spans.some((span) => span.style);
|
|
1680
|
+
if (!hasStyledSpans) {
|
|
1681
|
+
// No style info available - pattern match is enough
|
|
1682
|
+
return { matched: true, line, lineIndex: i };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// Check if any span matches both pattern and style
|
|
1686
|
+
const styleMatches = line.spans.some((span) => {
|
|
1687
|
+
if (!span.style) return false;
|
|
1688
|
+
const spanMatchesPattern =
|
|
1689
|
+
typeof pattern === "string" ? span.text.includes(pattern) : pattern.test(span.text);
|
|
1690
|
+
if (!spanMatchesPattern) return false;
|
|
1691
|
+
|
|
1692
|
+
// Check each requested style property
|
|
1693
|
+
const spanStyle = /** @type {Record<string, unknown>} */ (span.style);
|
|
1694
|
+
for (const [key, value] of Object.entries(style)) {
|
|
1695
|
+
if (spanStyle[key] !== value) return false;
|
|
1696
|
+
}
|
|
1697
|
+
return true;
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
if (styleMatches) {
|
|
1701
|
+
return { matched: true, line, lineIndex: i };
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return { matched: false };
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// =============================================================================
|
|
1709
|
+
// Terminal Stream Implementations
|
|
1710
|
+
// =============================================================================
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* Terminal stream that reads from JSONL log files (Claude/Codex logs).
|
|
1714
|
+
* Implements TerminalStream interface.
|
|
1715
|
+
* @implements {TerminalStream}
|
|
1716
|
+
*/
|
|
1717
|
+
class JsonlTerminalStream {
|
|
1718
|
+
/** @type {() => string | null} */
|
|
1719
|
+
logPathFinder;
|
|
1720
|
+
/** @type {'claude' | 'codex'} */
|
|
1721
|
+
format;
|
|
1722
|
+
/** @type {string | null} */
|
|
1723
|
+
logPath;
|
|
1724
|
+
/** @type {number} */
|
|
1725
|
+
offset;
|
|
1726
|
+
/** @type {boolean} */
|
|
1727
|
+
skipExisting;
|
|
1728
|
+
/** @type {boolean} */
|
|
1729
|
+
initialized;
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* @param {() => string | null} logPathFinder - Function that returns current log path (may change during session)
|
|
1733
|
+
* @param {'claude' | 'codex'} format - Log format for parsing entries
|
|
1734
|
+
* @param {{skipExisting?: boolean}} [opts] - Options
|
|
1735
|
+
*/
|
|
1736
|
+
constructor(logPathFinder, format, opts = {}) {
|
|
1737
|
+
this.logPathFinder = logPathFinder;
|
|
1738
|
+
this.format = format;
|
|
1739
|
+
this.logPath = null;
|
|
1740
|
+
this.offset = 0;
|
|
1741
|
+
this.skipExisting = opts.skipExisting ?? false;
|
|
1742
|
+
this.initialized = false;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Read new lines since last read.
|
|
1747
|
+
* @param {ReadOptions} [opts]
|
|
1748
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1749
|
+
*/
|
|
1750
|
+
async readNext(opts = {}) {
|
|
1751
|
+
// Check for new/changed log path
|
|
1752
|
+
const currentLogPath = this.logPathFinder();
|
|
1753
|
+
if (currentLogPath && currentLogPath !== this.logPath) {
|
|
1754
|
+
this.logPath = currentLogPath;
|
|
1755
|
+
if (existsSync(this.logPath)) {
|
|
1756
|
+
if (this.skipExisting && !this.initialized) {
|
|
1757
|
+
// Skip to end of file - only read new content
|
|
1758
|
+
this.offset = statSync(this.logPath).size;
|
|
1759
|
+
this.initialized = true;
|
|
1760
|
+
} else {
|
|
1761
|
+
// Read from beginning
|
|
1762
|
+
this.offset = 0;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (!this.logPath) {
|
|
1768
|
+
return [];
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const { entries, newOffset } = tailJsonl(this.logPath, this.offset);
|
|
1772
|
+
this.offset = newOffset;
|
|
1773
|
+
|
|
1774
|
+
const lines = [];
|
|
1775
|
+
for (const entry of entries) {
|
|
1776
|
+
const entryLines = parseJsonlEntry(entry, this.format);
|
|
1777
|
+
lines.push(...entryLines);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
if (opts.max && lines.length > opts.max) {
|
|
1781
|
+
return lines.slice(0, opts.max);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
return lines;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Wait for a line matching the query.
|
|
1789
|
+
* @param {MatchQuery} query
|
|
1790
|
+
* @param {WaitOptions} [opts]
|
|
1791
|
+
* @returns {Promise<MatchResult>}
|
|
1792
|
+
*/
|
|
1793
|
+
async waitForMatch(query, opts = {}) {
|
|
1794
|
+
const timeoutMs = opts.timeoutMs || 30000;
|
|
1795
|
+
const pollInterval = 100;
|
|
1796
|
+
const deadline = Date.now() + timeoutMs;
|
|
1797
|
+
|
|
1798
|
+
while (Date.now() < deadline) {
|
|
1799
|
+
const lines = await this.readNext();
|
|
1800
|
+
if (lines.length > 0) {
|
|
1801
|
+
const result = findMatch(lines, query);
|
|
1802
|
+
if (result.matched) {
|
|
1803
|
+
return result;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
await sleep(pollInterval);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return { matched: false };
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Terminal stream that reads from tmux screen capture.
|
|
1815
|
+
* Implements TerminalStream interface.
|
|
1816
|
+
* @implements {TerminalStream}
|
|
1817
|
+
*/
|
|
1818
|
+
class ScreenTerminalStream {
|
|
1819
|
+
/**
|
|
1820
|
+
* @param {string} session - tmux session name
|
|
1821
|
+
* @param {number} [scrollback] - Number of scrollback lines to capture
|
|
1822
|
+
*/
|
|
1823
|
+
constructor(session, scrollback = 0) {
|
|
1824
|
+
this.session = session;
|
|
1825
|
+
this.scrollback = scrollback;
|
|
1826
|
+
this.lastScreen = "";
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Read current screen lines (returns all visible lines on each call).
|
|
1831
|
+
* Note: Unlike JsonlTerminalStream, this returns the full screen each time.
|
|
1832
|
+
* @param {ReadOptions} [opts]
|
|
1833
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1834
|
+
*/
|
|
1835
|
+
async readNext(opts = {}) {
|
|
1836
|
+
const screen = tmuxCapture(this.session, this.scrollback);
|
|
1837
|
+
this.lastScreen = screen;
|
|
1838
|
+
|
|
1839
|
+
const lines = parseScreenLines(screen);
|
|
1840
|
+
|
|
1841
|
+
if (opts.max && lines.length > opts.max) {
|
|
1842
|
+
return lines.slice(-opts.max); // Return last N lines for screen capture
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
return lines;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
/**
|
|
1849
|
+
* Wait for a line matching the query.
|
|
1850
|
+
* @param {MatchQuery} query
|
|
1851
|
+
* @param {WaitOptions} [opts]
|
|
1852
|
+
* @returns {Promise<MatchResult>}
|
|
1853
|
+
*/
|
|
1854
|
+
async waitForMatch(query, opts = {}) {
|
|
1855
|
+
const timeoutMs = opts.timeoutMs || 30000;
|
|
1856
|
+
const pollInterval = 100;
|
|
1857
|
+
const deadline = Date.now() + timeoutMs;
|
|
1858
|
+
|
|
1859
|
+
while (Date.now() < deadline) {
|
|
1860
|
+
const lines = await this.readNext();
|
|
1861
|
+
const result = findMatch(lines, query);
|
|
1862
|
+
if (result.matched) {
|
|
1863
|
+
return result;
|
|
1864
|
+
}
|
|
1865
|
+
await sleep(pollInterval);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
return { matched: false };
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* Get the last captured screen (raw string).
|
|
1873
|
+
* Useful for compatibility with existing code that needs raw screen.
|
|
1874
|
+
* @returns {string}
|
|
1875
|
+
*/
|
|
1876
|
+
getLastScreen() {
|
|
1877
|
+
return this.lastScreen;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Terminal stream that reads from tmux screen capture with ANSI styling.
|
|
1883
|
+
* Uses `tmux capture-pane -e` to capture escape sequences.
|
|
1884
|
+
* Implements TerminalStream interface.
|
|
1885
|
+
* @implements {TerminalStream}
|
|
1886
|
+
*/
|
|
1887
|
+
class StyledScreenTerminalStream {
|
|
1888
|
+
/**
|
|
1889
|
+
* @param {string} session - tmux session name
|
|
1890
|
+
* @param {number} [scrollback] - Number of scrollback lines to capture
|
|
1891
|
+
*/
|
|
1892
|
+
constructor(session, scrollback = 0) {
|
|
1893
|
+
this.session = session;
|
|
1894
|
+
this.scrollback = scrollback;
|
|
1895
|
+
this.lastScreen = "";
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Read current screen lines with ANSI styling parsed.
|
|
1900
|
+
* @param {ReadOptions} [opts]
|
|
1901
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1902
|
+
*/
|
|
1903
|
+
async readNext(opts = {}) {
|
|
1904
|
+
const screen = tmuxCapture(this.session, this.scrollback, true); // withEscapes=true
|
|
1905
|
+
this.lastScreen = screen;
|
|
1906
|
+
|
|
1907
|
+
const lines = parseStyledScreenLines(screen);
|
|
1908
|
+
|
|
1909
|
+
if (opts.max && lines.length > opts.max) {
|
|
1910
|
+
return lines.slice(-opts.max); // Return last N lines for screen capture
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
return lines;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
/**
|
|
1917
|
+
* Wait for a line matching the query (supports style-aware matching).
|
|
1918
|
+
* @param {MatchQuery} query
|
|
1919
|
+
* @param {WaitOptions} [opts]
|
|
1920
|
+
* @returns {Promise<MatchResult>}
|
|
1921
|
+
*/
|
|
1922
|
+
async waitForMatch(query, opts = {}) {
|
|
1923
|
+
const timeoutMs = opts.timeoutMs || 30000;
|
|
1924
|
+
const pollInterval = 100;
|
|
1925
|
+
const deadline = Date.now() + timeoutMs;
|
|
1926
|
+
|
|
1927
|
+
while (Date.now() < deadline) {
|
|
1928
|
+
const lines = await this.readNext();
|
|
1929
|
+
const result = findMatch(lines, query);
|
|
1930
|
+
if (result.matched) {
|
|
1931
|
+
return result;
|
|
1932
|
+
}
|
|
1933
|
+
await sleep(pollInterval);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
return { matched: false };
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Get the last captured screen (raw string with ANSI codes).
|
|
1941
|
+
* @returns {string}
|
|
1942
|
+
*/
|
|
1943
|
+
getLastScreen() {
|
|
1944
|
+
return this.lastScreen;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Fake terminal stream for testing.
|
|
1950
|
+
* Implements TerminalStream interface.
|
|
1951
|
+
* @implements {TerminalStream}
|
|
1952
|
+
*/
|
|
1953
|
+
class FakeTerminalStream {
|
|
1954
|
+
/**
|
|
1955
|
+
* @param {TerminalLine[]} lines - Initial lines to provide
|
|
1956
|
+
*/
|
|
1957
|
+
constructor(lines = []) {
|
|
1958
|
+
this.lines = [...lines];
|
|
1959
|
+
this.readCount = 0;
|
|
1960
|
+
/** @type {TerminalLine[][]} */
|
|
1961
|
+
this.pendingLines = [];
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Queue lines to be returned on subsequent readNext calls.
|
|
1966
|
+
* @param {TerminalLine[]} lines
|
|
1967
|
+
*/
|
|
1968
|
+
queueLines(lines) {
|
|
1969
|
+
this.pendingLines.push(lines);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* Add more lines to the current buffer (simulates new output).
|
|
1974
|
+
* @param {TerminalLine[]} lines
|
|
1975
|
+
*/
|
|
1976
|
+
addLines(lines) {
|
|
1977
|
+
this.lines.push(...lines);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
/**
|
|
1981
|
+
* Read new lines since last read.
|
|
1982
|
+
* First call returns initial lines, subsequent calls return queued lines.
|
|
1983
|
+
* @param {ReadOptions} [opts]
|
|
1984
|
+
* @returns {Promise<TerminalLine[]>}
|
|
1985
|
+
*/
|
|
1986
|
+
async readNext(opts = {}) {
|
|
1987
|
+
this.readCount++;
|
|
1988
|
+
|
|
1989
|
+
/** @type {TerminalLine[]} */
|
|
1990
|
+
let result = [];
|
|
1991
|
+
if (this.readCount === 1) {
|
|
1992
|
+
result = this.lines;
|
|
1993
|
+
} else if (this.pendingLines.length > 0) {
|
|
1994
|
+
result = this.pendingLines.shift() || [];
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
if (opts.max && result.length > opts.max) {
|
|
1998
|
+
return result.slice(0, opts.max);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Wait for a line matching the query.
|
|
2006
|
+
* Immediately checks available lines without polling.
|
|
2007
|
+
* @param {MatchQuery} query
|
|
2008
|
+
* @param {WaitOptions} [_opts]
|
|
2009
|
+
* @returns {Promise<MatchResult>}
|
|
2010
|
+
*/
|
|
2011
|
+
async waitForMatch(query, _opts = {}) {
|
|
2012
|
+
// Check initial lines
|
|
2013
|
+
const result = findMatch(this.lines, query);
|
|
2014
|
+
if (result.matched) {
|
|
2015
|
+
return result;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// Check all pending lines
|
|
2019
|
+
for (const pendingBatch of this.pendingLines) {
|
|
2020
|
+
const batchResult = findMatch(pendingBatch, query);
|
|
2021
|
+
if (batchResult.matched) {
|
|
2022
|
+
return batchResult;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
return { matched: false };
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Create a TerminalLine from a raw string (helper for tests).
|
|
2031
|
+
* @param {string} raw
|
|
2032
|
+
* @returns {TerminalLine}
|
|
2033
|
+
*/
|
|
2034
|
+
static line(raw) {
|
|
2035
|
+
return { spans: [{ text: raw }], raw };
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Create multiple TerminalLines from raw strings (helper for tests).
|
|
2040
|
+
* @param {string[]} raws
|
|
2041
|
+
* @returns {TerminalLine[]}
|
|
2042
|
+
*/
|
|
2043
|
+
static lines(raws) {
|
|
2044
|
+
return raws.map((raw) => FakeTerminalStream.line(raw));
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
1123
2048
|
/**
|
|
1124
2049
|
* Extract pending tool from confirmation screen.
|
|
1125
2050
|
* @param {string} screen
|
|
@@ -2100,6 +3025,36 @@ const State = {
|
|
|
2100
3025
|
FEEDBACK_MODAL: "feedback_modal",
|
|
2101
3026
|
};
|
|
2102
3027
|
|
|
3028
|
+
/**
|
|
3029
|
+
* Check if the prompt symbol appears with bold styling in the last lines.
|
|
3030
|
+
* Used to distinguish actual prompts from text that happens to contain the symbol.
|
|
3031
|
+
* @param {string} session - tmux session name
|
|
3032
|
+
* @param {string} promptSymbol - The prompt symbol to look for
|
|
3033
|
+
* @returns {boolean}
|
|
3034
|
+
*/
|
|
3035
|
+
function hasStyledPrompt(session, promptSymbol) {
|
|
3036
|
+
const styledScreen = tmuxCapture(session, 0, true); // withEscapes=true
|
|
3037
|
+
|
|
3038
|
+
// If styled capture fails, fall back to allowing READY to avoid deadlock
|
|
3039
|
+
if (!styledScreen) {
|
|
3040
|
+
debug("state", "styled capture failed, falling back to unstyled check");
|
|
3041
|
+
return true;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
// Trim to match detectState behavior (removes trailing blank lines)
|
|
3045
|
+
const lines = parseStyledScreenLines(styledScreen.trim());
|
|
3046
|
+
const lastLines = lines.slice(-8);
|
|
3047
|
+
|
|
3048
|
+
for (const line of lastLines) {
|
|
3049
|
+
for (const span of line.spans) {
|
|
3050
|
+
if (span.text.includes(promptSymbol) && span.style?.bold) {
|
|
3051
|
+
return true;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
return false;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2103
3058
|
/**
|
|
2104
3059
|
* Pure function to detect agent state from screen content.
|
|
2105
3060
|
* @param {string} screen - The screen content to analyze
|
|
@@ -2108,8 +3063,11 @@ const State = {
|
|
|
2108
3063
|
* @param {string[]} [config.spinners] - Spinner characters indicating thinking
|
|
2109
3064
|
* @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
|
|
2110
3065
|
* @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
3066
|
+
* @param {(string | RegExp)[]} [config.activeWorkPatterns] - Patterns indicating active work (beats ready)
|
|
2111
3067
|
* @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
|
|
2112
3068
|
* @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
|
|
3069
|
+
* @param {string} [config.session] - tmux session for styled prompt verification
|
|
3070
|
+
* @param {boolean} [config.requireStyledPrompt] - If true, require prompt to be bold (Codex)
|
|
2113
3071
|
* @returns {string} The detected state
|
|
2114
3072
|
*/
|
|
2115
3073
|
function detectState(screen, config) {
|
|
@@ -2176,8 +3134,18 @@ function detectState(screen, config) {
|
|
|
2176
3134
|
// Ready - check BEFORE thinking to avoid false positives from timing messages like "✻ Worked for 45s"
|
|
2177
3135
|
// If the prompt symbol is visible, the agent is ready regardless of spinner characters in timing messages
|
|
2178
3136
|
if (lastLines.includes(config.promptSymbol)) {
|
|
2179
|
-
|
|
2180
|
-
|
|
3137
|
+
// If styled prompt check is enabled, verify prompt has expected styling
|
|
3138
|
+
// This prevents false positives from output containing the prompt symbol
|
|
3139
|
+
if (config.requireStyledPrompt && config.session) {
|
|
3140
|
+
if (hasStyledPrompt(config.session, config.promptSymbol)) {
|
|
3141
|
+
debug("state", `promptSymbol "${config.promptSymbol}" found with bold styling -> READY`);
|
|
3142
|
+
return State.READY;
|
|
3143
|
+
}
|
|
3144
|
+
debug("state", `promptSymbol "${config.promptSymbol}" found but not bold, continuing checks`);
|
|
3145
|
+
} else {
|
|
3146
|
+
debug("state", `promptSymbol "${config.promptSymbol}" found -> READY`);
|
|
3147
|
+
return State.READY;
|
|
3148
|
+
}
|
|
2181
3149
|
}
|
|
2182
3150
|
|
|
2183
3151
|
// Thinking - spinners (check last lines only)
|
|
@@ -2250,7 +3218,7 @@ function detectState(screen, config) {
|
|
|
2250
3218
|
* @property {string} [safeAllowedTools]
|
|
2251
3219
|
* @property {string | null} [sessionIdFlag]
|
|
2252
3220
|
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
2253
|
-
* @property {
|
|
3221
|
+
* @property {boolean} [requireStyledPrompt] - If true, require prompt to be bold for READY detection
|
|
2254
3222
|
*/
|
|
2255
3223
|
|
|
2256
3224
|
class Agent {
|
|
@@ -2298,8 +3266,8 @@ class Agent {
|
|
|
2298
3266
|
this.sessionIdFlag = config.sessionIdFlag || null;
|
|
2299
3267
|
/** @type {((sessionName: string) => string | null) | null} */
|
|
2300
3268
|
this.logPathFinder = config.logPathFinder || null;
|
|
2301
|
-
/** @type {
|
|
2302
|
-
this.
|
|
3269
|
+
/** @type {boolean} */
|
|
3270
|
+
this.requireStyledPrompt = config.requireStyledPrompt || false;
|
|
2303
3271
|
}
|
|
2304
3272
|
|
|
2305
3273
|
/**
|
|
@@ -2447,22 +3415,41 @@ class Agent {
|
|
|
2447
3415
|
}
|
|
2448
3416
|
|
|
2449
3417
|
/**
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
*
|
|
3418
|
+
* Create a terminal stream for reading agent output.
|
|
3419
|
+
* Returns JsonlTerminalStream for agents with log file support,
|
|
3420
|
+
* otherwise falls back to ScreenTerminalStream.
|
|
3421
|
+
* @param {string} sessionName
|
|
3422
|
+
* @param {{skipExisting?: boolean}} [opts] - Options
|
|
3423
|
+
* @returns {TerminalStream}
|
|
2453
3424
|
*/
|
|
2454
|
-
|
|
2455
|
-
if
|
|
2456
|
-
|
|
3425
|
+
createStream(sessionName, opts = {}) {
|
|
3426
|
+
// Prefer JSONL stream if agent has log path finder
|
|
3427
|
+
if (this.logPathFinder) {
|
|
3428
|
+
/** @type {'claude' | 'codex'} */
|
|
3429
|
+
const format = this.name === "claude" ? "claude" : "codex";
|
|
3430
|
+
return new JsonlTerminalStream(() => this.findLogPath(sessionName), format, opts);
|
|
2457
3431
|
}
|
|
2458
|
-
|
|
3432
|
+
// Fall back to screen capture
|
|
3433
|
+
return new ScreenTerminalStream(sessionName);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
/**
|
|
3437
|
+
* Create a styled terminal stream with ANSI color support.
|
|
3438
|
+
* Only uses screen capture (JSONL doesn't have style info).
|
|
3439
|
+
* @param {string} sessionName
|
|
3440
|
+
* @param {number} [scrollback]
|
|
3441
|
+
* @returns {StyledScreenTerminalStream}
|
|
3442
|
+
*/
|
|
3443
|
+
createStyledStream(sessionName, scrollback = 0) {
|
|
3444
|
+
return new StyledScreenTerminalStream(sessionName, scrollback);
|
|
2459
3445
|
}
|
|
2460
3446
|
|
|
2461
3447
|
/**
|
|
2462
3448
|
* @param {string} screen
|
|
3449
|
+
* @param {string} [session] - Optional session for styled prompt verification
|
|
2463
3450
|
* @returns {string}
|
|
2464
3451
|
*/
|
|
2465
|
-
getState(screen) {
|
|
3452
|
+
getState(screen, session) {
|
|
2466
3453
|
return detectState(screen, {
|
|
2467
3454
|
promptSymbol: this.promptSymbol,
|
|
2468
3455
|
spinners: this.spinners,
|
|
@@ -2471,6 +3458,8 @@ class Agent {
|
|
|
2471
3458
|
activeWorkPatterns: this.activeWorkPatterns,
|
|
2472
3459
|
confirmPatterns: this.confirmPatterns,
|
|
2473
3460
|
updatePromptPatterns: this.updatePromptPatterns,
|
|
3461
|
+
session,
|
|
3462
|
+
requireStyledPrompt: this.requireStyledPrompt,
|
|
2474
3463
|
});
|
|
2475
3464
|
}
|
|
2476
3465
|
|
|
@@ -2704,7 +3693,7 @@ const CodexAgent = new Agent({
|
|
|
2704
3693
|
reviewOptions: { branch: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
2705
3694
|
envVar: "AX_SESSION",
|
|
2706
3695
|
logPathFinder: findCodexLogPath,
|
|
2707
|
-
|
|
3696
|
+
requireStyledPrompt: true, // Codex prompt is bold, use this to avoid false positives
|
|
2708
3697
|
});
|
|
2709
3698
|
|
|
2710
3699
|
// =============================================================================
|
|
@@ -2722,7 +3711,7 @@ const ClaudeAgent = new Agent({
|
|
|
2722
3711
|
rateLimitPattern: /rate.?limit/i,
|
|
2723
3712
|
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2724
3713
|
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
2725
|
-
activeWorkPatterns: ["
|
|
3714
|
+
activeWorkPatterns: ["esc to interrupt"],
|
|
2726
3715
|
confirmPatterns: [
|
|
2727
3716
|
"Do you want to make this edit",
|
|
2728
3717
|
"Do you want to run this command",
|
|
@@ -2754,7 +3743,6 @@ const ClaudeAgent = new Agent({
|
|
|
2754
3743
|
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2755
3744
|
return null;
|
|
2756
3745
|
},
|
|
2757
|
-
logEntryFormatter: formatClaudeLogEntry,
|
|
2758
3746
|
});
|
|
2759
3747
|
|
|
2760
3748
|
// =============================================================================
|
|
@@ -2772,7 +3760,7 @@ const ClaudeAgent = new Agent({
|
|
|
2772
3760
|
async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2773
3761
|
const start = Date.now();
|
|
2774
3762
|
const initialScreen = tmuxCapture(session);
|
|
2775
|
-
const initialState = agent.getState(initialScreen);
|
|
3763
|
+
const initialState = agent.getState(initialScreen, session);
|
|
2776
3764
|
debug("waitUntilReady", `start: initialState=${initialState}, timeout=${timeoutMs}ms`);
|
|
2777
3765
|
|
|
2778
3766
|
// Dismiss feedback modal if present
|
|
@@ -2793,7 +3781,7 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2793
3781
|
while (Date.now() - start < timeoutMs) {
|
|
2794
3782
|
await sleep(POLL_MS);
|
|
2795
3783
|
const screen = tmuxCapture(session);
|
|
2796
|
-
const state = agent.getState(screen);
|
|
3784
|
+
const state = agent.getState(screen, session);
|
|
2797
3785
|
|
|
2798
3786
|
// Dismiss feedback modal if it appears
|
|
2799
3787
|
if (state === State.FEEDBACK_MODAL) {
|
|
@@ -2824,7 +3812,7 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2824
3812
|
const { onPoll, onStateChange, onReady } = hooks;
|
|
2825
3813
|
const start = Date.now();
|
|
2826
3814
|
const initialScreen = tmuxCapture(session);
|
|
2827
|
-
const initialState = agent.getState(initialScreen);
|
|
3815
|
+
const initialState = agent.getState(initialScreen, session);
|
|
2828
3816
|
debug("poll", `start: initialState=${initialState}, timeoutMs=${timeoutMs}`);
|
|
2829
3817
|
|
|
2830
3818
|
let lastScreen = initialScreen;
|
|
@@ -2840,7 +3828,7 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2840
3828
|
|
|
2841
3829
|
while (Date.now() - start < timeoutMs) {
|
|
2842
3830
|
const screen = tmuxCapture(session);
|
|
2843
|
-
const state = agent.getState(screen);
|
|
3831
|
+
const state = agent.getState(screen, session);
|
|
2844
3832
|
|
|
2845
3833
|
if (onPoll) onPoll(screen, state);
|
|
2846
3834
|
|
|
@@ -2906,16 +3894,19 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2906
3894
|
|
|
2907
3895
|
/**
|
|
2908
3896
|
* Wait for agent response with streaming output to console.
|
|
3897
|
+
* Uses TerminalStream abstraction for reading agent output.
|
|
2909
3898
|
* @param {Agent} agent
|
|
2910
3899
|
* @param {string} session
|
|
2911
3900
|
* @param {number} [timeoutMs]
|
|
2912
3901
|
* @returns {Promise<{state: string, screen: string}>}
|
|
2913
3902
|
*/
|
|
2914
3903
|
async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2915
|
-
|
|
2916
|
-
|
|
3904
|
+
// Create terminal stream for this agent/session
|
|
3905
|
+
// Skip existing content - only stream new responses
|
|
3906
|
+
const stream = agent.createStream(session, { skipExisting: true });
|
|
2917
3907
|
let printedThinking = false;
|
|
2918
|
-
debug("stream", `start:
|
|
3908
|
+
debug("stream", `start: using ${stream.constructor.name}`);
|
|
3909
|
+
|
|
2919
3910
|
// Sliding window for deduplication - only dedupe recent messages
|
|
2920
3911
|
// This catches Codex's duplicate log entries (A,B,A,B pattern) while
|
|
2921
3912
|
// allowing legitimate repeated messages across turns
|
|
@@ -2923,54 +3914,50 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2923
3914
|
const recentMessages = [];
|
|
2924
3915
|
const DEDUPE_WINDOW = 10;
|
|
2925
3916
|
|
|
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
|
-
}
|
|
3917
|
+
const streamNewLines = async () => {
|
|
3918
|
+
const lines = await stream.readNext();
|
|
3919
|
+
if (lines.length > 0) {
|
|
3920
|
+
debug("stream", `read ${lines.length} lines`);
|
|
2935
3921
|
}
|
|
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
3922
|
|
|
2954
|
-
|
|
3923
|
+
for (const line of lines) {
|
|
3924
|
+
const text = line.raw;
|
|
3925
|
+
if (!text) continue;
|
|
3926
|
+
|
|
3927
|
+
// Dedupe messages within sliding window (Codex logs can contain duplicates)
|
|
3928
|
+
// Tool calls are exempt: lineType === "tool" for JSONL streams, or starts with ">" for screen streams
|
|
3929
|
+
const isToolLine = line.lineType === "tool" || (!line.lineType && text.startsWith(">"));
|
|
3930
|
+
if (!isToolLine) {
|
|
3931
|
+
if (recentMessages.includes(text)) continue;
|
|
3932
|
+
recentMessages.push(text);
|
|
3933
|
+
if (recentMessages.length > DEDUPE_WINDOW) recentMessages.shift();
|
|
2955
3934
|
}
|
|
3935
|
+
|
|
3936
|
+
// Style based on content type
|
|
3937
|
+
// For screen streams, tool lines start with ">" and should be dimmed
|
|
3938
|
+
const isThinking = line.lineType === "thinking";
|
|
3939
|
+
const styled = isToolLine || isThinking ? styleText("dim", text) : text;
|
|
3940
|
+
console.log(styled);
|
|
2956
3941
|
}
|
|
2957
3942
|
};
|
|
2958
3943
|
|
|
2959
3944
|
return pollForResponse(agent, session, timeoutMs, {
|
|
2960
|
-
onPoll: () =>
|
|
3945
|
+
onPoll: () => streamNewLines(),
|
|
2961
3946
|
onStateChange: (state, lastState, screen) => {
|
|
2962
3947
|
if (state === State.THINKING && !printedThinking) {
|
|
2963
|
-
console.log("[THINKING]");
|
|
3948
|
+
console.log(styleText("dim", "[THINKING]"));
|
|
2964
3949
|
printedThinking = true;
|
|
2965
3950
|
} else if (state === State.CONFIRMING) {
|
|
2966
3951
|
const pendingTool = extractPendingToolFromScreen(screen);
|
|
2967
|
-
console.log(
|
|
3952
|
+
console.log(
|
|
3953
|
+
styleText("yellow", pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]"),
|
|
3954
|
+
);
|
|
2968
3955
|
}
|
|
2969
3956
|
if (lastState === State.THINKING && state !== State.THINKING) {
|
|
2970
3957
|
printedThinking = false;
|
|
2971
3958
|
}
|
|
2972
3959
|
},
|
|
2973
|
-
onReady: () =>
|
|
3960
|
+
onReady: () => streamNewLines(),
|
|
2974
3961
|
});
|
|
2975
3962
|
}
|
|
2976
3963
|
|
|
@@ -3046,7 +4033,7 @@ async function cmdStart(agent, session, { yolo = false, allowedTools = null } =
|
|
|
3046
4033
|
const start = Date.now();
|
|
3047
4034
|
while (Date.now() - start < STARTUP_TIMEOUT_MS) {
|
|
3048
4035
|
const screen = tmuxCapture(session);
|
|
3049
|
-
const state = agent.getState(screen);
|
|
4036
|
+
const state = agent.getState(screen, session);
|
|
3050
4037
|
|
|
3051
4038
|
if (state === State.UPDATE_PROMPT) {
|
|
3052
4039
|
await agent.handleUpdatePrompt(session);
|
|
@@ -3107,7 +4094,7 @@ function cmdAgents() {
|
|
|
3107
4094
|
const parsed = /** @type {ParsedSession} */ (parseSessionName(session));
|
|
3108
4095
|
const agent = parsed.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
3109
4096
|
const screen = tmuxCapture(session);
|
|
3110
|
-
const state = agent.getState(screen);
|
|
4097
|
+
const state = agent.getState(screen, session);
|
|
3111
4098
|
const type = parsed.archangelName ? "archangel" : "-";
|
|
3112
4099
|
const isDefault =
|
|
3113
4100
|
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
@@ -3270,7 +4257,7 @@ async function cmdArchangel(agentName) {
|
|
|
3270
4257
|
const start = Date.now();
|
|
3271
4258
|
while (Date.now() - start < ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
3272
4259
|
const screen = tmuxCapture(sessionName);
|
|
3273
|
-
const state = agent.getState(screen);
|
|
4260
|
+
const state = agent.getState(screen, sessionName);
|
|
3274
4261
|
|
|
3275
4262
|
if (state === State.UPDATE_PROMPT) {
|
|
3276
4263
|
await agent.handleUpdatePrompt(sessionName);
|
|
@@ -3436,7 +4423,7 @@ async function cmdArchangel(agentName) {
|
|
|
3436
4423
|
|
|
3437
4424
|
// Wait for ready
|
|
3438
4425
|
const screen = tmuxCapture(sessionName);
|
|
3439
|
-
const state = agent.getState(screen);
|
|
4426
|
+
const state = agent.getState(screen, sessionName);
|
|
3440
4427
|
|
|
3441
4428
|
if (state === State.RATE_LIMITED) {
|
|
3442
4429
|
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
@@ -4519,9 +5506,13 @@ async function cmdAsk(
|
|
|
4519
5506
|
? /** @type {string} */ (session)
|
|
4520
5507
|
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
4521
5508
|
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
5509
|
+
if (sessionExists) {
|
|
5510
|
+
await waitUntilReady(agent, activeSession, timeoutMs);
|
|
5511
|
+
tmuxSend(activeSession, "C-u"); // Clear any stale input
|
|
5512
|
+
await sleep(50);
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
await tmuxSendText(activeSession, message);
|
|
4525
5516
|
|
|
4526
5517
|
if (noWait) {
|
|
4527
5518
|
const parsed = parseSessionName(activeSession);
|
|
@@ -4566,6 +5557,92 @@ e.g.
|
|
|
4566
5557
|
}
|
|
4567
5558
|
}
|
|
4568
5559
|
|
|
5560
|
+
/**
|
|
5561
|
+
* @param {Agent} agent
|
|
5562
|
+
* @param {string} prompt
|
|
5563
|
+
* @param {{name?: string, maxLoops?: number, loop?: boolean, reset?: boolean, session?: string | null, yolo?: boolean, timeoutMs?: number}} [options]
|
|
5564
|
+
*/
|
|
5565
|
+
async function cmdDo(agent, prompt, options = {}) {
|
|
5566
|
+
const maxLoops = options.maxLoops || 10;
|
|
5567
|
+
const name = options.name || "default";
|
|
5568
|
+
const loop = options.loop || false;
|
|
5569
|
+
const reset = options.reset || false;
|
|
5570
|
+
const yolo = options.yolo || false;
|
|
5571
|
+
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
5572
|
+
|
|
5573
|
+
// Reset progress file if requested
|
|
5574
|
+
if (reset) {
|
|
5575
|
+
const progressPath = getDoProgressPath(name);
|
|
5576
|
+
writeFileSync(progressPath, "");
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
// Use provided session or start a new one
|
|
5580
|
+
const session = options.session
|
|
5581
|
+
? await cmdStart(agent, options.session, { yolo })
|
|
5582
|
+
: await cmdStart(agent, null, { yolo });
|
|
5583
|
+
|
|
5584
|
+
// Print session ID for targeting approvals when not in yolo mode
|
|
5585
|
+
if (!yolo) {
|
|
5586
|
+
const parsed = parseSessionName(session);
|
|
5587
|
+
const shortId = parsed?.uuid?.slice(0, 8) || session;
|
|
5588
|
+
console.error(`Session: ${shortId}`);
|
|
5589
|
+
}
|
|
5590
|
+
|
|
5591
|
+
const iterations = loop ? maxLoops : 1;
|
|
5592
|
+
|
|
5593
|
+
for (let i = 0; i < iterations; i++) {
|
|
5594
|
+
// Fresh context (except first iteration)
|
|
5595
|
+
if (i > 0) {
|
|
5596
|
+
tmuxSendLiteral(session, "/new");
|
|
5597
|
+
tmuxSend(session, "Enter");
|
|
5598
|
+
await waitUntilReady(agent, session, timeoutMs);
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
// Build prompt with preamble + progress context
|
|
5602
|
+
const fullPrompt = buildDoPrompt(prompt, name);
|
|
5603
|
+
|
|
5604
|
+
// Send prompt and submit
|
|
5605
|
+
await tmuxSendText(session, fullPrompt);
|
|
5606
|
+
|
|
5607
|
+
const { state, screen } = yolo
|
|
5608
|
+
? await autoApproveLoop(agent, session, timeoutMs, streamResponse)
|
|
5609
|
+
: await streamResponse(agent, session, timeoutMs);
|
|
5610
|
+
|
|
5611
|
+
if (state === State.RATE_LIMITED) {
|
|
5612
|
+
console.log(`\nRate limited: ${agent.parseRetryTime(screen)}`);
|
|
5613
|
+
process.exit(2);
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
if (state === State.CONFIRMING) {
|
|
5617
|
+
const parsed = parseSessionName(session);
|
|
5618
|
+
const shortId = parsed?.uuid?.slice(0, 8) || session;
|
|
5619
|
+
console.log(`\nAwaiting confirmation: ${formatConfirmationOutput(screen, agent)}`);
|
|
5620
|
+
console.log(`Add --session=${shortId} if you have multiple sessions`);
|
|
5621
|
+
console.log("Use 'ax approve --wait' or 'ax reject' to continue");
|
|
5622
|
+
process.exit(3);
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
const response = agent.getResponse(session, screen) || "";
|
|
5626
|
+
|
|
5627
|
+
// Check completion
|
|
5628
|
+
if (response.includes("<promise>COMPLETE</promise>")) {
|
|
5629
|
+
console.log(`\nCompleted after ${i + 1} iteration(s)`);
|
|
5630
|
+
return;
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
// Single iteration mode (default): exit with code 5 to signal "more work"
|
|
5634
|
+
if (!loop) {
|
|
5635
|
+
console.log(`\nIteration complete. Re-run to continue, or --reset to start over.`);
|
|
5636
|
+
process.exit(5);
|
|
5637
|
+
}
|
|
5638
|
+
|
|
5639
|
+
console.log(`\n--- Iteration ${i + 1}/${maxLoops} complete ---`);
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
console.log(`\nReached max iterations (${maxLoops}) without completion`);
|
|
5643
|
+
process.exit(1);
|
|
5644
|
+
}
|
|
5645
|
+
|
|
4569
5646
|
/**
|
|
4570
5647
|
* @param {Agent} agent
|
|
4571
5648
|
* @param {string | null | undefined} session
|
|
@@ -4578,7 +5655,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
4578
5655
|
}
|
|
4579
5656
|
|
|
4580
5657
|
const before = tmuxCapture(session);
|
|
4581
|
-
const beforeState = agent.getState(before);
|
|
5658
|
+
const beforeState = agent.getState(before, session);
|
|
4582
5659
|
if (beforeState !== State.CONFIRMING) {
|
|
4583
5660
|
console.log(`Already ${beforeState}`);
|
|
4584
5661
|
return;
|
|
@@ -4616,7 +5693,7 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
4616
5693
|
}
|
|
4617
5694
|
|
|
4618
5695
|
const before = tmuxCapture(session);
|
|
4619
|
-
const beforeState = agent.getState(before);
|
|
5696
|
+
const beforeState = agent.getState(before, session);
|
|
4620
5697
|
if (beforeState !== State.CONFIRMING) {
|
|
4621
5698
|
console.log(`Already ${beforeState}`);
|
|
4622
5699
|
return;
|
|
@@ -4708,6 +5785,12 @@ async function cmdReview(
|
|
|
4708
5785
|
? /** @type {string} */ (session)
|
|
4709
5786
|
: await cmdStart(agent, session, { yolo });
|
|
4710
5787
|
|
|
5788
|
+
if (sessionExists) {
|
|
5789
|
+
await waitUntilReady(agent, activeSession, timeoutMs);
|
|
5790
|
+
tmuxSend(activeSession, "C-u"); // Clear any stale input
|
|
5791
|
+
await sleep(50);
|
|
5792
|
+
}
|
|
5793
|
+
|
|
4711
5794
|
debug("review", `Codex path: sending /review command`);
|
|
4712
5795
|
tmuxSendLiteral(activeSession, "/review");
|
|
4713
5796
|
await sleep(50);
|
|
@@ -4804,7 +5887,7 @@ async function cmdOutput(
|
|
|
4804
5887
|
screen = tmuxCapture(session, 500);
|
|
4805
5888
|
}
|
|
4806
5889
|
|
|
4807
|
-
const state = agent.getState(screen);
|
|
5890
|
+
const state = agent.getState(screen, session);
|
|
4808
5891
|
|
|
4809
5892
|
if (state === State.RATE_LIMITED) {
|
|
4810
5893
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -4843,7 +5926,7 @@ function cmdStatus(agent, session) {
|
|
|
4843
5926
|
}
|
|
4844
5927
|
|
|
4845
5928
|
const screen = tmuxCapture(session);
|
|
4846
|
-
const state = agent.getState(screen);
|
|
5929
|
+
const state = agent.getState(screen, session);
|
|
4847
5930
|
|
|
4848
5931
|
if (state === State.RATE_LIMITED) {
|
|
4849
5932
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -4877,7 +5960,7 @@ function cmdDebug(agent, session, { scrollback = 0 } = {}) {
|
|
|
4877
5960
|
}
|
|
4878
5961
|
|
|
4879
5962
|
const screen = tmuxCapture(session, scrollback);
|
|
4880
|
-
const state = agent.getState(screen);
|
|
5963
|
+
const state = agent.getState(screen, session);
|
|
4881
5964
|
|
|
4882
5965
|
console.log(`=== Session: ${session} ===`);
|
|
4883
5966
|
console.log(`=== State: ${state} ===`);
|
|
@@ -5048,6 +6131,8 @@ Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
|
5048
6131
|
Messaging:
|
|
5049
6132
|
<message> Send message to ${name}
|
|
5050
6133
|
review [TYPE] [TARGET] Review code: uncommitted, branch [base], commit [ref], custom
|
|
6134
|
+
do <prompt> Run one iteration (re-run to continue, exit 5 = more work)
|
|
6135
|
+
Options: --name=NAME, --loop, --max-loops=N, --reset, --yolo
|
|
5051
6136
|
|
|
5052
6137
|
Sessions:
|
|
5053
6138
|
compact Summarise session to shrink context size
|
|
@@ -5067,7 +6152,7 @@ Archangels:
|
|
|
5067
6152
|
Recovery/State:
|
|
5068
6153
|
status Exit code: ready=0 rate_limit=2 confirm=3 thinking=4
|
|
5069
6154
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
5070
|
-
debug
|
|
6155
|
+
debug [SESSION] Show raw screen output and detected state
|
|
5071
6156
|
approve Approve pending action (send 'y')
|
|
5072
6157
|
reject Reject pending action (send 'n')
|
|
5073
6158
|
select N Select menu option N
|
|
@@ -5189,7 +6274,7 @@ async function main() {
|
|
|
5189
6274
|
const cmd = positionals[0];
|
|
5190
6275
|
|
|
5191
6276
|
// Dispatch commands
|
|
5192
|
-
if (cmd === "agents") return cmdAgents();
|
|
6277
|
+
if (cmd === "agents" || cmd === "list") return cmdAgents();
|
|
5193
6278
|
if (cmd === "target") {
|
|
5194
6279
|
const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
5195
6280
|
if (defaultSession) {
|
|
@@ -5204,8 +6289,14 @@ async function main() {
|
|
|
5204
6289
|
if (cmd === "recall") return cmdRecall(positionals[1]);
|
|
5205
6290
|
if (cmd === "archangel") return cmdArchangel(positionals[1]);
|
|
5206
6291
|
if (cmd === "kill") return cmdKill(session, { all, orphans, force });
|
|
5207
|
-
if (cmd === "attach")
|
|
5208
|
-
|
|
6292
|
+
if (cmd === "attach") {
|
|
6293
|
+
const attachSession = positionals[1] ? resolveSessionName(positionals[1]) : session;
|
|
6294
|
+
return cmdAttach(attachSession);
|
|
6295
|
+
}
|
|
6296
|
+
if (cmd === "log") {
|
|
6297
|
+
const logSession = positionals[1] ? resolveSessionName(positionals[1]) : session;
|
|
6298
|
+
return cmdLog(logSession, { tail, reasoning, follow });
|
|
6299
|
+
}
|
|
5209
6300
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
5210
6301
|
if (cmd === "rfp") {
|
|
5211
6302
|
if (positionals[1] === "wait") {
|
|
@@ -5224,6 +6315,23 @@ async function main() {
|
|
|
5224
6315
|
}
|
|
5225
6316
|
return cmdRfp(prompt, { archangels: flags.archangels, fresh, noWait });
|
|
5226
6317
|
}
|
|
6318
|
+
if (cmd === "do") {
|
|
6319
|
+
const rawPrompt = positionals.slice(1).join(" ");
|
|
6320
|
+
const prompt = await readStdinIfNeeded(rawPrompt);
|
|
6321
|
+
if (!prompt) {
|
|
6322
|
+
console.log("ERROR: no prompt provided");
|
|
6323
|
+
process.exit(1);
|
|
6324
|
+
}
|
|
6325
|
+
return cmdDo(agent, prompt, {
|
|
6326
|
+
name: flags.name || "default",
|
|
6327
|
+
maxLoops: flags.maxLoops || 10,
|
|
6328
|
+
loop: flags.loop,
|
|
6329
|
+
reset: flags.reset,
|
|
6330
|
+
session: flags.session ? session : null,
|
|
6331
|
+
yolo,
|
|
6332
|
+
timeoutMs,
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
5227
6335
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
5228
6336
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
5229
6337
|
if (cmd === "review") {
|
|
@@ -5235,7 +6343,10 @@ async function main() {
|
|
|
5235
6343
|
});
|
|
5236
6344
|
}
|
|
5237
6345
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
5238
|
-
if (cmd === "debug")
|
|
6346
|
+
if (cmd === "debug") {
|
|
6347
|
+
const debugSession = positionals[1] ? resolveSessionName(positionals[1]) : session;
|
|
6348
|
+
return cmdDebug(agent, debugSession);
|
|
6349
|
+
}
|
|
5239
6350
|
if (cmd === "output") {
|
|
5240
6351
|
const indexArg = positionals[1];
|
|
5241
6352
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
@@ -5244,7 +6355,23 @@ async function main() {
|
|
|
5244
6355
|
if (cmd === "send" && positionals.length > 1)
|
|
5245
6356
|
return cmdSend(session, positionals.slice(1).join(" "));
|
|
5246
6357
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
5247
|
-
if (cmd === "reset")
|
|
6358
|
+
if (cmd === "reset") {
|
|
6359
|
+
// Send /new and wait for completion
|
|
6360
|
+
await cmdAsk(agent, session, "/new", { timeoutMs });
|
|
6361
|
+
|
|
6362
|
+
// Find the newest session UUID and rename tmux session to match
|
|
6363
|
+
if (session && agent.name === "claude") {
|
|
6364
|
+
const newUuid = findNewestClaudeSessionUuid(session);
|
|
6365
|
+
if (newUuid) {
|
|
6366
|
+
const newName = rebuildSessionName(session, newUuid);
|
|
6367
|
+
if (newName && newName !== session) {
|
|
6368
|
+
tmuxRenameSession(session, newName);
|
|
6369
|
+
console.log(`Session: ${newName}`);
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
return;
|
|
6374
|
+
}
|
|
5248
6375
|
if (cmd === "select" && positionals[1])
|
|
5249
6376
|
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
5250
6377
|
|
|
@@ -5318,6 +6445,17 @@ export {
|
|
|
5318
6445
|
computePermissionHash,
|
|
5319
6446
|
formatClaudeLogEntry,
|
|
5320
6447
|
formatCodexLogEntry,
|
|
6448
|
+
// Terminal stream primitives
|
|
6449
|
+
parseJsonlEntry,
|
|
6450
|
+
parseScreenLines,
|
|
6451
|
+
parseAnsiLine,
|
|
6452
|
+
parseStyledScreenLines,
|
|
6453
|
+
findMatch,
|
|
6454
|
+
// Terminal stream implementations
|
|
6455
|
+
JsonlTerminalStream,
|
|
6456
|
+
ScreenTerminalStream,
|
|
6457
|
+
StyledScreenTerminalStream,
|
|
6458
|
+
FakeTerminalStream,
|
|
5321
6459
|
CodexAgent,
|
|
5322
6460
|
ClaudeAgent,
|
|
5323
6461
|
};
|