claude-yes 1.31.2 → 1.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -21
- package/dist/agent-yes.js +2 -0
- package/dist/amp-yes.js +2 -0
- package/dist/auggie-yes.js +2 -0
- package/dist/claude-yes.js +2 -20432
- package/dist/cli.js +18342 -10955
- package/dist/codex-yes.js +2 -20432
- package/dist/copilot-yes.js +2 -20432
- package/dist/cursor-yes.js +2 -20432
- package/dist/gemini-yes.js +2 -20432
- package/dist/grok-yes.js +2 -20432
- package/dist/index.js +16258 -13586
- package/dist/qwen-yes.js +2 -20432
- package/package.json +93 -81
- package/ts/ReadyManager.spec.ts +10 -10
- package/ts/ReadyManager.ts +1 -1
- package/ts/SUPPORTED_CLIS.ts +4 -0
- package/ts/catcher.spec.ts +69 -70
- package/ts/cli-idle.spec.ts +8 -8
- package/ts/cli.ts +18 -26
- package/ts/defineConfig.ts +4 -4
- package/ts/idleWaiter.spec.ts +9 -9
- package/ts/index.ts +474 -233
- package/ts/logger.ts +22 -0
- package/ts/parseCliArgs.spec.ts +146 -147
- package/ts/parseCliArgs.ts +127 -59
- package/ts/postbuild.ts +29 -15
- package/ts/pty-fix.ts +155 -0
- package/ts/pty.ts +19 -0
- package/ts/removeControlCharacters.spec.ts +37 -38
- package/ts/removeControlCharacters.ts +2 -1
- package/ts/runningLock.spec.ts +119 -125
- package/ts/runningLock.ts +44 -55
- package/ts/session-integration.spec.ts +34 -42
- package/ts/utils.spec.ts +35 -35
- package/ts/utils.ts +7 -7
- package/dist/cli.js.map +0 -365
- package/dist/index.js.map +0 -323
- package/ts/codex-resume.spec.ts +0 -239
- package/ts/codexSessionManager.spec.ts +0 -51
- package/ts/codexSessionManager.test.ts +0 -259
- package/ts/codexSessionManager.ts +0 -312
- package/ts/yesLog.spec.ts +0 -74
- package/ts/yesLog.ts +0 -27
package/ts/index.ts
CHANGED
|
@@ -1,57 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fromReadable, fromWritable } from
|
|
3
|
-
import { mkdir, writeFile } from
|
|
4
|
-
import path from
|
|
5
|
-
import DIE from
|
|
6
|
-
import sflow from
|
|
7
|
-
import { TerminalTextRender } from
|
|
8
|
-
import
|
|
9
|
-
import { catcher } from './catcher.js';
|
|
1
|
+
import { execaCommandSync, parseCommandString } from "execa";
|
|
2
|
+
import { fromReadable, fromWritable } from "from-node-stream";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import DIE from "phpdie";
|
|
6
|
+
import sflow from "sflow";
|
|
7
|
+
import { TerminalTextRender } from "terminal-render";
|
|
8
|
+
import { catcher } from "./catcher.ts";
|
|
10
9
|
import {
|
|
11
10
|
extractSessionId,
|
|
12
11
|
getSessionForCwd,
|
|
13
12
|
storeSessionForCwd,
|
|
14
|
-
} from
|
|
15
|
-
import { IdleWaiter } from
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
export { parseCliArgs } from './parseCliArgs';
|
|
13
|
+
} from "./resume/codexSessionManager.ts";
|
|
14
|
+
import { IdleWaiter } from "./idleWaiter.ts";
|
|
15
|
+
import pty, { ptyPackage } from "./pty.ts";
|
|
16
|
+
import { ReadyManager } from "./ReadyManager.ts";
|
|
17
|
+
import { removeControlCharacters } from "./removeControlCharacters.ts";
|
|
18
|
+
import { acquireLock, releaseLock, shouldUseLock } from "./runningLock.ts";
|
|
19
|
+
import { logger } from "./logger.ts";
|
|
20
|
+
import { createFifoStream } from "./beta/fifo.ts";
|
|
21
|
+
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
22
|
+
import winston from "winston";
|
|
23
|
+
import { mapObject, pipe } from "rambda";
|
|
24
|
+
|
|
27
25
|
export { removeControlCharacters };
|
|
28
26
|
|
|
29
27
|
export type AgentCliConfig = {
|
|
28
|
+
// cli
|
|
30
29
|
install?: string; // hint user for install command if not installed
|
|
31
30
|
version?: string; // hint user for version command to check if installed
|
|
32
31
|
binary?: string; // actual binary name if different from cli, e.g. cursor -> cursor-agent
|
|
32
|
+
defaultArgs?: string[]; // function to ensure certain args are present
|
|
33
|
+
|
|
34
|
+
// status detect, and actions
|
|
33
35
|
ready?: RegExp[]; // regex matcher for stdin ready, or line index for gemini
|
|
34
|
-
enter?: RegExp[]; // array of regex to match for sending Enter
|
|
35
36
|
fatal?: RegExp[]; // array of regex to match for fatal errors
|
|
37
|
+
exitCommands?: string[]; // commands to exit the cli gracefully
|
|
38
|
+
promptArg?: (string & {}) | "first-arg" | "last-arg"; // argument name to pass the prompt, e.g. --prompt, or first-arg for positional arg
|
|
39
|
+
|
|
40
|
+
// handle special format
|
|
41
|
+
noEOL?: boolean; // if true, do not split lines by \n when handling inputs, e.g. for codex, which uses cursor-move csi code instead of \n to move lines
|
|
42
|
+
|
|
43
|
+
// auto responds
|
|
44
|
+
enter?: RegExp[]; // array of regex to match for sending Enter
|
|
45
|
+
typingRespond?: { [message: string]: RegExp[] }; // type specified message to a specified pattern
|
|
46
|
+
|
|
47
|
+
// crash/resuming-session behaviour
|
|
36
48
|
restoreArgs?: string[]; // arguments to continue the session when crashed
|
|
37
|
-
|
|
38
|
-
noEOL?: boolean; // if true, do not split lines by \n, used for codex, which uses cursor-move csi code instead of \n to move lines
|
|
39
|
-
promptArg?: (string & {}) | 'first-arg' | 'last-arg'; // argument name to pass the prompt, e.g. --prompt, or first-arg for positional arg
|
|
40
|
-
bunx?: boolean; // if true, use bunx to run the binary
|
|
49
|
+
restartWithoutContinueArg?: RegExp[]; // array of regex to match for errors that require restart without continue args
|
|
41
50
|
};
|
|
42
|
-
export type
|
|
51
|
+
export type AgentYesConfig = {
|
|
52
|
+
configDir?: string; // directory to store agent-yes config files, e.g. session store
|
|
53
|
+
logsDir?: string; // directory to store agent-yes log files
|
|
43
54
|
clis: { [key: string]: AgentCliConfig };
|
|
44
55
|
};
|
|
45
56
|
|
|
46
|
-
// load user config from
|
|
47
|
-
export const config = await
|
|
48
|
-
|
|
57
|
+
// load user config from agent-yes.config.ts if exists
|
|
58
|
+
export const config = await import("../agent-yes.config.ts").then((mod) => mod.default || mod);
|
|
49
59
|
export const CLIS_CONFIG = config.clis as Record<
|
|
50
|
-
keyof Awaited<typeof config>[
|
|
60
|
+
keyof Awaited<typeof config>["clis"],
|
|
51
61
|
AgentCliConfig
|
|
52
62
|
>;
|
|
53
|
-
export type SUPPORTED_CLIS = keyof typeof CLIS_CONFIG;
|
|
54
|
-
export const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG) as SUPPORTED_CLIS[];
|
|
55
63
|
|
|
56
64
|
/**
|
|
57
65
|
* Main function to run agent-cli with automatic yes/no responses
|
|
@@ -68,20 +76,20 @@ export const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG) as SUPPORTED_CLIS[];
|
|
|
68
76
|
*
|
|
69
77
|
* @example
|
|
70
78
|
* ```typescript
|
|
71
|
-
* import
|
|
72
|
-
* await
|
|
79
|
+
* import agentYes from 'agent-yes';
|
|
80
|
+
* await agentYes({
|
|
73
81
|
* prompt: 'help me solve all todos in my codebase',
|
|
74
82
|
*
|
|
75
83
|
* // optional
|
|
76
84
|
* cliArgs: ['--verbose'], // additional args to pass to agent-cli
|
|
77
85
|
* exitOnIdle: 30000, // exit after 30 seconds of idle
|
|
78
86
|
* robust: true, // auto restart with --continue if claude crashes, default is true
|
|
79
|
-
* logFile: 'claude.log', // save logs to file
|
|
87
|
+
* logFile: 'claude-output.log', // save logs to file
|
|
80
88
|
* disableLock: false, // disable running lock (default is false)
|
|
81
89
|
* });
|
|
82
90
|
* ```
|
|
83
91
|
*/
|
|
84
|
-
export default async function
|
|
92
|
+
export default async function agentYes({
|
|
85
93
|
cli,
|
|
86
94
|
cliArgs = [],
|
|
87
95
|
prompt,
|
|
@@ -92,8 +100,11 @@ export default async function cliYes({
|
|
|
92
100
|
logFile,
|
|
93
101
|
removeControlCharactersFromStdout = false, // = !process.stdout.isTTY,
|
|
94
102
|
verbose = false,
|
|
95
|
-
queue =
|
|
103
|
+
queue = false,
|
|
96
104
|
install = false,
|
|
105
|
+
resume = false,
|
|
106
|
+
useSkills = false,
|
|
107
|
+
useFifo = false,
|
|
97
108
|
}: {
|
|
98
109
|
cli: SUPPORTED_CLIS;
|
|
99
110
|
cliArgs?: string[];
|
|
@@ -107,32 +118,33 @@ export default async function cliYes({
|
|
|
107
118
|
verbose?: boolean;
|
|
108
119
|
queue?: boolean;
|
|
109
120
|
install?: boolean; // if true, install the cli tool if not installed, e.g. will run `npm install -g cursor-agent`
|
|
121
|
+
resume?: boolean; // if true, resume previous session in current cwd if any
|
|
122
|
+
useSkills?: boolean; // if true, prepend SKILL.md header to the prompt for non-Claude agents
|
|
123
|
+
useFifo?: boolean; // if true, enable FIFO input stream on Linux for additional stdin input
|
|
110
124
|
}) {
|
|
111
125
|
// those overrides seems only works in bun
|
|
112
126
|
// await Promise.allSettled([
|
|
113
|
-
// import(path.join(process.cwd(), "
|
|
127
|
+
// import(path.join(process.cwd(), "agent-yes.config")),
|
|
114
128
|
// ])
|
|
115
129
|
// .then((e) => e.flatMap((e) => (e.status === "fulfilled" ? [e.value] : [])))
|
|
116
130
|
// .then(e=>e.at(0))
|
|
117
|
-
// .then((e) => e.default as ReturnType<typeof
|
|
131
|
+
// .then((e) => e.default as ReturnType<typeof defineAgentYesConfig>)
|
|
118
132
|
// .then(async (override) => deepMixin(config, override || {}))
|
|
119
133
|
// .catch((error) => {
|
|
120
134
|
// if (process.env.VERBOSE)
|
|
121
|
-
// console.warn("Fail to load
|
|
135
|
+
// console.warn("Fail to load agent-yes.config.ts", error);
|
|
122
136
|
// });
|
|
123
137
|
|
|
124
138
|
if (!cli) throw new Error(`cli is required`);
|
|
125
139
|
const conf =
|
|
126
140
|
CLIS_CONFIG[cli] ||
|
|
127
|
-
DIE(
|
|
128
|
-
`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(' ')}`,
|
|
129
|
-
);
|
|
141
|
+
DIE(`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(" ")}`);
|
|
130
142
|
|
|
131
143
|
// Acquire lock before starting agent (if in git repo or same cwd and lock is not disabled)
|
|
132
144
|
const workingDir = cwd ?? process.cwd();
|
|
133
145
|
if (queue) {
|
|
134
146
|
if (queue && shouldUseLock(workingDir)) {
|
|
135
|
-
await acquireLock(workingDir, prompt ??
|
|
147
|
+
await acquireLock(workingDir, prompt ?? "Interactive session");
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
// Register cleanup handlers for lock release
|
|
@@ -142,129 +154,227 @@ export default async function cliYes({
|
|
|
142
154
|
}
|
|
143
155
|
};
|
|
144
156
|
|
|
145
|
-
process.on(
|
|
157
|
+
process.on("exit", () => {
|
|
146
158
|
if (queue) releaseLock().catch(() => null);
|
|
147
159
|
});
|
|
148
|
-
process.on(
|
|
160
|
+
process.on("SIGINT", async (code) => {
|
|
149
161
|
await cleanupLock();
|
|
150
162
|
process.exit(code);
|
|
151
163
|
});
|
|
152
|
-
process.on(
|
|
164
|
+
process.on("SIGTERM", async (code) => {
|
|
153
165
|
await cleanupLock();
|
|
154
166
|
process.exit(code);
|
|
155
167
|
});
|
|
156
168
|
}
|
|
157
169
|
|
|
158
170
|
process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
|
|
171
|
+
|
|
159
172
|
let isFatal = false; // when true, do not restart on crash, and exit agent
|
|
173
|
+
let shouldRestartWithoutContinue = false; // when true, restart without continue args
|
|
174
|
+
|
|
160
175
|
const stdinReady = new ReadyManager();
|
|
176
|
+
const stdinFirstReady = new ReadyManager(); // if user send ctrl+c before
|
|
177
|
+
|
|
178
|
+
// force ready after 10s to avoid stuck forever if the ready-word mismatched
|
|
179
|
+
sleep(10e3).then(() => {
|
|
180
|
+
if (!stdinReady.isReady) stdinReady.ready();
|
|
181
|
+
if (!stdinFirstReady.isReady) stdinFirstReady.ready();
|
|
182
|
+
});
|
|
161
183
|
const nextStdout = new ReadyManager();
|
|
162
184
|
|
|
163
185
|
const shellOutputStream = new TransformStream<string, string>();
|
|
164
186
|
const outputWriter = shellOutputStream.writable.getWriter();
|
|
165
|
-
// const pty = await import('node-pty');
|
|
166
187
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
logger.debug(`Using ${ptyPackage} for pseudo terminal management.`);
|
|
189
|
+
|
|
190
|
+
const datetime = new Date().toISOString().replace(/\D/g, "").slice(0, 17);
|
|
191
|
+
const logPath = config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.log`);
|
|
192
|
+
const rawLogPath =
|
|
193
|
+
config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.raw.log`);
|
|
194
|
+
const rawLinesLogPath =
|
|
195
|
+
config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.lines.log`);
|
|
196
|
+
const debuggingLogsPath =
|
|
197
|
+
config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.debug.log`);
|
|
198
|
+
|
|
199
|
+
// add
|
|
200
|
+
if (debuggingLogsPath)
|
|
201
|
+
logger.add(
|
|
202
|
+
new winston.transports.File({
|
|
203
|
+
filename: debuggingLogsPath,
|
|
204
|
+
level: "debug",
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
174
207
|
|
|
175
208
|
// Detect if running as sub-agent
|
|
176
209
|
const isSubAgent = !!process.env.CLAUDE_PPID;
|
|
177
|
-
if (isSubAgent)
|
|
178
|
-
|
|
179
|
-
`[${cli}-yes] Running as sub-agent (CLAUDE_PPID=${process.env.CLAUDE_PPID})`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
210
|
+
if (isSubAgent)
|
|
211
|
+
logger.info(`[${cli}-yes] Running as sub-agent (CLAUDE_PPID=${process.env.CLAUDE_PPID})`);
|
|
182
212
|
|
|
183
|
-
const getPtyOptions = () =>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
}
|
|
213
|
+
const getPtyOptions = () => {
|
|
214
|
+
const ptyEnv = { ...(env ?? (process.env as Record<string, string>)) };
|
|
215
|
+
return {
|
|
216
|
+
name: "xterm-color",
|
|
217
|
+
...getTerminalDimensions(),
|
|
218
|
+
cwd: cwd ?? process.cwd(),
|
|
219
|
+
env: ptyEnv,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
192
222
|
|
|
193
223
|
// Apply CLI specific configurations (moved to CLI_CONFIGURES)
|
|
194
224
|
const cliConf = (CLIS_CONFIG as Record<string, AgentCliConfig>)[cli] || {};
|
|
195
|
-
cliArgs = cliConf.defaultArgs
|
|
196
|
-
|
|
197
|
-
|
|
225
|
+
cliArgs = cliConf.defaultArgs ? [...cliConf.defaultArgs, ...cliArgs] : cliArgs;
|
|
226
|
+
|
|
227
|
+
// If enabled, read SKILL.md header and prepend to the prompt for non-Claude agents
|
|
228
|
+
try {
|
|
229
|
+
const workingDir = cwd ?? process.cwd();
|
|
230
|
+
if (useSkills && cli !== "claude") {
|
|
231
|
+
// Find git root to determine search boundary
|
|
232
|
+
let gitRoot: string | null = null;
|
|
233
|
+
try {
|
|
234
|
+
const result = execaCommandSync("git rev-parse --show-toplevel", {
|
|
235
|
+
cwd: workingDir,
|
|
236
|
+
reject: false,
|
|
237
|
+
});
|
|
238
|
+
if (result.exitCode === 0) {
|
|
239
|
+
gitRoot = result.stdout.trim();
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Not a git repo, will only check cwd
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Walk up from cwd to git root (or stop at filesystem root) collecting SKILL.md files
|
|
246
|
+
const skillHeaders: string[] = [];
|
|
247
|
+
let currentDir = workingDir;
|
|
248
|
+
const searchLimit = gitRoot || path.parse(currentDir).root;
|
|
249
|
+
|
|
250
|
+
while (true) {
|
|
251
|
+
const skillPath = path.resolve(currentDir, "SKILL.md");
|
|
252
|
+
const md = await readFile(skillPath, "utf8").catch(() => null);
|
|
253
|
+
if (md) {
|
|
254
|
+
// Extract header (content before first level-2 heading `## `)
|
|
255
|
+
const headerMatch = md.match(/^[\s\S]*?(?=\n##\s)/);
|
|
256
|
+
const headerRaw = (headerMatch ? headerMatch[0] : md).trim();
|
|
257
|
+
if (headerRaw) {
|
|
258
|
+
skillHeaders.push(headerRaw);
|
|
259
|
+
if (verbose)
|
|
260
|
+
logger.info(`[skills] Found SKILL.md in ${currentDir} (${headerRaw.length} chars)`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Stop if we've reached git root or filesystem root
|
|
265
|
+
if (currentDir === searchLimit) break;
|
|
266
|
+
|
|
267
|
+
const parentDir = path.dirname(currentDir);
|
|
268
|
+
if (parentDir === currentDir) break; // Reached filesystem root
|
|
269
|
+
currentDir = parentDir;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (skillHeaders.length > 0) {
|
|
273
|
+
// Combine all headers (most specific first)
|
|
274
|
+
const combined = skillHeaders.join("\n\n---\n\n");
|
|
275
|
+
const MAX = 2000; // increased limit for multiple skills
|
|
276
|
+
const header = combined.length > MAX ? combined.slice(0, MAX) + "…" : combined;
|
|
277
|
+
const prefix = `Use this repository skill as context:\n\n${header}`;
|
|
278
|
+
prompt = prompt ? `${prefix}\n\n${prompt}` : prefix;
|
|
279
|
+
if (verbose)
|
|
280
|
+
logger.info(
|
|
281
|
+
`[skills] Injected ${skillHeaders.length} SKILL.md header(s) (${header.length} chars total)`,
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
if (verbose) logger.info("[skills] No SKILL.md found in directory hierarchy");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// Non-fatal; continue without skills
|
|
289
|
+
if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
|
|
290
|
+
}
|
|
198
291
|
|
|
199
292
|
// Handle --continue flag for codex session restoration
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
293
|
+
if (resume) {
|
|
294
|
+
if (cli === "codex" && resume) {
|
|
295
|
+
// Try to get stored session for this directory
|
|
296
|
+
const storedSessionId = await getSessionForCwd(workingDir);
|
|
297
|
+
if (storedSessionId) {
|
|
298
|
+
// Replace or add resume args
|
|
299
|
+
cliArgs = ["resume", storedSessionId, ...cliArgs];
|
|
300
|
+
await logger.debug(`resume|using stored session ID: ${storedSessionId}`);
|
|
301
|
+
} else {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`No stored session found for codex in directory: ${workingDir}, please try without resume option.`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
} else if (cli === "claude") {
|
|
307
|
+
// just add --continue flag for claude
|
|
308
|
+
cliArgs = ["--continue", ...cliArgs];
|
|
309
|
+
await logger.debug(`resume|adding --continue flag for claude`);
|
|
310
|
+
} else if (cli === "gemini") {
|
|
311
|
+
// Gemini supports session resume natively via --resume flag
|
|
312
|
+
// Sessions are project/directory-specific by default (stored in ~/.gemini/tmp/<project_hash>/chats/)
|
|
313
|
+
cliArgs = ["--resume", ...cliArgs];
|
|
314
|
+
await logger.debug(`resume|adding --resume flag for gemini`);
|
|
211
315
|
} else {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Resume option is not supported for cli: ${cli}, make a feature request if you want it. https://github.com/snomiao/agent-yes/issues`,
|
|
318
|
+
);
|
|
215
319
|
}
|
|
216
320
|
}
|
|
217
321
|
|
|
322
|
+
// If possible pass prompt via cli args, its usually faster than stdin
|
|
218
323
|
if (prompt && cliConf.promptArg) {
|
|
219
|
-
if (cliConf.promptArg ===
|
|
324
|
+
if (cliConf.promptArg === "first-arg") {
|
|
220
325
|
cliArgs = [prompt, ...cliArgs];
|
|
221
326
|
prompt = undefined; // clear prompt to avoid sending later
|
|
222
|
-
} else if (cliConf.promptArg ===
|
|
327
|
+
} else if (cliConf.promptArg === "last-arg") {
|
|
223
328
|
cliArgs = [...cliArgs, prompt];
|
|
224
329
|
prompt = undefined; // clear prompt to avoid sending later
|
|
225
|
-
} else if (cliConf.promptArg.startsWith(
|
|
330
|
+
} else if (cliConf.promptArg.startsWith("--")) {
|
|
226
331
|
cliArgs = [cliConf.promptArg, prompt, ...cliArgs];
|
|
227
332
|
prompt = undefined; // clear prompt to avoid sending later
|
|
228
333
|
} else {
|
|
229
|
-
|
|
334
|
+
logger.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
|
|
230
335
|
}
|
|
231
336
|
}
|
|
232
|
-
|
|
337
|
+
// Determine the actual cli command to run
|
|
233
338
|
|
|
234
339
|
const spawn = () => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
340
|
+
const cliCommand = cliConf?.binary || cli;
|
|
341
|
+
let [bin, ...args] = [...parseCommandString(cliCommand), ...cliArgs];
|
|
342
|
+
if (verbose) logger.info(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
|
|
343
|
+
logger.info(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
|
|
344
|
+
// throw new Error(JSON.stringify([bin!, args, getPtyOptions()]))
|
|
345
|
+
const spawned = pty.spawn(bin!, args, getPtyOptions());
|
|
346
|
+
logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid}`);
|
|
347
|
+
// if (globalThis.Bun)
|
|
348
|
+
// args = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`);
|
|
349
|
+
return spawned;
|
|
239
350
|
};
|
|
351
|
+
|
|
240
352
|
let shell = catcher(
|
|
241
353
|
// error handler
|
|
242
|
-
(error: unknown,
|
|
243
|
-
|
|
354
|
+
(error: unknown, _fn, ..._args) => {
|
|
355
|
+
logger.error(`Fatal: Failed to start ${cli}.`);
|
|
356
|
+
|
|
357
|
+
const isNotFound = isCommandNotFoundError(error);
|
|
358
|
+
if (cliConf?.install && isNotFound) {
|
|
359
|
+
logger.info(`Please install the cli by run ${cliConf.install}`);
|
|
244
360
|
|
|
245
|
-
if (cliConf?.install && isCommandNotFoundError(error))
|
|
246
361
|
if (install) {
|
|
247
|
-
|
|
248
|
-
execaCommandSync(cliConf.install, { stdio:
|
|
249
|
-
|
|
250
|
-
`${cli} installed successfully. Please rerun the command.`,
|
|
251
|
-
);
|
|
362
|
+
logger.info(`Attempting to install ${cli}...`);
|
|
363
|
+
execaCommandSync(cliConf.install, { stdio: "inherit" });
|
|
364
|
+
logger.info(`${cli} installed successfully. Please rerun the command.`);
|
|
252
365
|
return spawn();
|
|
366
|
+
} else {
|
|
367
|
+
logger.error(
|
|
368
|
+
`If you did not installed it yet, Please install it first: ${cliConf.install}`,
|
|
369
|
+
);
|
|
370
|
+
throw error;
|
|
253
371
|
}
|
|
254
|
-
|
|
255
|
-
`If you did not installed it yet, Please install it first: ${cliConf.install}`,
|
|
256
|
-
);
|
|
372
|
+
}
|
|
257
373
|
|
|
258
|
-
if (
|
|
259
|
-
globalThis.Bun &&
|
|
260
|
-
error instanceof Error &&
|
|
261
|
-
error.stack?.includes('bun-pty')
|
|
262
|
-
) {
|
|
374
|
+
if (globalThis.Bun && error instanceof Error && error.stack?.includes("bun-pty")) {
|
|
263
375
|
// try to fix bun-pty issues
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
);
|
|
267
|
-
require('./fix-pty.js');
|
|
376
|
+
logger.error(`Detected bun-pty issue, attempted to fix it. Please try again.`);
|
|
377
|
+
require("./pty-fix");
|
|
268
378
|
// unable to retry with same process, so exit here.
|
|
269
379
|
}
|
|
270
380
|
throw error;
|
|
@@ -272,9 +382,9 @@ export default async function cliYes({
|
|
|
272
382
|
function isCommandNotFoundError(e: unknown) {
|
|
273
383
|
if (e instanceof Error) {
|
|
274
384
|
return (
|
|
275
|
-
e.message.includes(
|
|
276
|
-
e.message.includes(
|
|
277
|
-
e.message.includes(
|
|
385
|
+
e.message.includes("command not found") || // unix
|
|
386
|
+
e.message.includes("ENOENT") || // unix
|
|
387
|
+
e.message.includes("spawn") // windows
|
|
278
388
|
);
|
|
279
389
|
}
|
|
280
390
|
return false;
|
|
@@ -283,7 +393,6 @@ export default async function cliYes({
|
|
|
283
393
|
spawn,
|
|
284
394
|
)();
|
|
285
395
|
const pendingExitCode = Promise.withResolvers<number | null>();
|
|
286
|
-
let pendingExitCodeValue = null;
|
|
287
396
|
|
|
288
397
|
async function onData(data: string) {
|
|
289
398
|
// append data to the buffer, so we can process it later
|
|
@@ -295,31 +404,50 @@ export default async function cliYes({
|
|
|
295
404
|
stdinReady.unready(); // start buffer stdin
|
|
296
405
|
const agentCrashed = exitCode !== 0;
|
|
297
406
|
|
|
407
|
+
// Handle restart without continue args (e.g., "No conversation found to continue")
|
|
408
|
+
// logger.debug(``, { shouldRestartWithoutContinue, robust })
|
|
409
|
+
if (shouldRestartWithoutContinue) {
|
|
410
|
+
shouldRestartWithoutContinue = false; // reset flag
|
|
411
|
+
isFatal = false; // reset fatal flag to allow restart
|
|
412
|
+
|
|
413
|
+
// Restart without continue args - use original cliArgs without restoreArgs
|
|
414
|
+
const cliCommand = cliConf?.binary || cli;
|
|
415
|
+
let [bin, ...args] = [
|
|
416
|
+
...parseCommandString(cliCommand),
|
|
417
|
+
...cliArgs.filter((arg) => !["--continue", "--resume"].includes(arg)),
|
|
418
|
+
];
|
|
419
|
+
logger.info(`Restarting ${cli} ${JSON.stringify([bin, ...args])}`);
|
|
420
|
+
|
|
421
|
+
shell = pty.spawn(bin!, args, getPtyOptions());
|
|
422
|
+
shell.onData(onData);
|
|
423
|
+
shell.onExit(onExit);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
298
427
|
if (agentCrashed && robust && conf?.restoreArgs) {
|
|
299
428
|
if (!conf.restoreArgs) {
|
|
300
|
-
|
|
429
|
+
logger.warn(
|
|
301
430
|
`robust is only supported for ${Object.entries(CLIS_CONFIG)
|
|
302
431
|
.filter(([_, v]) => v.restoreArgs)
|
|
303
432
|
.map(([k]) => k)
|
|
304
|
-
.join(
|
|
433
|
+
.join(", ")} currently, not ${cli}`,
|
|
305
434
|
);
|
|
435
|
+
return;
|
|
306
436
|
}
|
|
307
|
-
if (isFatal)
|
|
308
|
-
return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
|
|
309
|
-
}
|
|
437
|
+
if (isFatal) return pendingExitCode.resolve(exitCode);
|
|
310
438
|
|
|
311
|
-
|
|
439
|
+
logger.info(`${cli} crashed, restarting...`);
|
|
312
440
|
|
|
313
441
|
// For codex, try to use stored session ID for this directory
|
|
314
442
|
let restoreArgs = conf.restoreArgs;
|
|
315
|
-
if (cli ===
|
|
443
|
+
if (cli === "codex") {
|
|
316
444
|
const storedSessionId = await getSessionForCwd(workingDir);
|
|
317
445
|
if (storedSessionId) {
|
|
318
446
|
// Use specific session ID instead of --last
|
|
319
|
-
restoreArgs = [
|
|
320
|
-
|
|
447
|
+
restoreArgs = ["resume", storedSessionId];
|
|
448
|
+
logger.debug(`restore|using stored session ID: ${storedSessionId}`);
|
|
321
449
|
} else {
|
|
322
|
-
|
|
450
|
+
logger.debug(`restore|no stored session, using default restore args`);
|
|
323
451
|
}
|
|
324
452
|
}
|
|
325
453
|
|
|
@@ -328,11 +456,11 @@ export default async function cliYes({
|
|
|
328
456
|
shell.onExit(onExit);
|
|
329
457
|
return;
|
|
330
458
|
}
|
|
331
|
-
return pendingExitCode.resolve(
|
|
459
|
+
return pendingExitCode.resolve(exitCode);
|
|
332
460
|
});
|
|
333
461
|
|
|
334
|
-
// when current tty resized, resize the pty
|
|
335
|
-
process.stdout.on(
|
|
462
|
+
// when current tty resized, resize the pty too
|
|
463
|
+
process.stdout.on("resize", () => {
|
|
336
464
|
const { cols, rows } = getTerminalDimensions(); // minimum 80 columns to avoid layout issues
|
|
337
465
|
shell.resize(cols, rows); // minimum 80 columns to avoid layout issues
|
|
338
466
|
});
|
|
@@ -341,133 +469,240 @@ export default async function cliYes({
|
|
|
341
469
|
const isStillWorkingQ = () =>
|
|
342
470
|
terminalRender
|
|
343
471
|
.render()
|
|
344
|
-
.replace(/\s+/g,
|
|
472
|
+
.replace(/\s+/g, " ")
|
|
345
473
|
.match(/esc to interrupt|to run in background/);
|
|
346
474
|
|
|
347
475
|
const idleWaiter = new IdleWaiter();
|
|
348
476
|
if (exitOnIdle)
|
|
349
477
|
idleWaiter.wait(exitOnIdle).then(async () => {
|
|
350
478
|
if (isStillWorkingQ()) {
|
|
351
|
-
|
|
352
|
-
'[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet',
|
|
353
|
-
);
|
|
479
|
+
logger.warn("[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet");
|
|
354
480
|
return;
|
|
355
481
|
}
|
|
356
482
|
|
|
357
|
-
|
|
483
|
+
logger.info("[${cli}-yes] ${cli} is idle, exiting...");
|
|
358
484
|
await exitAgent();
|
|
359
485
|
});
|
|
360
486
|
|
|
361
|
-
// console.log(
|
|
362
|
-
// `[${cli}-yes] Started ${cli} with args: ${[cliCommand, ...cliArgs].join(" ")}`
|
|
363
|
-
// );
|
|
364
487
|
// Message streaming
|
|
365
488
|
|
|
366
|
-
|
|
489
|
+
// Message streaming with stdin and optional FIFO (Linux only)
|
|
490
|
+
|
|
491
|
+
await sflow(fromReadable<Buffer>(process.stdin))
|
|
367
492
|
.map((buffer) => buffer.toString())
|
|
368
|
-
|
|
493
|
+
|
|
494
|
+
.by(function handleTerminateSignals(s) {
|
|
495
|
+
let aborted = false;
|
|
496
|
+
return s.map((chunk) => {
|
|
497
|
+
// handle CTRL+Z and filter it out, as I dont know how to support it yet
|
|
498
|
+
if (!aborted && chunk === "\u001A") {
|
|
499
|
+
return "";
|
|
500
|
+
}
|
|
501
|
+
// handle CTRL+C, when stdin is not ready (no response from agent yet, usually this is when agent loading)
|
|
502
|
+
if (!aborted && !stdinReady.isReady && chunk === "\u0003") {
|
|
503
|
+
logger.error("User aborted: SIGINT");
|
|
504
|
+
shell.kill("SIGINT");
|
|
505
|
+
pendingExitCode.resolve(130); // SIGINT
|
|
506
|
+
aborted = true;
|
|
507
|
+
return chunk; // still pass into agent, but they prob be killed XD
|
|
508
|
+
}
|
|
509
|
+
return chunk; // normal inputs
|
|
510
|
+
});
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// read from FIFO if available, e.g. /tmp/agent-yes-*.stdin, which can be used to send additional input from other processes
|
|
514
|
+
.by((s) => {
|
|
515
|
+
if (!useFifo) return s;
|
|
516
|
+
const fifoResult = createFifoStream(cli);
|
|
517
|
+
if (!fifoResult) return s;
|
|
518
|
+
pendingExitCode.promise.finally(() => fifoResult.cleanup());
|
|
519
|
+
return s.merge(fifoResult.stream);
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
// .map((e) => e.replaceAll('\x1a', '')) // remove ctrl+z from user's input, to prevent bug (but this seems bug)
|
|
369
523
|
// .forEach(e => appendFile('.cache/io.log', "input |" + JSON.stringify(e) + '\n')) // for debugging
|
|
370
|
-
|
|
524
|
+
|
|
525
|
+
.onStart(async function promptOnStart() {
|
|
526
|
+
// send prompt when start
|
|
527
|
+
logger.debug("Sending prompt message: " + JSON.stringify(prompt));
|
|
528
|
+
if (prompt) await sendMessage(prompt);
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// pipe content by shell
|
|
371
532
|
.by({
|
|
372
533
|
writable: new WritableStream<string>({
|
|
373
534
|
write: async (data) => {
|
|
374
535
|
await stdinReady.wait();
|
|
375
|
-
// await idleWaiter.wait(20); // wait for idle for 200ms to avoid messing up claude's input
|
|
376
536
|
shell.write(data);
|
|
377
537
|
},
|
|
378
538
|
}),
|
|
379
539
|
readable: shellOutputStream.readable,
|
|
380
540
|
})
|
|
541
|
+
|
|
381
542
|
.forEach(() => idleWaiter.ping())
|
|
382
543
|
.forEach(() => nextStdout.ready())
|
|
383
|
-
.forEach(async (text) => {
|
|
384
|
-
terminalRender.write(text);
|
|
385
|
-
// todo: .onStatus((msg)=> shell.write(msg))
|
|
386
|
-
if (process.stdin.isTTY) return; // only handle it when stdin is not tty
|
|
387
|
-
if (!text.includes('\u001b[6n')) return; // only asked for cursor position
|
|
388
|
-
// todo: use terminalRender API to get cursor position when new version is available
|
|
389
|
-
// xterm replies CSI row; column R if asked cursor position
|
|
390
|
-
// https://en.wikipedia.org/wiki/ANSI_escape_code#:~:text=citation%20needed%5D-,xterm%20replies,-CSI%20row%C2%A0%3B
|
|
391
|
-
// when agent asking position, respond with row; col
|
|
392
|
-
// const rendered = terminalRender.render();
|
|
393
|
-
const { col, row } = terminalRender.getCursorPosition();
|
|
394
|
-
shell.write(`\u001b[${row};${col}R`); // reply cli when getting cursor position
|
|
395
|
-
await yesLog`cursor|respond position: row=${String(row)}, col=${String(col)}`;
|
|
396
|
-
// const row = rendered.split('\n').length + 1;
|
|
397
|
-
// const col = (rendered.split('\n').slice(-1)[0]?.length || 0) + 1;
|
|
398
|
-
})
|
|
399
544
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
.
|
|
406
|
-
|
|
407
|
-
return
|
|
545
|
+
.forkTo(async function rawLogger(f) {
|
|
546
|
+
if (!rawLogPath) return f.run(); // no stream
|
|
547
|
+
|
|
548
|
+
// try stream the raw log for realtime debugging, including control chars, note: it will be a huge file
|
|
549
|
+
return await mkdir(path.dirname(rawLogPath), { recursive: true })
|
|
550
|
+
.then(() => {
|
|
551
|
+
logger.debug(`[${cli}-yes] raw logs streaming to ${rawLogPath}`);
|
|
552
|
+
return f
|
|
553
|
+
.forEach(async (chars) => {
|
|
554
|
+
await writeFile(rawLogPath, chars, { flag: "a" }).catch(() => null);
|
|
555
|
+
})
|
|
556
|
+
.run();
|
|
408
557
|
})
|
|
409
|
-
.
|
|
410
|
-
|
|
411
|
-
.forEach(async (e, i) => {
|
|
412
|
-
// ready matcher: if matched, mark stdin ready
|
|
413
|
-
if (conf.ready?.some((rx: RegExp) => e.match(rx))) {
|
|
414
|
-
await yesLog`ready |${e}`;
|
|
415
|
-
if (cli === 'gemini' && i <= 80) return; // gemini initial noise, only after many lines
|
|
416
|
-
stdinReady.ready();
|
|
417
|
-
}
|
|
558
|
+
.catch(() => f.run());
|
|
559
|
+
})
|
|
418
560
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
561
|
+
// handle cursor position requests and render terminal output
|
|
562
|
+
.by(function consoleResponder(e) {
|
|
563
|
+
// wait for cli ready and send prompt if provided
|
|
564
|
+
if (cli === "codex") shell.write(`\u001b[1;1R`); // send cursor position response when stdin is not tty
|
|
565
|
+
return e.forEach((text) => {
|
|
566
|
+
// render terminal output for log file
|
|
567
|
+
terminalRender.write(text);
|
|
568
|
+
|
|
569
|
+
// Handle Device Attributes query (DA) - ESC[c or ESC[0c
|
|
570
|
+
// This must be handled regardless of TTY status
|
|
571
|
+
if (text.includes("\u001b[c") || text.includes("\u001b[0c")) {
|
|
572
|
+
// Respond shell with VT100 with Advanced Video Option
|
|
573
|
+
shell.write("\u001b[?1;2c");
|
|
574
|
+
if (verbose) {
|
|
575
|
+
logger.debug("device|respond DA: VT100 with Advanced Video Option");
|
|
424
576
|
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
425
579
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
580
|
+
// todo: .onStatus((msg)=> shell.write(msg))
|
|
581
|
+
if (process.stdin.isTTY) return; // only handle it when stdin is not tty, because tty already handled this
|
|
582
|
+
|
|
583
|
+
if (!text.includes("\u001b[6n")) return; // only asked for cursor position
|
|
584
|
+
// todo: use terminalRender API to get cursor position when new version is available
|
|
585
|
+
// xterm replies CSI row; column R if asked cursor position
|
|
586
|
+
// https://en.wikipedia.org/wiki/ANSI_escape_code#:~:text=citation%20needed%5D-,xterm%20replies,-CSI%20row%C2%A0%3B
|
|
587
|
+
// when agent asking position, respond with row; col
|
|
588
|
+
// const rendered = terminalRender.render();
|
|
589
|
+
const { col, row } = terminalRender.getCursorPosition();
|
|
590
|
+
shell.write(`\u001b[${row};${col}R`); // reply cli when getting cursor position
|
|
591
|
+
logger.debug(`cursor|respond position: row=${String(row)}, col=${String(col)}`);
|
|
592
|
+
// const row = rendered.split('\n').length + 1;
|
|
593
|
+
// const col = (rendered.split('\n').slice(-1)[0]?.length || 0) + 1;
|
|
594
|
+
});
|
|
595
|
+
})
|
|
432
596
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
597
|
+
// auto-response
|
|
598
|
+
.forkTo(function autoResponse(e) {
|
|
599
|
+
return (
|
|
600
|
+
e
|
|
601
|
+
.map((e) => removeControlCharacters(e))
|
|
602
|
+
// .map((e) => e.replaceAll("\r", "")) // remove carriage return
|
|
603
|
+
.by((s) => {
|
|
604
|
+
if (conf.noEOL) return s; // codex use cursor-move csi code insteadof \n to move lines, so the output have no \n at all, this hack prevents stuck on unended line
|
|
605
|
+
return s.lines({ EOL: "NONE" }); // other clis use ink, which is rerendering the block based on \n lines
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
// .forkTo(async function rawLinesLogger(f) {
|
|
609
|
+
// if (!rawLinesLogPath) return f.run(); // no stream
|
|
610
|
+
// // try stream the raw log for realtime debugging, including control chars, note: it will be a huge file
|
|
611
|
+
// return await mkdir(path.dirname(rawLinesLogPath), { recursive: true })
|
|
612
|
+
// .then(() => {
|
|
613
|
+
// logger.debug(`[${cli}-yes] raw lines logs streaming to ${rawLinesLogPath}`);
|
|
614
|
+
// return f
|
|
615
|
+
// .forEach(async (chars, i) => {
|
|
616
|
+
// await writeFile(rawLinesLogPath, `L${i}|` + chars, { flag: "a" }).catch(() => null);
|
|
617
|
+
// })
|
|
618
|
+
// .run();
|
|
619
|
+
// })
|
|
620
|
+
// .catch(() => f.run());
|
|
621
|
+
// })
|
|
622
|
+
|
|
623
|
+
// Generic auto-response handler driven by CLI_CONFIGURES
|
|
624
|
+
.forEach(async function autoResponseOnChunk(e, i) {
|
|
625
|
+
logger.debug(`stdout|${e}`);
|
|
626
|
+
// ready matcher: if matched, mark stdin ready
|
|
627
|
+
if (conf.ready?.some((rx: RegExp) => e.match(rx))) {
|
|
628
|
+
logger.debug(`ready |${e}`);
|
|
629
|
+
if (cli === "gemini" && i <= 80) return; // gemini initial noise, only after many lines
|
|
630
|
+
stdinReady.ready();
|
|
631
|
+
stdinFirstReady.ready();
|
|
439
632
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
633
|
+
// enter matchers: send Enter when any enter regex matches
|
|
634
|
+
|
|
635
|
+
if (conf.enter?.some((rx: RegExp) => e.match(rx))) {
|
|
636
|
+
logger.debug(`enter |${e}`);
|
|
637
|
+
return await sendEnter(400); // wait for idle for a short while and then send Enter
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// typingRespond matcher: if matched, send the specified message
|
|
641
|
+
const typingResponded = await sflow(Object.entries(conf.typingRespond ?? {}))
|
|
642
|
+
.filter(([_sendString, onThePatterns]) => onThePatterns.some((rx) => e.match(rx)))
|
|
643
|
+
.map(async ([sendString]) => await sendMessage(sendString, { waitForReady: false }))
|
|
644
|
+
.toCount();
|
|
645
|
+
if (typingResponded) return;
|
|
646
|
+
|
|
647
|
+
// fatal matchers: set isFatal flag when matched
|
|
648
|
+
if (conf.fatal?.some((rx: RegExp) => e.match(rx))) {
|
|
649
|
+
logger.debug(`fatal |${e}`);
|
|
650
|
+
isFatal = true;
|
|
651
|
+
await exitAgent();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// restartWithoutContinueArg matchers: set flag to restart without continue args
|
|
655
|
+
if (conf.restartWithoutContinueArg?.some((rx: RegExp) => e.match(rx))) {
|
|
656
|
+
await logger.debug(`restart-without-continue|${e}`);
|
|
657
|
+
shouldRestartWithoutContinue = true;
|
|
658
|
+
isFatal = true; // also set fatal to trigger exit
|
|
659
|
+
await exitAgent();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// session ID capture for codex
|
|
663
|
+
if (cli === "codex") {
|
|
664
|
+
const sessionId = extractSessionId(e);
|
|
665
|
+
if (sessionId) {
|
|
666
|
+
await logger.debug(`session|captured session ID: ${sessionId}`);
|
|
667
|
+
await storeSessionForCwd(workingDir, sessionId);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
.run()
|
|
672
|
+
);
|
|
673
|
+
})
|
|
674
|
+
.by((s) => (removeControlCharactersFromStdout ? s.map((e) => removeControlCharacters(e)) : s))
|
|
675
|
+
|
|
676
|
+
// terminate whole stream when shell did exited (already crash-handled)
|
|
677
|
+
.by(
|
|
678
|
+
new TransformStream({
|
|
679
|
+
start: function terminator(ctrl) {
|
|
680
|
+
pendingExitCode.promise.then(() => ctrl.terminate());
|
|
681
|
+
},
|
|
682
|
+
transform: (e, ctrl) => ctrl.enqueue(e),
|
|
683
|
+
flush: (ctrl) => ctrl.terminate(),
|
|
684
|
+
}),
|
|
446
685
|
)
|
|
447
|
-
.to(fromWritable(process.stdout))
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const exitCode = await pendingExitCode.promise; // wait for the shell to exit
|
|
455
|
-
console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
456
|
-
|
|
457
|
-
// Update task status and release lock
|
|
458
|
-
if (queue && shouldUseLock(workingDir)) {
|
|
459
|
-
await updateCurrentTaskStatus(
|
|
460
|
-
exitCode === 0 ? 'completed' : 'failed',
|
|
461
|
-
).catch(() => null);
|
|
462
|
-
await releaseLock().catch(() => null);
|
|
686
|
+
.to(fromWritable(process.stdout));
|
|
687
|
+
|
|
688
|
+
if (logPath) {
|
|
689
|
+
await mkdir(path.dirname(logPath), { recursive: true }).catch(() => null);
|
|
690
|
+
await writeFile(logPath, terminalRender.render()).catch(() => null);
|
|
691
|
+
logger.info(`[${cli}-yes] Full logs saved to ${logPath}`);
|
|
463
692
|
}
|
|
464
693
|
|
|
694
|
+
// and then get its exitcode
|
|
695
|
+
const exitCode = await pendingExitCode.promise;
|
|
696
|
+
logger.info(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
697
|
+
|
|
698
|
+
// Update task status.writable release lock
|
|
699
|
+
await outputWriter.close();
|
|
700
|
+
|
|
701
|
+
// deprecated logFile option, we have logPath now, but keep for backward compatibility
|
|
465
702
|
if (logFile) {
|
|
466
|
-
verbose
|
|
703
|
+
if (verbose) logger.info(`[${cli}-yes] Writing rendered logs to ${logFile}`);
|
|
467
704
|
const logFilePath = path.resolve(logFile);
|
|
468
|
-
await mkdir(path.dirname(logFilePath), { recursive: true }).catch(
|
|
469
|
-
() => null,
|
|
470
|
-
);
|
|
705
|
+
await mkdir(path.dirname(logFilePath), { recursive: true }).catch(() => null);
|
|
471
706
|
await writeFile(logFilePath, terminalRender.render());
|
|
472
707
|
}
|
|
473
708
|
|
|
@@ -476,19 +711,21 @@ export default async function cliYes({
|
|
|
476
711
|
async function sendEnter(waitms = 1000) {
|
|
477
712
|
// wait for idle for a bit to let agent cli finish rendering
|
|
478
713
|
const st = Date.now();
|
|
479
|
-
await idleWaiter.wait(waitms);
|
|
714
|
+
await idleWaiter.wait(waitms); // wait for idle a while
|
|
480
715
|
const et = Date.now();
|
|
481
716
|
// process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
|
|
482
|
-
|
|
717
|
+
logger.debug(`sendEn| idleWaiter.wait(${String(waitms)}) took ${String(et - st)}ms`);
|
|
483
718
|
nextStdout.unready();
|
|
484
|
-
|
|
719
|
+
// send the enter key
|
|
720
|
+
shell.write("\r");
|
|
721
|
+
|
|
485
722
|
// retry once if not received any output in 1 second after sending Enter
|
|
486
723
|
await Promise.race([
|
|
487
724
|
nextStdout.wait(),
|
|
488
725
|
new Promise<void>((resolve) =>
|
|
489
726
|
setTimeout(() => {
|
|
490
727
|
if (!nextStdout.ready) {
|
|
491
|
-
shell.write(
|
|
728
|
+
shell.write("\r");
|
|
492
729
|
}
|
|
493
730
|
resolve();
|
|
494
731
|
}, 1000),
|
|
@@ -496,25 +733,25 @@ export default async function cliYes({
|
|
|
496
733
|
]);
|
|
497
734
|
}
|
|
498
735
|
|
|
499
|
-
async function sendMessage(message: string) {
|
|
500
|
-
await stdinReady.wait();
|
|
736
|
+
async function sendMessage(message: string, { waitForReady = true } = {}) {
|
|
737
|
+
if (waitForReady) await stdinReady.wait();
|
|
501
738
|
// show in-place message: write msg and move cursor back start
|
|
502
|
-
|
|
739
|
+
logger.debug(`send |${message}`);
|
|
503
740
|
nextStdout.unready();
|
|
504
741
|
shell.write(message);
|
|
505
742
|
idleWaiter.ping(); // just sent a message, wait for echo
|
|
506
|
-
|
|
743
|
+
logger.debug(`waiting next stdout|${message}`);
|
|
507
744
|
await nextStdout.wait();
|
|
508
|
-
|
|
745
|
+
logger.debug(`sending enter`);
|
|
509
746
|
await sendEnter(1000);
|
|
510
|
-
|
|
747
|
+
logger.debug(`sent enter`);
|
|
511
748
|
}
|
|
512
749
|
|
|
513
750
|
async function exitAgent() {
|
|
514
751
|
robust = false; // disable robust to avoid auto restart
|
|
515
752
|
|
|
516
753
|
// send exit command to the shell, must sleep a bit to avoid claude treat it as pasted input
|
|
517
|
-
|
|
754
|
+
for (const cmd of cliConf.exitCommands ?? ["/exit"]) await sendMessage(cmd);
|
|
518
755
|
|
|
519
756
|
// wait for shell to exit or kill it with a timeout
|
|
520
757
|
let exited = false;
|
|
@@ -542,3 +779,7 @@ export default async function cliYes({
|
|
|
542
779
|
};
|
|
543
780
|
}
|
|
544
781
|
}
|
|
782
|
+
|
|
783
|
+
function sleep(ms: number) {
|
|
784
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
785
|
+
}
|