agent-yes 1.60.1 → 1.60.3
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/dist/SUPPORTED_CLIS-BHDvBHvX.js +1952 -0
- package/dist/{agent-yes.config-CrgoI5sj.js → agent-yes.config-B-sre0vp.js} +2 -2
- package/dist/agent-yes.config-XmUcKFde.js +4 -0
- package/dist/cli.js +13 -4505
- package/dist/index.js +2 -2
- package/dist/logger-CY9ormLF.js +16 -0
- package/package.json +10 -10
- package/ts/installEnv.ts +2 -1
- package/ts/rustBinary.ts +3 -3
- package/dist/SUPPORTED_CLIS-nJ4Lx02F.js +0 -10431
- package/dist/agent-yes.config-BZYHa6lN.js +0 -4
- package/dist/logger-DH1Rx9HI.js +0 -11293
|
@@ -0,0 +1,1952 @@
|
|
|
1
|
+
import { t as logger } from "./logger-CY9ormLF.js";
|
|
2
|
+
import { arch, platform } from "process";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync } from "fs";
|
|
5
|
+
import path, { dirname, join } from "path";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path$1 from "node:path";
|
|
9
|
+
import winston from "winston";
|
|
10
|
+
import { execaCommandSync, parseCommandString } from "execa";
|
|
11
|
+
import { fromWritable } from "from-node-stream";
|
|
12
|
+
import { appendFile, mkdir as mkdir$1, readFile as readFile$1, readdir, rename, writeFile as writeFile$1 } from "fs/promises";
|
|
13
|
+
import DIE from "phpdie";
|
|
14
|
+
import sflow from "sflow";
|
|
15
|
+
import { TerminalRenderStream } from "terminal-render";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { lock } from "proper-lockfile";
|
|
18
|
+
import { execSync as execSync$1 } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
|
|
21
|
+
//#region \0rolldown/runtime.js
|
|
22
|
+
var __defProp = Object.defineProperty;
|
|
23
|
+
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
24
|
+
var __exportAll = (all, no_symbols) => {
|
|
25
|
+
let target = {};
|
|
26
|
+
for (var name in all) {
|
|
27
|
+
__defProp(target, name, {
|
|
28
|
+
get: all[name],
|
|
29
|
+
enumerable: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (!no_symbols) {
|
|
33
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ts/resume/codexSessionManager.ts
|
|
40
|
+
const getSessionsFile = () => process.env.CLI_YES_TEST_HOME ? path.join(process.env.CLI_YES_TEST_HOME, ".config", "agent-yes", "codex-sessions.json") : path.join(homedir(), ".config", "agent-yes", "codex-sessions.json");
|
|
41
|
+
const getCodexSessionsDir = () => process.env.CLI_YES_TEST_HOME ? path.join(process.env.CLI_YES_TEST_HOME, ".codex", "sessions") : path.join(homedir(), ".codex", "sessions");
|
|
42
|
+
/**
|
|
43
|
+
* Load the session map from the config file
|
|
44
|
+
*/
|
|
45
|
+
async function loadSessionMap() {
|
|
46
|
+
try {
|
|
47
|
+
const content = await readFile$1(getSessionsFile(), "utf-8");
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save the session map to the config file
|
|
55
|
+
*/
|
|
56
|
+
async function saveSessionMap(sessionMap) {
|
|
57
|
+
try {
|
|
58
|
+
const sessionsFile = getSessionsFile();
|
|
59
|
+
await mkdir$1(path.dirname(sessionsFile), { recursive: true });
|
|
60
|
+
await writeFile$1(sessionsFile, JSON.stringify(sessionMap, null, 2));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn("Failed to save codex session map:", error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Store a session ID for a specific working directory
|
|
67
|
+
*/
|
|
68
|
+
async function storeSessionForCwd(cwd, sessionId) {
|
|
69
|
+
const sessionMap = await loadSessionMap();
|
|
70
|
+
sessionMap[cwd] = {
|
|
71
|
+
sessionId,
|
|
72
|
+
lastUsed: (/* @__PURE__ */ new Date()).toISOString()
|
|
73
|
+
};
|
|
74
|
+
await saveSessionMap(sessionMap);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse a codex session file to extract session metadata
|
|
78
|
+
*/
|
|
79
|
+
async function parseCodexSessionFile(filePath) {
|
|
80
|
+
try {
|
|
81
|
+
const lines = (await readFile$1(filePath, "utf-8")).trim().split("\n");
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (!line.trim()) continue;
|
|
84
|
+
const data = JSON.parse(line);
|
|
85
|
+
if (data.type === "session_meta" && data.payload) {
|
|
86
|
+
const payload = data.payload;
|
|
87
|
+
return {
|
|
88
|
+
id: payload.id,
|
|
89
|
+
timestamp: payload.timestamp || data.timestamp,
|
|
90
|
+
cwd: payload.cwd,
|
|
91
|
+
filePath,
|
|
92
|
+
git: payload.git
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get all codex sessions from the .codex/sessions directory
|
|
103
|
+
*/
|
|
104
|
+
async function getAllCodexSessions() {
|
|
105
|
+
const sessions = [];
|
|
106
|
+
const codexSessionsDir = getCodexSessionsDir();
|
|
107
|
+
try {
|
|
108
|
+
const years = await readdir(codexSessionsDir);
|
|
109
|
+
for (const year of years) {
|
|
110
|
+
const yearPath = path.join(codexSessionsDir, year);
|
|
111
|
+
try {
|
|
112
|
+
const months = await readdir(yearPath);
|
|
113
|
+
for (const month of months) {
|
|
114
|
+
const monthPath = path.join(yearPath, month);
|
|
115
|
+
try {
|
|
116
|
+
const days = await readdir(monthPath);
|
|
117
|
+
for (const day of days) {
|
|
118
|
+
const dayPath = path.join(monthPath, day);
|
|
119
|
+
try {
|
|
120
|
+
const files = await readdir(dayPath);
|
|
121
|
+
for (const file of files) if (file.endsWith(".jsonl")) {
|
|
122
|
+
const session = await parseCodexSessionFile(path.join(dayPath, file));
|
|
123
|
+
if (session) sessions.push(session);
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
return sessions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the most recent session for a specific working directory from actual codex files
|
|
138
|
+
*/
|
|
139
|
+
async function getMostRecentCodexSessionForCwd(targetCwd) {
|
|
140
|
+
return (await getAllCodexSessions()).filter((session) => session.cwd === targetCwd)[0] || null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the last session ID for a specific working directory
|
|
144
|
+
* Now checks actual codex session files first, falls back to stored mapping
|
|
145
|
+
*/
|
|
146
|
+
async function getSessionForCwd(cwd) {
|
|
147
|
+
const recentSession = await getMostRecentCodexSessionForCwd(cwd);
|
|
148
|
+
if (recentSession) return recentSession.id;
|
|
149
|
+
return (await loadSessionMap())[cwd]?.sessionId || null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Extract session ID from codex output
|
|
153
|
+
* Session IDs are UUIDs in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
154
|
+
*/
|
|
155
|
+
function extractSessionId(output) {
|
|
156
|
+
const match = output.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
|
|
157
|
+
return match ? match[0] : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region ts/pty.ts
|
|
162
|
+
async function getPty() {
|
|
163
|
+
return globalThis.Bun ? await import("bun-pty").catch((error) => {
|
|
164
|
+
logger.error("Failed to load bun-pty:", error);
|
|
165
|
+
throw error;
|
|
166
|
+
}) : await import("node-pty").catch((error) => {
|
|
167
|
+
logger.error("Failed to load node-pty:", error);
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const pty = await getPty();
|
|
172
|
+
const ptyPackage = globalThis.Bun ? "bun-pty" : "node-pty";
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region ts/removeControlCharacters.ts
|
|
176
|
+
function removeControlCharacters(str) {
|
|
177
|
+
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region ts/runningLock.ts
|
|
182
|
+
const getLockDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
|
|
183
|
+
const getLockFile = () => path.join(getLockDir(), "running.lock.json");
|
|
184
|
+
const MAX_RETRIES = 5;
|
|
185
|
+
const RETRY_DELAYS = [
|
|
186
|
+
50,
|
|
187
|
+
100,
|
|
188
|
+
200,
|
|
189
|
+
400,
|
|
190
|
+
800
|
|
191
|
+
];
|
|
192
|
+
const POLL_INTERVAL = 2e3;
|
|
193
|
+
/**
|
|
194
|
+
* Check if a process is running
|
|
195
|
+
*/
|
|
196
|
+
function isProcessRunning(pid) {
|
|
197
|
+
try {
|
|
198
|
+
process.kill(pid, 0);
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get git repository root for a directory
|
|
206
|
+
*/
|
|
207
|
+
function getGitRoot(cwd) {
|
|
208
|
+
try {
|
|
209
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
210
|
+
cwd,
|
|
211
|
+
encoding: "utf8",
|
|
212
|
+
stdio: [
|
|
213
|
+
"pipe",
|
|
214
|
+
"pipe",
|
|
215
|
+
"ignore"
|
|
216
|
+
]
|
|
217
|
+
}).trim();
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Check if directory is in a git repository
|
|
224
|
+
*/
|
|
225
|
+
function isGitRepo(cwd) {
|
|
226
|
+
try {
|
|
227
|
+
return getGitRoot(cwd) !== null;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Resolve path to real path (handling symlinks)
|
|
234
|
+
*/
|
|
235
|
+
function resolveRealPath(p) {
|
|
236
|
+
try {
|
|
237
|
+
return path.resolve(p);
|
|
238
|
+
} catch {
|
|
239
|
+
return p;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Sleep for a given number of milliseconds
|
|
244
|
+
*/
|
|
245
|
+
function sleep$1(ms) {
|
|
246
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Read lock file with retry logic and stale lock cleanup
|
|
250
|
+
*/
|
|
251
|
+
async function readLockFile() {
|
|
252
|
+
try {
|
|
253
|
+
const lockDir = getLockDir();
|
|
254
|
+
const lockFilePath = getLockFile();
|
|
255
|
+
await mkdir$1(lockDir, { recursive: true });
|
|
256
|
+
if (!existsSync(lockFilePath)) return { tasks: [] };
|
|
257
|
+
const content = await readFile$1(lockFilePath, "utf8");
|
|
258
|
+
const lockFile = JSON.parse(content);
|
|
259
|
+
lockFile.tasks = lockFile.tasks.filter((task) => {
|
|
260
|
+
if (isProcessRunning(task.pid)) return true;
|
|
261
|
+
return false;
|
|
262
|
+
});
|
|
263
|
+
return lockFile;
|
|
264
|
+
} catch {
|
|
265
|
+
return { tasks: [] };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Write lock file atomically with retry logic
|
|
270
|
+
*/
|
|
271
|
+
async function writeLockFile(lockFile, retryCount = 0) {
|
|
272
|
+
try {
|
|
273
|
+
const lockDir = getLockDir();
|
|
274
|
+
const lockFilePath = getLockFile();
|
|
275
|
+
await mkdir$1(lockDir, { recursive: true });
|
|
276
|
+
const tempFile = `${lockFilePath}.tmp.${process.pid}`;
|
|
277
|
+
await writeFile$1(tempFile, JSON.stringify(lockFile, null, 2), "utf8");
|
|
278
|
+
await rename(tempFile, lockFilePath);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (retryCount < MAX_RETRIES) {
|
|
281
|
+
await sleep$1(RETRY_DELAYS[retryCount] || 800);
|
|
282
|
+
return writeLockFile(lockFile, retryCount + 1);
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Check if lock exists for the current working directory
|
|
289
|
+
*/
|
|
290
|
+
async function checkLock(cwd, _prompt) {
|
|
291
|
+
const resolvedCwd = resolveRealPath(cwd);
|
|
292
|
+
const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
|
|
293
|
+
const lockKey = gitRoot || resolvedCwd;
|
|
294
|
+
const blockingTasks = (await readLockFile()).tasks.filter((task) => {
|
|
295
|
+
if (!isProcessRunning(task.pid)) return false;
|
|
296
|
+
if (task.status !== "running") return false;
|
|
297
|
+
if (gitRoot && task.gitRoot) return task.gitRoot === gitRoot;
|
|
298
|
+
else return task.cwd === lockKey;
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
isLocked: blockingTasks.length > 0,
|
|
302
|
+
blockingTasks,
|
|
303
|
+
lockKey
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Add a task to the lock file
|
|
308
|
+
*/
|
|
309
|
+
async function addTask(task) {
|
|
310
|
+
const lockFile = await readLockFile();
|
|
311
|
+
lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== task.pid);
|
|
312
|
+
lockFile.tasks.push(task);
|
|
313
|
+
await writeLockFile(lockFile);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Update task status
|
|
317
|
+
*/
|
|
318
|
+
async function updateTaskStatus(pid, status) {
|
|
319
|
+
const lockFile = await readLockFile();
|
|
320
|
+
const task = lockFile.tasks.find((t) => t.pid === pid);
|
|
321
|
+
if (task) {
|
|
322
|
+
task.status = status;
|
|
323
|
+
await writeLockFile(lockFile);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Remove a task from the lock file
|
|
328
|
+
*/
|
|
329
|
+
async function removeTask(pid) {
|
|
330
|
+
const lockFile = await readLockFile();
|
|
331
|
+
lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== pid);
|
|
332
|
+
await writeLockFile(lockFile);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Wait for lock to be released
|
|
336
|
+
*/
|
|
337
|
+
async function waitForUnlock(blockingTasks, currentTask) {
|
|
338
|
+
const blockingTask = blockingTasks[0];
|
|
339
|
+
if (!blockingTask) return;
|
|
340
|
+
console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
|
|
341
|
+
console.log(` Press 'b' to bypass queue, 'k' to kill previous instance`);
|
|
342
|
+
await addTask({
|
|
343
|
+
...currentTask,
|
|
344
|
+
status: "queued"
|
|
345
|
+
});
|
|
346
|
+
const stdin = process.stdin;
|
|
347
|
+
const wasRaw = stdin.isRaw;
|
|
348
|
+
stdin.setRawMode?.(true);
|
|
349
|
+
stdin.resume();
|
|
350
|
+
let bypassed = false;
|
|
351
|
+
let killed = false;
|
|
352
|
+
const keyHandler = (key) => {
|
|
353
|
+
const char = key.toString();
|
|
354
|
+
if (char === "b" || char === "B") {
|
|
355
|
+
console.log("\n⚡ Bypassing queue...");
|
|
356
|
+
bypassed = true;
|
|
357
|
+
} else if (char === "k" || char === "K") {
|
|
358
|
+
console.log("\n🔪 Killing previous instance...");
|
|
359
|
+
killed = true;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
stdin.on("data", keyHandler);
|
|
363
|
+
let dots = 0;
|
|
364
|
+
while (true) {
|
|
365
|
+
if (bypassed) {
|
|
366
|
+
await updateTaskStatus(currentTask.pid, "running");
|
|
367
|
+
console.log("✓ Queue bypassed, starting task...");
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (killed && blockingTask) {
|
|
371
|
+
try {
|
|
372
|
+
process.kill(blockingTask.pid, "SIGTERM");
|
|
373
|
+
console.log(`✓ Killed process ${blockingTask.pid}`);
|
|
374
|
+
await sleep$1(1e3);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.log(`⚠️ Could not kill process ${blockingTask.pid}: ${err}`);
|
|
377
|
+
}
|
|
378
|
+
killed = false;
|
|
379
|
+
}
|
|
380
|
+
await sleep$1(POLL_INTERVAL);
|
|
381
|
+
if (!(await checkLock(currentTask.cwd, currentTask.task)).isLocked) {
|
|
382
|
+
await updateTaskStatus(currentTask.pid, "running");
|
|
383
|
+
console.log(`\n✓ Lock released, starting task...`);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
dots = (dots + 1) % 4;
|
|
387
|
+
process.stdout.write(`\r⏳ Queueing${".".repeat(dots)}${" ".repeat(3 - dots)}`);
|
|
388
|
+
}
|
|
389
|
+
stdin.off("data", keyHandler);
|
|
390
|
+
stdin.setRawMode?.(wasRaw);
|
|
391
|
+
if (!wasRaw) stdin.pause();
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Acquire lock or wait if locked
|
|
395
|
+
*/
|
|
396
|
+
async function acquireLock(cwd, prompt = "no prompt provided") {
|
|
397
|
+
const resolvedCwd = resolveRealPath(cwd);
|
|
398
|
+
const task = {
|
|
399
|
+
cwd: resolvedCwd,
|
|
400
|
+
gitRoot: (isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null) || void 0,
|
|
401
|
+
task: prompt.substring(0, 100),
|
|
402
|
+
pid: process.pid,
|
|
403
|
+
status: "running",
|
|
404
|
+
startedAt: Date.now(),
|
|
405
|
+
lockedAt: Date.now()
|
|
406
|
+
};
|
|
407
|
+
const lockCheck = await checkLock(resolvedCwd, prompt);
|
|
408
|
+
if (lockCheck.isLocked) await waitForUnlock(lockCheck.blockingTasks, task);
|
|
409
|
+
else await addTask(task);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Release lock for current process
|
|
413
|
+
*/
|
|
414
|
+
async function releaseLock(pid = process.pid) {
|
|
415
|
+
await removeTask(pid);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Check if we should use locking for this directory
|
|
419
|
+
* Only use locking if we're in a git repository
|
|
420
|
+
*/
|
|
421
|
+
function shouldUseLock(_cwd) {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region ts/JsonlStore.ts
|
|
427
|
+
/**
|
|
428
|
+
* A lightweight NeDB-style JSONL persistence layer.
|
|
429
|
+
*
|
|
430
|
+
* - Append-only writes (one JSON object per line)
|
|
431
|
+
* - Same `_id` → last line wins (fields merged)
|
|
432
|
+
* - `$$deleted` lines act as tombstones
|
|
433
|
+
* - Crash recovery: skip partial last line, recover from temp file
|
|
434
|
+
* - Multi-process safe via proper-lockfile (reads don't need lock)
|
|
435
|
+
* - Compact on close: deduplicates into clean file via atomic rename
|
|
436
|
+
*/
|
|
437
|
+
var JsonlStore = class {
|
|
438
|
+
filePath;
|
|
439
|
+
tempPath;
|
|
440
|
+
lockPath;
|
|
441
|
+
docs = /* @__PURE__ */ new Map();
|
|
442
|
+
constructor(filePath) {
|
|
443
|
+
this.filePath = filePath;
|
|
444
|
+
this.tempPath = filePath + "~";
|
|
445
|
+
this.lockPath = path.dirname(filePath);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Load all records from the JSONL file. No lock needed.
|
|
449
|
+
* Handles crash recovery: partial last line skipped, temp file recovery.
|
|
450
|
+
*/
|
|
451
|
+
async load() {
|
|
452
|
+
await mkdir$1(path.dirname(this.filePath), { recursive: true });
|
|
453
|
+
if (!existsSync(this.filePath) && existsSync(this.tempPath)) {
|
|
454
|
+
logger.debug("[JsonlStore] Recovering from temp file");
|
|
455
|
+
await rename(this.tempPath, this.filePath);
|
|
456
|
+
}
|
|
457
|
+
this.docs = /* @__PURE__ */ new Map();
|
|
458
|
+
let raw = "";
|
|
459
|
+
try {
|
|
460
|
+
raw = await readFile$1(this.filePath, "utf-8");
|
|
461
|
+
} catch (err) {
|
|
462
|
+
if (err.code === "ENOENT") return this.docs;
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
const lines = raw.split("\n");
|
|
466
|
+
let corruptCount = 0;
|
|
467
|
+
for (const line of lines) {
|
|
468
|
+
const trimmed = line.trim();
|
|
469
|
+
if (!trimmed) continue;
|
|
470
|
+
try {
|
|
471
|
+
const doc = JSON.parse(trimmed);
|
|
472
|
+
if (!doc._id) continue;
|
|
473
|
+
if (doc.$$deleted) this.docs.delete(doc._id);
|
|
474
|
+
else {
|
|
475
|
+
const existing = this.docs.get(doc._id);
|
|
476
|
+
if (existing) this.docs.set(doc._id, {
|
|
477
|
+
...existing,
|
|
478
|
+
...doc
|
|
479
|
+
});
|
|
480
|
+
else this.docs.set(doc._id, doc);
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
corruptCount++;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (corruptCount > 0) logger.debug(`[JsonlStore] Skipped ${corruptCount} corrupt line(s) in ${this.filePath}`);
|
|
487
|
+
return this.docs;
|
|
488
|
+
}
|
|
489
|
+
/** Get all live documents. */
|
|
490
|
+
getAll() {
|
|
491
|
+
return Array.from(this.docs.values());
|
|
492
|
+
}
|
|
493
|
+
/** Find a document by _id. */
|
|
494
|
+
getById(id) {
|
|
495
|
+
return this.docs.get(id);
|
|
496
|
+
}
|
|
497
|
+
/** Find documents matching a predicate. */
|
|
498
|
+
find(predicate) {
|
|
499
|
+
return this.getAll().filter(predicate);
|
|
500
|
+
}
|
|
501
|
+
/** Find first document matching a predicate. */
|
|
502
|
+
findOne(predicate) {
|
|
503
|
+
for (const doc of this.docs.values()) if (predicate(doc)) return doc;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Append a new document. Acquires lock.
|
|
507
|
+
* If no _id is provided, one is generated.
|
|
508
|
+
*/
|
|
509
|
+
async append(doc) {
|
|
510
|
+
const id = doc._id || generateId();
|
|
511
|
+
const { _id: _, ...rest } = doc;
|
|
512
|
+
const fullDoc = {
|
|
513
|
+
_id: id,
|
|
514
|
+
...rest
|
|
515
|
+
};
|
|
516
|
+
return await this.withLock(async () => {
|
|
517
|
+
await appendFile(this.filePath, JSON.stringify(fullDoc) + "\n");
|
|
518
|
+
const existing = this.docs.get(fullDoc._id);
|
|
519
|
+
if (existing) this.docs.set(fullDoc._id, {
|
|
520
|
+
...existing,
|
|
521
|
+
...fullDoc
|
|
522
|
+
});
|
|
523
|
+
else this.docs.set(fullDoc._id, fullDoc);
|
|
524
|
+
return fullDoc;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Update a document by _id. Appends a merge line. Acquires lock.
|
|
529
|
+
*/
|
|
530
|
+
async updateById(id, patch) {
|
|
531
|
+
await this.withLock(async () => {
|
|
532
|
+
const line = {
|
|
533
|
+
_id: id,
|
|
534
|
+
...patch
|
|
535
|
+
};
|
|
536
|
+
await appendFile(this.filePath, JSON.stringify(line) + "\n");
|
|
537
|
+
const existing = this.docs.get(id);
|
|
538
|
+
if (existing) this.docs.set(id, {
|
|
539
|
+
...existing,
|
|
540
|
+
...patch
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Delete a document by _id. Appends a tombstone. Acquires lock.
|
|
546
|
+
*/
|
|
547
|
+
async deleteById(id) {
|
|
548
|
+
await this.withLock(async () => {
|
|
549
|
+
const tombstone = {
|
|
550
|
+
_id: id,
|
|
551
|
+
$$deleted: true
|
|
552
|
+
};
|
|
553
|
+
await appendFile(this.filePath, JSON.stringify(tombstone) + "\n");
|
|
554
|
+
this.docs.delete(id);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Compact the file: deduplicate entries, remove tombstones.
|
|
559
|
+
* Writes to temp file, fsyncs, then atomic renames.
|
|
560
|
+
* Acquires lock.
|
|
561
|
+
*/
|
|
562
|
+
async compact() {
|
|
563
|
+
await this.withLock(async () => {
|
|
564
|
+
const lines = Array.from(this.docs.values()).map((doc) => {
|
|
565
|
+
const { _id, $$deleted, ...rest } = doc;
|
|
566
|
+
return JSON.stringify({
|
|
567
|
+
_id,
|
|
568
|
+
...rest
|
|
569
|
+
});
|
|
570
|
+
}).join("\n");
|
|
571
|
+
const content = lines ? lines + "\n" : "";
|
|
572
|
+
await writeFile$1(this.tempPath, content);
|
|
573
|
+
const fd = openSync(this.tempPath, "r");
|
|
574
|
+
fsyncSync(fd);
|
|
575
|
+
closeSync(fd);
|
|
576
|
+
await rename(this.tempPath, this.filePath);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async withLock(fn) {
|
|
580
|
+
const dir = path.dirname(this.filePath);
|
|
581
|
+
let release;
|
|
582
|
+
try {
|
|
583
|
+
release = await lock(dir, {
|
|
584
|
+
lockfilePath: this.filePath + ".lock",
|
|
585
|
+
retries: {
|
|
586
|
+
retries: 5,
|
|
587
|
+
minTimeout: 50,
|
|
588
|
+
maxTimeout: 500
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return await fn();
|
|
592
|
+
} finally {
|
|
593
|
+
if (release) await release();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
let idCounter = 0;
|
|
598
|
+
function generateId() {
|
|
599
|
+
return Date.now().toString(36) + (idCounter++).toString(36) + Math.random().toString(36).slice(2, 6);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region ts/pidStore.ts
|
|
604
|
+
var PidStore = class PidStore {
|
|
605
|
+
storeDir;
|
|
606
|
+
store;
|
|
607
|
+
constructor(workingDir) {
|
|
608
|
+
this.storeDir = path.resolve(workingDir, ".agent-yes");
|
|
609
|
+
this.store = new JsonlStore(path.join(this.storeDir, "pid-records.jsonl"));
|
|
610
|
+
}
|
|
611
|
+
async init() {
|
|
612
|
+
try {
|
|
613
|
+
await this.ensureGitignore();
|
|
614
|
+
await this.store.load();
|
|
615
|
+
await this.cleanStaleRecords();
|
|
616
|
+
} catch (error) {
|
|
617
|
+
logger.warn("[pidStore] Failed to initialize:", error);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async registerProcess({ pid, cli, args, prompt, cwd }) {
|
|
621
|
+
const now = Date.now();
|
|
622
|
+
const record = {
|
|
623
|
+
pid,
|
|
624
|
+
cli,
|
|
625
|
+
args: JSON.stringify(args),
|
|
626
|
+
prompt,
|
|
627
|
+
cwd,
|
|
628
|
+
logFile: path.resolve(this.getLogDir(), `${pid}.log`),
|
|
629
|
+
fifoFile: this.getFifoPath(pid),
|
|
630
|
+
status: "active",
|
|
631
|
+
exitReason: "",
|
|
632
|
+
startedAt: now
|
|
633
|
+
};
|
|
634
|
+
const existing = this.store.findOne((doc) => doc.pid === pid);
|
|
635
|
+
if (existing) await this.store.updateById(existing._id, record);
|
|
636
|
+
else await this.store.append(record);
|
|
637
|
+
const result = this.store.findOne((doc) => doc.pid === pid);
|
|
638
|
+
if (!result) {
|
|
639
|
+
const allRecords = this.store.getAll();
|
|
640
|
+
logger.error(`[pidStore] Failed to find record for PID ${pid}. All records:`, allRecords);
|
|
641
|
+
throw new Error(`Failed to register process ${pid}`);
|
|
642
|
+
}
|
|
643
|
+
logger.debug(`[pidStore] Registered process ${pid}`);
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
async updateStatus(pid, status, extra) {
|
|
647
|
+
const existing = this.store.findOne((doc) => doc.pid === pid);
|
|
648
|
+
if (!existing) return;
|
|
649
|
+
const patch = { status };
|
|
650
|
+
if (extra?.exitReason !== void 0) patch.exitReason = extra.exitReason;
|
|
651
|
+
if (extra?.exitCode !== void 0) patch.exitCode = extra.exitCode;
|
|
652
|
+
await this.store.updateById(existing._id, patch);
|
|
653
|
+
logger.debug(`[pidStore] Updated process ${pid} status=${status}`);
|
|
654
|
+
}
|
|
655
|
+
getAllRecords() {
|
|
656
|
+
return this.store.getAll();
|
|
657
|
+
}
|
|
658
|
+
getLogDir() {
|
|
659
|
+
return path.resolve(this.storeDir, "logs");
|
|
660
|
+
}
|
|
661
|
+
getFifoPath(pid) {
|
|
662
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\agent-yes-${pid}`;
|
|
663
|
+
else return path.resolve(this.storeDir, "fifo", `${pid}.stdin`);
|
|
664
|
+
}
|
|
665
|
+
async cleanStaleRecords() {
|
|
666
|
+
const activeRecords = this.store.find((r) => r.status !== "exited");
|
|
667
|
+
for (const record of activeRecords) if (!this.isProcessAlive(record.pid)) {
|
|
668
|
+
await this.store.updateById(record._id, {
|
|
669
|
+
status: "exited",
|
|
670
|
+
exitReason: "stale-cleanup"
|
|
671
|
+
});
|
|
672
|
+
logger.debug(`[pidStore] Cleaned stale record for PID ${record.pid}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async close() {
|
|
676
|
+
try {
|
|
677
|
+
await this.store.compact();
|
|
678
|
+
} catch (error) {
|
|
679
|
+
logger.debug("[pidStore] Compact on close failed:", error);
|
|
680
|
+
}
|
|
681
|
+
logger.debug("[pidStore] Database compacted and closed");
|
|
682
|
+
}
|
|
683
|
+
isProcessAlive(pid) {
|
|
684
|
+
try {
|
|
685
|
+
process.kill(pid, 0);
|
|
686
|
+
return true;
|
|
687
|
+
} catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async ensureGitignore() {
|
|
692
|
+
const gitignorePath = path.join(this.storeDir, ".gitignore");
|
|
693
|
+
const gitignoreContent = `# Auto-generated .gitignore for agent-yes
|
|
694
|
+
# Ignore all log files and runtime data
|
|
695
|
+
logs/
|
|
696
|
+
fifo/
|
|
697
|
+
pid-db/
|
|
698
|
+
*.jsonl
|
|
699
|
+
*.jsonl~
|
|
700
|
+
*.jsonl.lock
|
|
701
|
+
*.sqlite
|
|
702
|
+
*.sqlite-*
|
|
703
|
+
*.log
|
|
704
|
+
*.raw.log
|
|
705
|
+
*.lines.log
|
|
706
|
+
*.debug.log
|
|
707
|
+
|
|
708
|
+
# Ignore .gitignore itself
|
|
709
|
+
.gitignore
|
|
710
|
+
|
|
711
|
+
`;
|
|
712
|
+
try {
|
|
713
|
+
await mkdir$1(this.storeDir, { recursive: true });
|
|
714
|
+
await writeFile$1(gitignorePath, gitignoreContent, { flag: "wx" });
|
|
715
|
+
logger.debug(`[pidStore] Created .gitignore in ${this.storeDir}`);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (error.code !== "EEXIST") logger.warn(`[pidStore] Failed to create .gitignore:`, error);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
static async findActiveFifo(workingDir) {
|
|
721
|
+
try {
|
|
722
|
+
const store = new PidStore(workingDir);
|
|
723
|
+
await store.init();
|
|
724
|
+
const records = store.store.find((r) => r.status !== "exited").sort((a, b) => b.startedAt - a.startedAt);
|
|
725
|
+
await store.close();
|
|
726
|
+
return records[0]?.fifoFile ?? null;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
logger.warn("[pidStore] findActiveFifo failed:", error);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region ts/idleWaiter.ts
|
|
736
|
+
/**
|
|
737
|
+
* A utility class to wait for idle periods based on activity pings.
|
|
738
|
+
*
|
|
739
|
+
* @example
|
|
740
|
+
* const idleWaiter = new IdleWaiter();
|
|
741
|
+
*
|
|
742
|
+
* // Somewhere in your code, when activity occurs:
|
|
743
|
+
* idleWaiter.ping();
|
|
744
|
+
*
|
|
745
|
+
* // To wait for an idle period of 5 seconds:
|
|
746
|
+
* await idleWaiter.wait(5000);
|
|
747
|
+
* console.log('System has been idle for 5 seconds');
|
|
748
|
+
*/
|
|
749
|
+
var IdleWaiter = class {
|
|
750
|
+
lastActivityTime = Date.now();
|
|
751
|
+
checkInterval = 100;
|
|
752
|
+
constructor() {
|
|
753
|
+
this.ping();
|
|
754
|
+
}
|
|
755
|
+
ping() {
|
|
756
|
+
this.lastActivityTime = Date.now();
|
|
757
|
+
return this;
|
|
758
|
+
}
|
|
759
|
+
async wait(ms) {
|
|
760
|
+
while (this.lastActivityTime >= Date.now() - ms) await new Promise((resolve) => setTimeout(resolve, this.checkInterval));
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region ts/ReadyManager.ts
|
|
766
|
+
var ReadyManager = class {
|
|
767
|
+
isReady = false;
|
|
768
|
+
readyQueue = [];
|
|
769
|
+
wait() {
|
|
770
|
+
if (this.isReady) return;
|
|
771
|
+
return new Promise((resolve) => this.readyQueue.push(resolve));
|
|
772
|
+
}
|
|
773
|
+
unready() {
|
|
774
|
+
this.isReady = false;
|
|
775
|
+
}
|
|
776
|
+
ready() {
|
|
777
|
+
this.isReady = true;
|
|
778
|
+
if (!this.readyQueue.length) return;
|
|
779
|
+
this.readyQueue.splice(0).map((resolve) => resolve());
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
//#endregion
|
|
784
|
+
//#region ts/core/messaging.ts
|
|
785
|
+
/**
|
|
786
|
+
* Send Enter key to the shell after waiting for idle state
|
|
787
|
+
* @param context Message context with shell and state managers
|
|
788
|
+
* @param waitms Milliseconds to wait for idle before sending Enter (default: 1000)
|
|
789
|
+
*/
|
|
790
|
+
async function sendEnter(context, waitms = 1e3) {
|
|
791
|
+
const st = Date.now();
|
|
792
|
+
await context.idleWaiter.wait(waitms);
|
|
793
|
+
const et = Date.now();
|
|
794
|
+
logger.debug(`sendingEnter| idleWaiter.wait(${String(waitms)}) took ${String(et - st)}ms`);
|
|
795
|
+
context.nextStdout.unready();
|
|
796
|
+
context.shell.write("\r");
|
|
797
|
+
logger.debug(`enterSent| idleWaiter.wait(${String(waitms)}) took ${String(et - st)}ms`);
|
|
798
|
+
await Promise.race([context.nextStdout.wait(), new Promise((resolve) => setTimeout(() => {
|
|
799
|
+
if (!context.nextStdout.ready) context.shell.write("\r");
|
|
800
|
+
resolve();
|
|
801
|
+
}, 1e3))]);
|
|
802
|
+
await Promise.race([context.nextStdout.wait(), new Promise((resolve) => setTimeout(() => {
|
|
803
|
+
if (!context.nextStdout.ready) context.shell.write("\r");
|
|
804
|
+
resolve();
|
|
805
|
+
}, 3e3))]);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Send a message to the shell
|
|
809
|
+
* @param context Message context with shell and state managers
|
|
810
|
+
* @param message Message string to send
|
|
811
|
+
* @param options Options for message sending
|
|
812
|
+
*/
|
|
813
|
+
async function sendMessage(context, message, { waitForReady = true } = {}) {
|
|
814
|
+
if (waitForReady) await context.stdinReady.wait();
|
|
815
|
+
logger.debug(`send |${message}`);
|
|
816
|
+
context.nextStdout.unready();
|
|
817
|
+
context.shell.write(message);
|
|
818
|
+
context.idleWaiter.ping();
|
|
819
|
+
logger.debug(`waiting next stdout|${message}`);
|
|
820
|
+
await context.nextStdout.wait();
|
|
821
|
+
logger.debug(`sending enter`);
|
|
822
|
+
await sendEnter(context, 1e3);
|
|
823
|
+
logger.debug(`sent enter`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region ts/core/logging.ts
|
|
828
|
+
/**
|
|
829
|
+
* Initialize log paths based on PID
|
|
830
|
+
* @param pidStore PID store instance
|
|
831
|
+
* @param pid Process ID
|
|
832
|
+
* @returns Object containing all log paths
|
|
833
|
+
*/
|
|
834
|
+
async function initializeLogPaths(pidStore, pid) {
|
|
835
|
+
const logDir = pidStore.getLogDir();
|
|
836
|
+
await mkdir$1(logDir, { recursive: true });
|
|
837
|
+
return {
|
|
838
|
+
logPath: logDir,
|
|
839
|
+
rawLogPath: path.resolve(path.dirname(logDir), `${pid}.raw.log`),
|
|
840
|
+
rawLinesLogPath: path.resolve(path.dirname(logDir), `${pid}.lines.log`),
|
|
841
|
+
debuggingLogsPath: path.resolve(path.dirname(logDir), `${pid}.debug.log`)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Setup debug logging to file
|
|
846
|
+
* @param debuggingLogsPath Path to debug log file
|
|
847
|
+
*/
|
|
848
|
+
function setupDebugLogging(debuggingLogsPath) {
|
|
849
|
+
if (debuggingLogsPath) logger.add(new winston.transports.File({
|
|
850
|
+
filename: debuggingLogsPath,
|
|
851
|
+
level: "debug"
|
|
852
|
+
}));
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Save rendered terminal output to log file
|
|
856
|
+
* @param logPath Path to log file
|
|
857
|
+
* @param content Rendered content to save
|
|
858
|
+
*/
|
|
859
|
+
async function saveLogFile(logPath, content) {
|
|
860
|
+
if (!logPath) return;
|
|
861
|
+
await mkdir$1(path.dirname(logPath), { recursive: true }).catch(() => null);
|
|
862
|
+
await writeFile$1(logPath, content).catch(() => null);
|
|
863
|
+
logger.info(`Full logs saved to ${logPath}`);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Save logs to deprecated logFile option (for backward compatibility)
|
|
867
|
+
* @param logFile User-specified log file path
|
|
868
|
+
* @param content Rendered content to save
|
|
869
|
+
* @param verbose Whether to log verbose messages
|
|
870
|
+
*/
|
|
871
|
+
async function saveDeprecatedLogFile(logFile, content, verbose) {
|
|
872
|
+
if (!logFile) return;
|
|
873
|
+
if (verbose) logger.info(`Writing rendered logs to ${logFile}`);
|
|
874
|
+
const logFilePath = path.resolve(logFile);
|
|
875
|
+
await mkdir$1(path.dirname(logFilePath), { recursive: true }).catch(() => null);
|
|
876
|
+
await writeFile$1(logFilePath, content);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region ts/tryCatch.ts
|
|
881
|
+
/**
|
|
882
|
+
* A utility function to wrap another function with a try-catch block.
|
|
883
|
+
* If an error occurs during the execution of the function, the provided
|
|
884
|
+
* catchFn is called with the error, the original function, and its arguments.
|
|
885
|
+
*
|
|
886
|
+
* @param catchFn - The function to call when an error occurs.
|
|
887
|
+
* @param fn - The function to wrap.
|
|
888
|
+
* @returns A new function that wraps the original function with error handling.
|
|
889
|
+
*/
|
|
890
|
+
function tryCatch(catchFn, fn) {
|
|
891
|
+
let attempts = 0;
|
|
892
|
+
return function robustFn(...args) {
|
|
893
|
+
try {
|
|
894
|
+
attempts++;
|
|
895
|
+
return fn(...args);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
return catchFn(error, attempts, robustFn, ...args);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region package.json
|
|
904
|
+
var name = "agent-yes";
|
|
905
|
+
var version = "1.60.3";
|
|
906
|
+
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region ts/pty-fix.ts
|
|
909
|
+
var pty_fix_exports = /* @__PURE__ */ __exportAll({});
|
|
910
|
+
function getLibraryName() {
|
|
911
|
+
switch (platform) {
|
|
912
|
+
case "win32": return "rust_pty.dll";
|
|
913
|
+
case "darwin": return arch === "arm64" ? "librust_pty_arm64.dylib" : "librust_pty.dylib";
|
|
914
|
+
case "linux": return arch === "arm64" ? "librust_pty_arm64.so" : "librust_pty.so";
|
|
915
|
+
default: return "librust_pty.so";
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function rebuildBunPty() {
|
|
919
|
+
try {
|
|
920
|
+
const cargoCmd = platform === "win32" ? "cargo.exe" : "cargo";
|
|
921
|
+
try {
|
|
922
|
+
execSync(`${cargoCmd} --version`, { stdio: "ignore" });
|
|
923
|
+
} catch {
|
|
924
|
+
console.warn("Warning: Rust/Cargo not found. bun-pty native module may not work.");
|
|
925
|
+
console.warn("To fix this, install Rust: https://rustup.rs/");
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const rustPtyDir = join(bunPtyPath, "rust-pty");
|
|
929
|
+
const isWindows = platform === "win32";
|
|
930
|
+
const tempBase = isWindows ? process.env.TEMP || "C:\\Temp" : "/tmp";
|
|
931
|
+
if (!existsSync(join(rustPtyDir, "Cargo.toml"))) {
|
|
932
|
+
console.log("Source code not found in npm package, cloning from repository...");
|
|
933
|
+
const tmpDir = join(tempBase, `bun-pty-build-${Date.now()}`);
|
|
934
|
+
try {
|
|
935
|
+
execSync(`git clone https://github.com/snomiao/bun-pty.git "${tmpDir}"`, { stdio: "inherit" });
|
|
936
|
+
if (isWindows) execSync(`cd /d "${tmpDir}" && cargo build --release --manifest-path rust-pty\\Cargo.toml`, { stdio: "inherit" });
|
|
937
|
+
else execSync(`cd "${tmpDir}" && cargo build --release --manifest-path rust-pty/Cargo.toml`, { stdio: "inherit" });
|
|
938
|
+
const builtLib = join(tmpDir, "rust-pty", "target", "release", libName);
|
|
939
|
+
if (existsSync(builtLib)) {
|
|
940
|
+
const targetDir = join(rustPtyDir, "target", "release");
|
|
941
|
+
if (isWindows) {
|
|
942
|
+
execSync(`if not exist "${targetDir}" mkdir "${targetDir}"`, {});
|
|
943
|
+
execSync(`copy /Y "${builtLib}" "${libPath}"`, {});
|
|
944
|
+
} else {
|
|
945
|
+
execSync(`mkdir -p "${targetDir}"`, { stdio: "inherit" });
|
|
946
|
+
execSync(`cp "${builtLib}" "${libPath}"`, { stdio: "inherit" });
|
|
947
|
+
}
|
|
948
|
+
console.log("Successfully rebuilt bun-pty native module");
|
|
949
|
+
}
|
|
950
|
+
if (isWindows) execSync(`rmdir /s /q "${tmpDir}"`, { stdio: "ignore" });
|
|
951
|
+
else execSync(`rm -rf "${tmpDir}"`, { stdio: "ignore" });
|
|
952
|
+
} catch (buildError) {
|
|
953
|
+
console.error("Failed to build bun-pty:", buildError instanceof Error ? buildError.message : buildError);
|
|
954
|
+
console.warn("The application may not work correctly without bun-pty");
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
console.log("Building bun-pty from source...");
|
|
958
|
+
if (isWindows) execSync(`cd /d "${rustPtyDir}" && cargo build --release`, { stdio: "inherit" });
|
|
959
|
+
else execSync(`cd "${rustPtyDir}" && cargo build --release`, { stdio: "inherit" });
|
|
960
|
+
console.log("Successfully rebuilt bun-pty native module");
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
console.error("Failed to rebuild bun-pty:", error instanceof Error ? error.message : error);
|
|
964
|
+
console.warn("The application may not work correctly without bun-pty");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
var bunPtyPath, libName, libPath;
|
|
968
|
+
var init_pty_fix = __esmMin((() => {
|
|
969
|
+
bunPtyPath = dirname(fileURLToPath(import.meta.resolve("@snomiao/bun-pty"))) + "/..";
|
|
970
|
+
libName = getLibraryName();
|
|
971
|
+
libPath = join(bunPtyPath, "rust-pty", "target", "release", libName);
|
|
972
|
+
if (!existsSync(bunPtyPath)) {
|
|
973
|
+
console.log({ bunPtyPath });
|
|
974
|
+
console.log("bun-pty not found, skipping pty-fix in ");
|
|
975
|
+
process.exit(0);
|
|
976
|
+
}
|
|
977
|
+
if (platform === "linux") try {
|
|
978
|
+
const lddOutput = execSync(`ldd "${libPath}" 2>&1`, { encoding: "utf8" });
|
|
979
|
+
if (lddOutput.includes("GLIBC") && lddOutput.includes("not found")) {
|
|
980
|
+
console.log("GLIBC compatibility issue detected, rebuilding bun-pty...");
|
|
981
|
+
rebuildBunPty();
|
|
982
|
+
} else console.log("bun-pty binary is compatible");
|
|
983
|
+
} catch {
|
|
984
|
+
console.log("Checking bun-pty compatibility...");
|
|
985
|
+
rebuildBunPty();
|
|
986
|
+
}
|
|
987
|
+
else if (platform === "win32") if (!existsSync(libPath)) {
|
|
988
|
+
console.log("Windows DLL not found, attempting to rebuild...");
|
|
989
|
+
rebuildBunPty();
|
|
990
|
+
} else console.log("bun-pty Windows DLL found");
|
|
991
|
+
else if (platform === "darwin") if (!existsSync(libPath)) {
|
|
992
|
+
console.log("macOS dylib not found, attempting to rebuild...");
|
|
993
|
+
rebuildBunPty();
|
|
994
|
+
} else console.log("bun-pty macOS dylib found");
|
|
995
|
+
else console.log(`Platform ${platform} may require manual configuration`);
|
|
996
|
+
}));
|
|
997
|
+
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region ts/core/spawner.ts
|
|
1000
|
+
/**
|
|
1001
|
+
* Get install command based on platform and configuration
|
|
1002
|
+
*
|
|
1003
|
+
* Selects the appropriate install command from the configuration
|
|
1004
|
+
* based on the current platform (Windows/Unix) and available shells.
|
|
1005
|
+
* Falls back to npm if platform-specific commands aren't available.
|
|
1006
|
+
*
|
|
1007
|
+
* @param installConfig - Install command configuration (string or platform-specific object)
|
|
1008
|
+
* @returns Install command string or null if no suitable command found
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```typescript
|
|
1012
|
+
* // Simple string config
|
|
1013
|
+
* getInstallCommand('npm install -g claude-cli')
|
|
1014
|
+
*
|
|
1015
|
+
* // Platform-specific config
|
|
1016
|
+
* getInstallCommand({
|
|
1017
|
+
* windows: 'npm install -g claude-cli',
|
|
1018
|
+
* unix: 'curl -fsSL install.sh | sh',
|
|
1019
|
+
* npm: 'npm install -g claude-cli'
|
|
1020
|
+
* })
|
|
1021
|
+
* ```
|
|
1022
|
+
*/
|
|
1023
|
+
function getInstallCommand(installConfig) {
|
|
1024
|
+
if (typeof installConfig === "string") return installConfig;
|
|
1025
|
+
const isWindows = process.platform === "win32";
|
|
1026
|
+
const platform = isWindows ? "windows" : "unix";
|
|
1027
|
+
if (installConfig[platform]) return installConfig[platform];
|
|
1028
|
+
if (isWindows && installConfig.powershell) return installConfig.powershell;
|
|
1029
|
+
if (!isWindows && installConfig.bash) return installConfig.bash;
|
|
1030
|
+
if (installConfig.npm) return installConfig.npm;
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Check if error is a command not found error
|
|
1035
|
+
*/
|
|
1036
|
+
function isCommandNotFoundError(e) {
|
|
1037
|
+
if (e instanceof Error) return e.message.includes("command not found") || e.message.includes("ENOENT") || e.message.includes("spawn");
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Spawn agent CLI process with error handling and auto-install
|
|
1042
|
+
*
|
|
1043
|
+
* Creates a new PTY process for the specified CLI with comprehensive error
|
|
1044
|
+
* handling. If the CLI is not found and auto-install is enabled, attempts
|
|
1045
|
+
* to install it automatically. Includes special handling for bun-pty issues.
|
|
1046
|
+
*
|
|
1047
|
+
* @param options - Spawn configuration options
|
|
1048
|
+
* @returns IPty process instance
|
|
1049
|
+
* @throws Error if CLI not found and installation fails or is disabled
|
|
1050
|
+
*
|
|
1051
|
+
* @example
|
|
1052
|
+
* ```typescript
|
|
1053
|
+
* const shell = spawnAgent({
|
|
1054
|
+
* cli: 'claude',
|
|
1055
|
+
* cliConf: config.clis.claude,
|
|
1056
|
+
* cliArgs: ['--verbose'],
|
|
1057
|
+
* verbose: true,
|
|
1058
|
+
* install: false,
|
|
1059
|
+
* ptyOptions: {
|
|
1060
|
+
* name: 'xterm-color',
|
|
1061
|
+
* cols: 80,
|
|
1062
|
+
* rows: 30,
|
|
1063
|
+
* cwd: '/path/to/project',
|
|
1064
|
+
* env: process.env
|
|
1065
|
+
* }
|
|
1066
|
+
* });
|
|
1067
|
+
* ```
|
|
1068
|
+
*/
|
|
1069
|
+
function spawnAgent(options) {
|
|
1070
|
+
const { cli, cliConf, cliArgs, verbose, install, ptyOptions } = options;
|
|
1071
|
+
const spawn = () => {
|
|
1072
|
+
let [bin, ...args] = [...parseCommandString(cliConf?.binary || cli), ...cliArgs];
|
|
1073
|
+
logger.debug(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
|
|
1074
|
+
const spawned = pty.spawn(bin, args, ptyOptions);
|
|
1075
|
+
logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid} (agent-yes v${version})`);
|
|
1076
|
+
return spawned;
|
|
1077
|
+
};
|
|
1078
|
+
return tryCatch((error, attempts, spawn, ...args) => {
|
|
1079
|
+
logger.error(`Fatal: Failed to start ${cli}.`);
|
|
1080
|
+
const isNotFound = isCommandNotFoundError(error);
|
|
1081
|
+
if (cliConf?.install && isNotFound) {
|
|
1082
|
+
const installCmd = getInstallCommand(cliConf.install);
|
|
1083
|
+
if (!installCmd) {
|
|
1084
|
+
logger.error(`No suitable install command found for ${cli} on this platform`);
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
logger.info(`Please install the cli by run ${installCmd}`);
|
|
1088
|
+
if (install) {
|
|
1089
|
+
logger.debug(`Attempting to install ${cli}...`);
|
|
1090
|
+
execSync$1(installCmd, { stdio: "inherit" });
|
|
1091
|
+
logger.info(`${cli} installed successfully. Please rerun the command.`);
|
|
1092
|
+
return spawn(...args);
|
|
1093
|
+
} else {
|
|
1094
|
+
logger.error(`If you did not installed it yet, Please install it first: ${installCmd}`);
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (globalThis.Bun && error instanceof Error && error.stack?.includes("bun-pty")) {
|
|
1099
|
+
logger.error(`Detected bun-pty issue, attempted to fix it. Please try again.`);
|
|
1100
|
+
init_pty_fix();
|
|
1101
|
+
}
|
|
1102
|
+
throw error;
|
|
1103
|
+
}, spawn)();
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region ts/core/context.ts
|
|
1108
|
+
/**
|
|
1109
|
+
* Shared context for agent session
|
|
1110
|
+
*
|
|
1111
|
+
* Groups related state and dependencies for easier passing between modules.
|
|
1112
|
+
* This class encapsulates all stateful components needed during an agent session,
|
|
1113
|
+
* including the PTY shell, configuration, state managers, and flags.
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* ```typescript
|
|
1117
|
+
* const ctx = new AgentContext({
|
|
1118
|
+
* shell,
|
|
1119
|
+
* pidStore,
|
|
1120
|
+
* logPaths,
|
|
1121
|
+
* cli: 'claude',
|
|
1122
|
+
* cliConf,
|
|
1123
|
+
* verbose: true,
|
|
1124
|
+
* robust: true
|
|
1125
|
+
* });
|
|
1126
|
+
*
|
|
1127
|
+
* // Access message context for sending messages
|
|
1128
|
+
* await sendMessage(ctx.messageContext, 'Hello');
|
|
1129
|
+
*
|
|
1130
|
+
* // Check and update state
|
|
1131
|
+
* if (ctx.isFatal) {
|
|
1132
|
+
* await exitAgent();
|
|
1133
|
+
* }
|
|
1134
|
+
* ```
|
|
1135
|
+
*/
|
|
1136
|
+
var AgentContext = class {
|
|
1137
|
+
shell;
|
|
1138
|
+
pidStore;
|
|
1139
|
+
logPaths;
|
|
1140
|
+
cli;
|
|
1141
|
+
cliConf;
|
|
1142
|
+
verbose;
|
|
1143
|
+
robust;
|
|
1144
|
+
stdinReady = new ReadyManager();
|
|
1145
|
+
stdinFirstReady = new ReadyManager();
|
|
1146
|
+
nextStdout = new ReadyManager();
|
|
1147
|
+
idleWaiter = new IdleWaiter();
|
|
1148
|
+
isFatal = false;
|
|
1149
|
+
shouldRestartWithoutContinue = false;
|
|
1150
|
+
autoYesEnabled = true;
|
|
1151
|
+
constructor(params) {
|
|
1152
|
+
this.shell = params.shell;
|
|
1153
|
+
this.pidStore = params.pidStore;
|
|
1154
|
+
this.logPaths = params.logPaths;
|
|
1155
|
+
this.cli = params.cli;
|
|
1156
|
+
this.cliConf = params.cliConf;
|
|
1157
|
+
this.verbose = params.verbose;
|
|
1158
|
+
this.robust = params.robust;
|
|
1159
|
+
this.autoYesEnabled = params.autoYes ?? true;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Get message context for sendMessage/sendEnter helpers
|
|
1163
|
+
*
|
|
1164
|
+
* Provides a lightweight object with only the dependencies needed
|
|
1165
|
+
* for message sending operations, avoiding circular references.
|
|
1166
|
+
*
|
|
1167
|
+
* @returns MessageContext object for use with sendMessage/sendEnter
|
|
1168
|
+
*/
|
|
1169
|
+
get messageContext() {
|
|
1170
|
+
return {
|
|
1171
|
+
shell: this.shell,
|
|
1172
|
+
idleWaiter: this.idleWaiter,
|
|
1173
|
+
stdinReady: this.stdinReady,
|
|
1174
|
+
nextStdout: this.nextStdout
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region ts/core/streamHelpers.ts
|
|
1181
|
+
/**
|
|
1182
|
+
* Stream processing utilities for terminal I/O
|
|
1183
|
+
*
|
|
1184
|
+
* Provides helper functions for stream lifecycle management.
|
|
1185
|
+
*/
|
|
1186
|
+
/**
|
|
1187
|
+
* Create a terminator transform stream that ends when promise resolves
|
|
1188
|
+
*
|
|
1189
|
+
* Creates a TransformStream that automatically terminates when the provided
|
|
1190
|
+
* promise resolves. Used to stop output processing when the agent exits.
|
|
1191
|
+
*
|
|
1192
|
+
* @param exitPromise - Promise that resolves when stream should terminate
|
|
1193
|
+
* @returns TransformStream that terminates on promise resolution
|
|
1194
|
+
*
|
|
1195
|
+
* @example
|
|
1196
|
+
* ```typescript
|
|
1197
|
+
* const exitPromise = Promise.withResolvers<number>();
|
|
1198
|
+
* stream.by(createTerminatorStream(exitPromise.promise));
|
|
1199
|
+
*
|
|
1200
|
+
* // Later, when agent exits:
|
|
1201
|
+
* exitPromise.resolve(0);
|
|
1202
|
+
* ```
|
|
1203
|
+
*/
|
|
1204
|
+
function createTerminatorStream(exitPromise) {
|
|
1205
|
+
return new TransformStream({
|
|
1206
|
+
start: function terminator(ctrl) {
|
|
1207
|
+
exitPromise.then(() => ctrl.terminate());
|
|
1208
|
+
},
|
|
1209
|
+
transform: (e, ctrl) => ctrl.enqueue(e),
|
|
1210
|
+
flush: (ctrl) => ctrl.terminate()
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region ts/agentRegistry.ts
|
|
1216
|
+
const MAX_BUFFER_SIZE = 1e3;
|
|
1217
|
+
var AgentRegistry = class {
|
|
1218
|
+
agents = /* @__PURE__ */ new Map();
|
|
1219
|
+
/**
|
|
1220
|
+
* Register a new agent instance
|
|
1221
|
+
*/
|
|
1222
|
+
register(pid, instance) {
|
|
1223
|
+
this.agents.set(pid, instance);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Unregister an agent instance
|
|
1227
|
+
*/
|
|
1228
|
+
unregister(pid) {
|
|
1229
|
+
this.agents.delete(pid);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Get an agent instance by PID
|
|
1233
|
+
*/
|
|
1234
|
+
get(pid) {
|
|
1235
|
+
return this.agents.get(pid);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* List all registered agents
|
|
1239
|
+
*/
|
|
1240
|
+
list() {
|
|
1241
|
+
return Array.from(this.agents.values());
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Append stdout data to an agent's buffer (circular buffer)
|
|
1245
|
+
*/
|
|
1246
|
+
appendStdout(pid, data) {
|
|
1247
|
+
const instance = this.agents.get(pid);
|
|
1248
|
+
if (!instance) return;
|
|
1249
|
+
const lines = data.split("\n");
|
|
1250
|
+
instance.stdoutBuffer.push(...lines);
|
|
1251
|
+
if (instance.stdoutBuffer.length > MAX_BUFFER_SIZE) instance.stdoutBuffer = instance.stdoutBuffer.slice(-MAX_BUFFER_SIZE);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Get stdout from an agent's buffer
|
|
1255
|
+
*/
|
|
1256
|
+
getStdout(pid, tail) {
|
|
1257
|
+
const instance = this.agents.get(pid);
|
|
1258
|
+
if (!instance) return [];
|
|
1259
|
+
if (tail !== void 0) return instance.stdoutBuffer.slice(-tail);
|
|
1260
|
+
return instance.stdoutBuffer;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
const globalAgentRegistry = new AgentRegistry();
|
|
1264
|
+
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region ts/installEnv.ts
|
|
1267
|
+
const installDir = path$1.join(import.meta.dirname ?? import.meta.dir, "..");
|
|
1268
|
+
function parseEnvContent(content) {
|
|
1269
|
+
const result = {};
|
|
1270
|
+
for (const line of content.split("\n")) {
|
|
1271
|
+
const trimmed = line.trim();
|
|
1272
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1273
|
+
const eqIndex = trimmed.indexOf("=");
|
|
1274
|
+
if (eqIndex < 0) continue;
|
|
1275
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
1276
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
1277
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1278
|
+
result[key] = value;
|
|
1279
|
+
}
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
let _installEnv = null;
|
|
1283
|
+
/**
|
|
1284
|
+
* Load .env from the agent-yes install directory (not the working dir).
|
|
1285
|
+
* Install dir is ${import.meta.dir}/.. relative to this file.
|
|
1286
|
+
* Cached after first load.
|
|
1287
|
+
*/
|
|
1288
|
+
async function loadInstallEnv() {
|
|
1289
|
+
if (_installEnv) return _installEnv;
|
|
1290
|
+
const envPath = path$1.join(installDir, ".env");
|
|
1291
|
+
try {
|
|
1292
|
+
_installEnv = parseEnvContent(await readFile(envPath, "utf-8"));
|
|
1293
|
+
} catch {
|
|
1294
|
+
_installEnv = {};
|
|
1295
|
+
}
|
|
1296
|
+
return _installEnv;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Get a value from the install .env, falling back to process.env.
|
|
1300
|
+
*/
|
|
1301
|
+
async function getInstallEnv(key) {
|
|
1302
|
+
return (await loadInstallEnv())[key] ?? process.env[key];
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
//#endregion
|
|
1306
|
+
//#region ts/webhookNotifier.ts
|
|
1307
|
+
/**
|
|
1308
|
+
* Notify the AGENT_YES_MESSAGE_WEBHOOK URL with a status message.
|
|
1309
|
+
*
|
|
1310
|
+
* AGENT_YES_MESSAGE_WEBHOOK should be set in the agent-yes install dir .env, e.g.:
|
|
1311
|
+
* AGENT_YES_MESSAGE_WEBHOOK=https://example.com/hook?q=%s
|
|
1312
|
+
*
|
|
1313
|
+
* The %s placeholder is replaced with the URL-encoded message:
|
|
1314
|
+
* [STATUS] hostname:cwd details
|
|
1315
|
+
*/
|
|
1316
|
+
async function notifyWebhook(status, details, cwd = process.cwd()) {
|
|
1317
|
+
const webhookTemplate = await getInstallEnv("AGENT_YES_MESSAGE_WEBHOOK");
|
|
1318
|
+
if (!webhookTemplate) return;
|
|
1319
|
+
const message = `[${status}] ${os.hostname()}:${cwd}${details ? " " + details : ""}`;
|
|
1320
|
+
const url = webhookTemplate.replace("%s", encodeURIComponent(message));
|
|
1321
|
+
try {
|
|
1322
|
+
const res = await fetch(url);
|
|
1323
|
+
logger.debug(`[webhook] ${status} notified (${res.status}): ${url}`);
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
logger.warn(`[webhook] Failed to notify ${status}: ${error}`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
//#endregion
|
|
1330
|
+
//#region ts/index.ts
|
|
1331
|
+
const config = await import("./agent-yes.config-XmUcKFde.js").then((mod) => mod.default || mod);
|
|
1332
|
+
const CLIS_CONFIG = config.clis;
|
|
1333
|
+
/**
|
|
1334
|
+
* Main function to run agent-cli with automatic yes/no responses
|
|
1335
|
+
* @param options Configuration options
|
|
1336
|
+
* @param options.continueOnCrash - If true, automatically restart agent-cli when it crashes:
|
|
1337
|
+
* 1. Shows message 'agent-cli crashed, restarting..'
|
|
1338
|
+
* 2. Spawns a new 'agent-cli --continue' process
|
|
1339
|
+
* 3. Re-attaches the new process to the shell stdio (pipes new process stdin/stdout)
|
|
1340
|
+
* 4. If it crashes with "No conversation found to continue", exits the process
|
|
1341
|
+
* @param options.exitOnIdle - Exit when agent-cli is idle. Boolean or timeout in milliseconds, recommended 5000 - 60000, default is false
|
|
1342
|
+
* @param options.cliArgs - Additional arguments to pass to the agent-cli CLI
|
|
1343
|
+
* @param options.removeControlCharactersFromStdout - Remove ANSI control characters from stdout. Defaults to !process.stdout.isTTY
|
|
1344
|
+
* @param options.disableLock - Disable the running lock feature that prevents concurrent agents in the same directory/repo
|
|
1345
|
+
*
|
|
1346
|
+
* @example
|
|
1347
|
+
* ```typescript
|
|
1348
|
+
* import agentYes from 'agent-yes';
|
|
1349
|
+
* await agentYes({
|
|
1350
|
+
* prompt: 'help me solve all todos in my codebase',
|
|
1351
|
+
*
|
|
1352
|
+
* // optional
|
|
1353
|
+
* cliArgs: ['--verbose'], // additional args to pass to agent-cli
|
|
1354
|
+
* exitOnIdle: 30000, // exit after 30 seconds of idle
|
|
1355
|
+
* robust: true, // auto restart with --continue if claude crashes, default is true
|
|
1356
|
+
* logFile: 'claude-output.log', // save logs to file
|
|
1357
|
+
* disableLock: false, // disable running lock (default is false)
|
|
1358
|
+
* });
|
|
1359
|
+
* ```
|
|
1360
|
+
*/
|
|
1361
|
+
async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true }) {
|
|
1362
|
+
if (!cli) throw new Error(`cli is required`);
|
|
1363
|
+
const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(" ")}`);
|
|
1364
|
+
const workingDir = cwd ?? process.cwd();
|
|
1365
|
+
if (queue) {
|
|
1366
|
+
if (queue && shouldUseLock(workingDir)) await acquireLock(workingDir, prompt ?? "Interactive session");
|
|
1367
|
+
const cleanupLock = async () => {
|
|
1368
|
+
if (queue && shouldUseLock(workingDir)) await releaseLock().catch(() => null);
|
|
1369
|
+
};
|
|
1370
|
+
process.on("exit", () => {
|
|
1371
|
+
if (queue) releaseLock().catch(() => null);
|
|
1372
|
+
});
|
|
1373
|
+
process.on("SIGINT", async (code) => {
|
|
1374
|
+
await cleanupLock();
|
|
1375
|
+
process.exit(code);
|
|
1376
|
+
});
|
|
1377
|
+
process.on("SIGTERM", async (code) => {
|
|
1378
|
+
await cleanupLock();
|
|
1379
|
+
process.exit(code);
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
const pidStore = new PidStore(workingDir);
|
|
1383
|
+
await pidStore.init();
|
|
1384
|
+
let userSentCtrlC = false;
|
|
1385
|
+
if (verbose) logger.debug(`[stdin] isTTY: ${process.stdin.isTTY}, setRawMode available: ${!!process.stdin.setRawMode}`);
|
|
1386
|
+
process.stdin.setRawMode?.(true);
|
|
1387
|
+
if (verbose) logger.debug(`[stdin] Raw mode set, isRaw: ${process.stdin.isRaw}`);
|
|
1388
|
+
const terminalStream = new TerminalRenderStream({ mode: "raw" });
|
|
1389
|
+
const terminalRender = terminalStream.getRenderer();
|
|
1390
|
+
const outputWriter = terminalStream.writable.getWriter();
|
|
1391
|
+
logger.debug(`Using ${ptyPackage} for pseudo terminal management.`);
|
|
1392
|
+
if (!!process.env.CLAUDE_PPID) logger.info(`[${cli}-yes] Running as sub-agent (CLAUDE_PPID=${process.env.CLAUDE_PPID})`);
|
|
1393
|
+
const cliConf = CLIS_CONFIG[cli] || {};
|
|
1394
|
+
cliArgs = cliConf.defaultArgs ? [...cliConf.defaultArgs, ...cliArgs] : cliArgs;
|
|
1395
|
+
try {
|
|
1396
|
+
const workingDir = cwd ?? process.cwd();
|
|
1397
|
+
if (useSkills && cli !== "claude") {
|
|
1398
|
+
let gitRoot = null;
|
|
1399
|
+
try {
|
|
1400
|
+
const result = execaCommandSync("git rev-parse --show-toplevel", {
|
|
1401
|
+
cwd: workingDir,
|
|
1402
|
+
reject: false
|
|
1403
|
+
});
|
|
1404
|
+
if (result.exitCode === 0) gitRoot = result.stdout.trim();
|
|
1405
|
+
} catch {}
|
|
1406
|
+
const skillHeaders = [];
|
|
1407
|
+
let currentDir = workingDir;
|
|
1408
|
+
const searchLimit = gitRoot || path.parse(currentDir).root;
|
|
1409
|
+
while (true) {
|
|
1410
|
+
const md = await readFile$1(path.resolve(currentDir, "SKILL.md"), "utf8").catch(() => null);
|
|
1411
|
+
if (md) {
|
|
1412
|
+
const headerMatch = md.match(/^[\s\S]*?(?=\n##\s)/);
|
|
1413
|
+
const headerRaw = (headerMatch ? headerMatch[0] : md).trim();
|
|
1414
|
+
if (headerRaw) {
|
|
1415
|
+
skillHeaders.push(headerRaw);
|
|
1416
|
+
if (verbose) logger.info(`[skills] Found SKILL.md in ${currentDir} (${headerRaw.length} chars)`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (currentDir === searchLimit) break;
|
|
1420
|
+
const parentDir = path.dirname(currentDir);
|
|
1421
|
+
if (parentDir === currentDir) break;
|
|
1422
|
+
currentDir = parentDir;
|
|
1423
|
+
}
|
|
1424
|
+
if (skillHeaders.length > 0) {
|
|
1425
|
+
const combined = skillHeaders.join("\n\n---\n\n");
|
|
1426
|
+
const MAX = 2e3;
|
|
1427
|
+
const header = combined.length > MAX ? combined.slice(0, MAX) + "…" : combined;
|
|
1428
|
+
const prefix = `Use this repository skill as context:\n\n${header}`;
|
|
1429
|
+
prompt = prompt ? `${prefix}\n\n${prompt}` : prefix;
|
|
1430
|
+
if (verbose) logger.info(`[skills] Injected ${skillHeaders.length} SKILL.md header(s) (${header.length} chars total)`);
|
|
1431
|
+
} else if (verbose) logger.info("[skills] No SKILL.md found in directory hierarchy");
|
|
1432
|
+
}
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
|
|
1435
|
+
}
|
|
1436
|
+
if (resume) if (cli === "codex" && resume) {
|
|
1437
|
+
const storedSessionId = await getSessionForCwd(workingDir);
|
|
1438
|
+
if (storedSessionId) {
|
|
1439
|
+
cliArgs = [
|
|
1440
|
+
"resume",
|
|
1441
|
+
storedSessionId,
|
|
1442
|
+
...cliArgs
|
|
1443
|
+
];
|
|
1444
|
+
await logger.debug(`resume|using stored session ID: ${storedSessionId}`);
|
|
1445
|
+
} else throw new Error(`No stored session found for codex in directory: ${workingDir}, please try without resume option.`);
|
|
1446
|
+
} else if (cli === "claude") {
|
|
1447
|
+
cliArgs = ["--continue", ...cliArgs];
|
|
1448
|
+
await logger.debug(`resume|adding --continue flag for claude`);
|
|
1449
|
+
} else if (cli === "gemini") {
|
|
1450
|
+
cliArgs = ["--resume", ...cliArgs];
|
|
1451
|
+
await logger.debug(`resume|adding --resume flag for gemini`);
|
|
1452
|
+
} else throw new Error(`Resume option is not supported for cli: ${cli}, make a feature request if you want it. https://github.com/snomiao/agent-yes/issues`);
|
|
1453
|
+
if (prompt && cliConf.promptArg) if (cliConf.promptArg === "first-arg") {
|
|
1454
|
+
cliArgs = [prompt, ...cliArgs];
|
|
1455
|
+
prompt = void 0;
|
|
1456
|
+
} else if (cliConf.promptArg === "last-arg") {
|
|
1457
|
+
cliArgs = [...cliArgs, prompt];
|
|
1458
|
+
prompt = void 0;
|
|
1459
|
+
} else if (cliConf.promptArg.startsWith("--")) {
|
|
1460
|
+
cliArgs = [
|
|
1461
|
+
cliConf.promptArg,
|
|
1462
|
+
prompt,
|
|
1463
|
+
...cliArgs
|
|
1464
|
+
];
|
|
1465
|
+
prompt = void 0;
|
|
1466
|
+
} else logger.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
|
|
1467
|
+
const ptyEnv = { ...env ?? process.env };
|
|
1468
|
+
const ptyOptions = {
|
|
1469
|
+
name: "xterm-color",
|
|
1470
|
+
...getTerminalDimensions(),
|
|
1471
|
+
cwd: cwd ?? process.cwd(),
|
|
1472
|
+
env: ptyEnv
|
|
1473
|
+
};
|
|
1474
|
+
let shell = spawnAgent({
|
|
1475
|
+
cli,
|
|
1476
|
+
cliConf,
|
|
1477
|
+
cliArgs,
|
|
1478
|
+
verbose,
|
|
1479
|
+
install,
|
|
1480
|
+
ptyOptions
|
|
1481
|
+
});
|
|
1482
|
+
try {
|
|
1483
|
+
await pidStore.registerProcess({
|
|
1484
|
+
pid: shell.pid,
|
|
1485
|
+
cli,
|
|
1486
|
+
args: cliArgs,
|
|
1487
|
+
prompt,
|
|
1488
|
+
cwd: workingDir
|
|
1489
|
+
});
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
logger.warn(`[pidStore] Failed to register process ${shell.pid}:`, error);
|
|
1492
|
+
}
|
|
1493
|
+
notifyWebhook("RUNNING", prompt ?? "", workingDir).catch(() => null);
|
|
1494
|
+
const logPaths = await initializeLogPaths(pidStore, shell.pid);
|
|
1495
|
+
setupDebugLogging(logPaths.debuggingLogsPath);
|
|
1496
|
+
const ctx = new AgentContext({
|
|
1497
|
+
shell,
|
|
1498
|
+
pidStore,
|
|
1499
|
+
logPaths,
|
|
1500
|
+
cli,
|
|
1501
|
+
cliConf,
|
|
1502
|
+
verbose,
|
|
1503
|
+
robust,
|
|
1504
|
+
autoYes
|
|
1505
|
+
});
|
|
1506
|
+
try {
|
|
1507
|
+
globalAgentRegistry.register(shell.pid, {
|
|
1508
|
+
pid: shell.pid,
|
|
1509
|
+
context: ctx,
|
|
1510
|
+
cwd: workingDir,
|
|
1511
|
+
cli,
|
|
1512
|
+
prompt,
|
|
1513
|
+
startTime: Date.now(),
|
|
1514
|
+
stdoutBuffer: []
|
|
1515
|
+
});
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
logger.warn(`[agentRegistry] Failed to register agent ${shell.pid}:`, error);
|
|
1518
|
+
}
|
|
1519
|
+
if (!ctx.autoYesEnabled) process.stderr.write("\x1B[33m[auto-yes: OFF]\x1B[0m Press Ctrl+Y to toggle\n");
|
|
1520
|
+
if (cliConf.ready && cliConf.ready.length === 0 || !ctx.autoYesEnabled) {
|
|
1521
|
+
ctx.stdinReady.ready();
|
|
1522
|
+
ctx.stdinFirstReady.ready();
|
|
1523
|
+
}
|
|
1524
|
+
sleep(1e4).then(() => {
|
|
1525
|
+
if (!ctx.stdinReady.isReady) ctx.stdinReady.ready();
|
|
1526
|
+
if (!ctx.stdinFirstReady.isReady) ctx.stdinFirstReady.ready();
|
|
1527
|
+
});
|
|
1528
|
+
const pendingExitCode = Promise.withResolvers();
|
|
1529
|
+
function onData(data) {
|
|
1530
|
+
const currentPid = shell.pid;
|
|
1531
|
+
outputWriter.write(data);
|
|
1532
|
+
globalAgentRegistry.appendStdout(currentPid, data);
|
|
1533
|
+
}
|
|
1534
|
+
shell.onData(onData);
|
|
1535
|
+
shell.onExit(async function onExit({ exitCode }) {
|
|
1536
|
+
const exitedPid = shell.pid;
|
|
1537
|
+
globalAgentRegistry.unregister(exitedPid);
|
|
1538
|
+
ctx.stdinReady.unready();
|
|
1539
|
+
const agentCrashed = exitCode !== 0 && !(exitCode === 130 || exitCode === 143 || userSentCtrlC);
|
|
1540
|
+
if (ctx.shouldRestartWithoutContinue) {
|
|
1541
|
+
try {
|
|
1542
|
+
await pidStore.updateStatus(exitedPid, "exited", {
|
|
1543
|
+
exitReason: "restarted",
|
|
1544
|
+
exitCode: exitCode ?? void 0
|
|
1545
|
+
});
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
logger.warn(`[pidStore] Failed to update status for PID ${exitedPid}:`, error);
|
|
1548
|
+
}
|
|
1549
|
+
ctx.shouldRestartWithoutContinue = false;
|
|
1550
|
+
ctx.isFatal = false;
|
|
1551
|
+
let [bin, ...args] = [...parseCommandString(cliConf?.binary || cli), ...cliArgs.filter((arg) => !["--continue", "--resume"].includes(arg))];
|
|
1552
|
+
logger.info(`Restarting ${cli} ${JSON.stringify([bin, ...args])}`);
|
|
1553
|
+
const restartPtyOptions = {
|
|
1554
|
+
name: "xterm-color",
|
|
1555
|
+
...getTerminalDimensions(),
|
|
1556
|
+
cwd: cwd ?? process.cwd(),
|
|
1557
|
+
env: ptyEnv
|
|
1558
|
+
};
|
|
1559
|
+
shell = pty.spawn(bin, args, restartPtyOptions);
|
|
1560
|
+
try {
|
|
1561
|
+
await pidStore.registerProcess({
|
|
1562
|
+
pid: shell.pid,
|
|
1563
|
+
cli,
|
|
1564
|
+
args,
|
|
1565
|
+
prompt,
|
|
1566
|
+
cwd: workingDir
|
|
1567
|
+
});
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
logger.warn(`[pidStore] Failed to register restarted process ${shell.pid}:`, error);
|
|
1570
|
+
}
|
|
1571
|
+
ctx.shell = shell;
|
|
1572
|
+
try {
|
|
1573
|
+
globalAgentRegistry.register(shell.pid, {
|
|
1574
|
+
pid: shell.pid,
|
|
1575
|
+
context: ctx,
|
|
1576
|
+
cwd: workingDir,
|
|
1577
|
+
cli,
|
|
1578
|
+
prompt,
|
|
1579
|
+
startTime: Date.now(),
|
|
1580
|
+
stdoutBuffer: []
|
|
1581
|
+
});
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
logger.warn(`[agentRegistry] Failed to register restarted agent ${shell.pid}:`, error);
|
|
1584
|
+
}
|
|
1585
|
+
shell.onData(onData);
|
|
1586
|
+
shell.onExit(onExit);
|
|
1587
|
+
if (cliConf.ready && cliConf.ready.length === 0 || !ctx.autoYesEnabled) {
|
|
1588
|
+
ctx.stdinReady.ready();
|
|
1589
|
+
ctx.stdinFirstReady.ready();
|
|
1590
|
+
}
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (agentCrashed && robust && conf?.restoreArgs) {
|
|
1594
|
+
if (!conf.restoreArgs) {
|
|
1595
|
+
logger.warn(`robust is only supported for ${Object.entries(CLIS_CONFIG).filter(([_, v]) => v.restoreArgs).map(([k]) => k).join(", ")} currently, not ${cli}`);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (ctx.isFatal) {
|
|
1599
|
+
try {
|
|
1600
|
+
await pidStore.updateStatus(exitedPid, "exited", {
|
|
1601
|
+
exitReason: "fatal",
|
|
1602
|
+
exitCode: exitCode ?? void 0
|
|
1603
|
+
});
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
logger.warn(`[pidStore] Failed to update status for PID ${exitedPid}:`, error);
|
|
1606
|
+
}
|
|
1607
|
+
notifyWebhook("EXIT", `fatal exitCode=${exitCode ?? "?"}`, workingDir).catch(() => null);
|
|
1608
|
+
return pendingExitCode.resolve(exitCode);
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
await pidStore.updateStatus(exitedPid, "exited", {
|
|
1612
|
+
exitReason: "restarted",
|
|
1613
|
+
exitCode: exitCode ?? void 0
|
|
1614
|
+
});
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
logger.warn(`[pidStore] Failed to update status for PID ${exitedPid}:`, error);
|
|
1617
|
+
}
|
|
1618
|
+
logger.info(`${cli} crashed (exit code: ${exitCode}), restarting...`);
|
|
1619
|
+
let restoreArgs = conf.restoreArgs;
|
|
1620
|
+
if (cli === "codex") {
|
|
1621
|
+
const storedSessionId = await getSessionForCwd(workingDir);
|
|
1622
|
+
if (storedSessionId) {
|
|
1623
|
+
restoreArgs = ["resume", storedSessionId];
|
|
1624
|
+
logger.debug(`restore|using stored session ID: ${storedSessionId}`);
|
|
1625
|
+
} else logger.debug(`restore|no stored session, using default restore args`);
|
|
1626
|
+
}
|
|
1627
|
+
const restorePtyOptions = {
|
|
1628
|
+
name: "xterm-color",
|
|
1629
|
+
...getTerminalDimensions(),
|
|
1630
|
+
cwd: cwd ?? process.cwd(),
|
|
1631
|
+
env: ptyEnv
|
|
1632
|
+
};
|
|
1633
|
+
shell = pty.spawn(cli, restoreArgs, restorePtyOptions);
|
|
1634
|
+
try {
|
|
1635
|
+
await pidStore.registerProcess({
|
|
1636
|
+
pid: shell.pid,
|
|
1637
|
+
cli,
|
|
1638
|
+
args: restoreArgs,
|
|
1639
|
+
prompt,
|
|
1640
|
+
cwd: workingDir
|
|
1641
|
+
});
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
logger.warn(`[pidStore] Failed to register restored process ${shell.pid}:`, error);
|
|
1644
|
+
}
|
|
1645
|
+
ctx.shell = shell;
|
|
1646
|
+
try {
|
|
1647
|
+
globalAgentRegistry.register(shell.pid, {
|
|
1648
|
+
pid: shell.pid,
|
|
1649
|
+
context: ctx,
|
|
1650
|
+
cwd: workingDir,
|
|
1651
|
+
cli,
|
|
1652
|
+
prompt,
|
|
1653
|
+
startTime: Date.now(),
|
|
1654
|
+
stdoutBuffer: []
|
|
1655
|
+
});
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
logger.warn(`[agentRegistry] Failed to register restored agent ${shell.pid}:`, error);
|
|
1658
|
+
}
|
|
1659
|
+
shell.onData(onData);
|
|
1660
|
+
shell.onExit(onExit);
|
|
1661
|
+
if (cliConf.ready && cliConf.ready.length === 0 || !ctx.autoYesEnabled) {
|
|
1662
|
+
ctx.stdinReady.ready();
|
|
1663
|
+
ctx.stdinFirstReady.ready();
|
|
1664
|
+
}
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const exitReason = agentCrashed ? "crash" : "normal";
|
|
1668
|
+
try {
|
|
1669
|
+
await pidStore.updateStatus(exitedPid, "exited", {
|
|
1670
|
+
exitReason,
|
|
1671
|
+
exitCode: exitCode ?? void 0
|
|
1672
|
+
});
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
logger.warn(`[pidStore] Failed to update status for PID ${exitedPid}:`, error);
|
|
1675
|
+
}
|
|
1676
|
+
notifyWebhook("EXIT", `${exitReason} exitCode=${exitCode ?? "?"}`, workingDir).catch(() => null);
|
|
1677
|
+
return pendingExitCode.resolve(exitCode);
|
|
1678
|
+
});
|
|
1679
|
+
process.stdout.on("resize", () => {
|
|
1680
|
+
const { cols, rows } = getTerminalDimensions();
|
|
1681
|
+
shell.resize(cols, rows);
|
|
1682
|
+
});
|
|
1683
|
+
const isStillWorkingQ = () => {
|
|
1684
|
+
const rendered = terminalRender.tail(24).replace(/\s+/g, " ");
|
|
1685
|
+
return conf.working?.some((rgx) => rgx.test(rendered));
|
|
1686
|
+
};
|
|
1687
|
+
let lastHeartbeatRendered = "";
|
|
1688
|
+
const heartbeatInterval = setInterval(async () => {
|
|
1689
|
+
try {
|
|
1690
|
+
const rendered = removeControlCharacters(terminalRender.tail(12));
|
|
1691
|
+
if (rendered === lastHeartbeatRendered) return;
|
|
1692
|
+
lastHeartbeatRendered = rendered;
|
|
1693
|
+
const lines = rendered.split("\n").filter((line) => line.trim());
|
|
1694
|
+
for (const line of lines) {
|
|
1695
|
+
if (conf.ready?.some((rx) => rx.test(line))) {
|
|
1696
|
+
logger.debug(`heartbeat|ready |${line}`);
|
|
1697
|
+
ctx.stdinReady.ready();
|
|
1698
|
+
ctx.stdinFirstReady.ready();
|
|
1699
|
+
}
|
|
1700
|
+
if (conf.enter?.some((rx) => rx.test(line))) {
|
|
1701
|
+
logger.debug(`heartbeat|sendEnter matched|${line}`);
|
|
1702
|
+
await sendEnter(ctx.messageContext, 400);
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
const typeingRespondMatched = Object.entries(conf.typingRespond ?? {}).filter(([_sendString, onThePatterns]) => onThePatterns.some((rx) => rx.test(line)));
|
|
1706
|
+
if (typeingRespondMatched.length) {
|
|
1707
|
+
await sflow(typeingRespondMatched).map(async ([sendString]) => await sendMessage(ctx.messageContext, sendString, { waitForReady: false })).toCount();
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
if (conf.fatal?.some((rx) => rx.test(line))) {
|
|
1711
|
+
logger.debug(`heartbeat|fatal |${line}`);
|
|
1712
|
+
ctx.isFatal = true;
|
|
1713
|
+
await exitAgent();
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
if (conf.restartWithoutContinueArg?.some((rx) => rx.test(line))) {
|
|
1717
|
+
logger.debug(`heartbeat|restart-without-continue|${line}`);
|
|
1718
|
+
ctx.shouldRestartWithoutContinue = true;
|
|
1719
|
+
ctx.isFatal = true;
|
|
1720
|
+
await exitAgent();
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
if (cli === "codex") {
|
|
1724
|
+
const sessionId = extractSessionId(line);
|
|
1725
|
+
if (sessionId) {
|
|
1726
|
+
logger.debug(`heartbeat|session|captured session ID: ${sessionId}`);
|
|
1727
|
+
await storeSessionForCwd(workingDir, sessionId);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
logger.debug(`heartbeat|error: ${error}`);
|
|
1733
|
+
}
|
|
1734
|
+
}, 800);
|
|
1735
|
+
const cleanupHeartbeat = () => clearInterval(heartbeatInterval);
|
|
1736
|
+
shell.onExit(cleanupHeartbeat);
|
|
1737
|
+
if (exitOnIdle) ctx.idleWaiter.wait(exitOnIdle).then(async () => {
|
|
1738
|
+
await pidStore.updateStatus(shell.pid, "idle").catch(() => null);
|
|
1739
|
+
if (isStillWorkingQ()) {
|
|
1740
|
+
logger.warn(`[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet`);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
logger.info(`[${cli}-yes] ${cli} is idle, exiting...`);
|
|
1744
|
+
notifyWebhook("IDLE", "", workingDir).catch(() => null);
|
|
1745
|
+
await exitAgent();
|
|
1746
|
+
});
|
|
1747
|
+
const stdinStream = new ReadableStream({
|
|
1748
|
+
start(controller) {
|
|
1749
|
+
process.stdin.resume();
|
|
1750
|
+
let closed = false;
|
|
1751
|
+
const dataHandler = (chunk) => {
|
|
1752
|
+
try {
|
|
1753
|
+
controller.enqueue(chunk);
|
|
1754
|
+
} catch {}
|
|
1755
|
+
};
|
|
1756
|
+
const endHandler = () => {
|
|
1757
|
+
if (closed) return;
|
|
1758
|
+
closed = true;
|
|
1759
|
+
try {
|
|
1760
|
+
controller.close();
|
|
1761
|
+
} catch {}
|
|
1762
|
+
};
|
|
1763
|
+
const errorHandler = (err) => {
|
|
1764
|
+
if (closed) return;
|
|
1765
|
+
closed = true;
|
|
1766
|
+
try {
|
|
1767
|
+
controller.error(err);
|
|
1768
|
+
} catch {}
|
|
1769
|
+
};
|
|
1770
|
+
process.stdin.on("data", dataHandler);
|
|
1771
|
+
process.stdin.on("end", endHandler);
|
|
1772
|
+
process.stdin.on("close", endHandler);
|
|
1773
|
+
process.stdin.on("error", errorHandler);
|
|
1774
|
+
},
|
|
1775
|
+
cancel(reason) {
|
|
1776
|
+
process.stdin.pause();
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
let aborted = false;
|
|
1780
|
+
await sflow(stdinStream).map((buffer) => {
|
|
1781
|
+
const str = buffer.toString();
|
|
1782
|
+
const CTRL_Z = "";
|
|
1783
|
+
const CTRL_C = "";
|
|
1784
|
+
if (!aborted && str === CTRL_Z) return "";
|
|
1785
|
+
if (!aborted && !ctx.stdinReady.isReady && str === CTRL_C) {
|
|
1786
|
+
logger.error("User aborted: SIGINT");
|
|
1787
|
+
shell.kill("SIGINT");
|
|
1788
|
+
pendingExitCode.resolve(130);
|
|
1789
|
+
aborted = true;
|
|
1790
|
+
return str;
|
|
1791
|
+
}
|
|
1792
|
+
if (str === CTRL_C) {
|
|
1793
|
+
userSentCtrlC = true;
|
|
1794
|
+
setTimeout(() => {
|
|
1795
|
+
userSentCtrlC = false;
|
|
1796
|
+
}, 2e3);
|
|
1797
|
+
}
|
|
1798
|
+
return str;
|
|
1799
|
+
}).map((() => {
|
|
1800
|
+
let line = "";
|
|
1801
|
+
const toggleAutoYes = () => {
|
|
1802
|
+
ctx.autoYesEnabled = !ctx.autoYesEnabled;
|
|
1803
|
+
if (!ctx.autoYesEnabled) {
|
|
1804
|
+
ctx.stdinReady.ready();
|
|
1805
|
+
ctx.stdinFirstReady.ready();
|
|
1806
|
+
}
|
|
1807
|
+
const status = ctx.autoYesEnabled ? "\x1B[32m[auto-yes: ON]\x1B[0m" : "\x1B[33m[auto-yes: OFF]\x1B[0m";
|
|
1808
|
+
process.stderr.write(`\r${status} (Ctrl+Y to toggle)\n`);
|
|
1809
|
+
};
|
|
1810
|
+
return (data) => {
|
|
1811
|
+
let out = "";
|
|
1812
|
+
for (const ch of data) {
|
|
1813
|
+
if (ch === "") {
|
|
1814
|
+
toggleAutoYes();
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
if (ch === "\r" || ch === "\n") {
|
|
1818
|
+
if (line.length <= 20) {
|
|
1819
|
+
if (line.replace(/[\x00-\x1f]|\x1b\[[0-9;]*[A-Za-z]|\[[A-Z]/g, "").trim() === "/auto") {
|
|
1820
|
+
out += "";
|
|
1821
|
+
toggleAutoYes();
|
|
1822
|
+
line = "";
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
line = "";
|
|
1827
|
+
out += ch;
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (ch === "" || ch === "\b") {
|
|
1831
|
+
if (line.length > 0) line = line.slice(0, -1);
|
|
1832
|
+
out += ch;
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
if (ch >= " " && ch <= "~" && line.length < 50) line += ch;
|
|
1836
|
+
out += ch;
|
|
1837
|
+
}
|
|
1838
|
+
return out;
|
|
1839
|
+
};
|
|
1840
|
+
})()).onStart(async function promptOnStart() {
|
|
1841
|
+
logger.debug("Sending prompt message: " + JSON.stringify(prompt));
|
|
1842
|
+
if (prompt) await sendMessage(ctx.messageContext, prompt);
|
|
1843
|
+
}).by({
|
|
1844
|
+
writable: new WritableStream({ write: async (data) => {
|
|
1845
|
+
await ctx.stdinReady.wait();
|
|
1846
|
+
shell.write(data);
|
|
1847
|
+
} }),
|
|
1848
|
+
readable: terminalStream.readable
|
|
1849
|
+
}).forEach(() => {
|
|
1850
|
+
ctx.idleWaiter.ping();
|
|
1851
|
+
pidStore.updateStatus(shell.pid, "active").catch(() => null);
|
|
1852
|
+
ctx.nextStdout.ready();
|
|
1853
|
+
}).forkTo(async function rawLogger(f) {
|
|
1854
|
+
const rawLogPath = ctx.logPaths.rawLogPath;
|
|
1855
|
+
if (!rawLogPath) return f.run();
|
|
1856
|
+
return await mkdir$1(path.dirname(rawLogPath), { recursive: true }).then(() => {
|
|
1857
|
+
logger.debug(`[${cli}-yes] raw logs streaming to ${rawLogPath}`);
|
|
1858
|
+
return f.forEach(async (chars) => {
|
|
1859
|
+
await writeFile$1(rawLogPath, chars, { flag: "a" }).catch(() => null);
|
|
1860
|
+
}).run();
|
|
1861
|
+
}).catch(() => f.run());
|
|
1862
|
+
}).by(function consoleResponder(e) {
|
|
1863
|
+
let lastRendered = "";
|
|
1864
|
+
return e.forEach((chunk) => {
|
|
1865
|
+
terminalRender.write(chunk);
|
|
1866
|
+
if (chunk.includes("\x1B[c") || chunk.includes("\x1B[0c")) {
|
|
1867
|
+
shell.write("\x1B[?1;2c");
|
|
1868
|
+
if (verbose) logger.debug("device|respond DA: VT100 with Advanced Video Option");
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
if (process.stdin.isTTY) return;
|
|
1872
|
+
if (!chunk.includes("\x1B[6n")) return;
|
|
1873
|
+
const { col, row } = terminalRender.getCursorPosition();
|
|
1874
|
+
shell.write(`\u001b[${row};${col}R`);
|
|
1875
|
+
logger.debug(`cursor|respond position: row=${String(row)}, col=${String(col)}`);
|
|
1876
|
+
}).forEach(async (line, lineIndex) => {
|
|
1877
|
+
if (terminalRender.tail(24) === lastRendered) return;
|
|
1878
|
+
logger.debug(`stdout|${line}`);
|
|
1879
|
+
if (conf.ready?.some((rx) => line.match(rx))) {
|
|
1880
|
+
logger.debug(`ready |${line}`);
|
|
1881
|
+
if (cli === "gemini" && lineIndex <= 80) return;
|
|
1882
|
+
ctx.stdinReady.ready();
|
|
1883
|
+
ctx.stdinFirstReady.ready();
|
|
1884
|
+
}
|
|
1885
|
+
if (conf.enter?.some((rx) => line.match(rx))) {
|
|
1886
|
+
logger.debug(`sendEnter matched|${line}`);
|
|
1887
|
+
return await sendEnter(ctx.messageContext, 400);
|
|
1888
|
+
}
|
|
1889
|
+
const typeingRespondMatched = Object.entries(conf.typingRespond ?? {}).filter(([_sendString, onThePatterns]) => onThePatterns.some((rx) => line.match(rx)));
|
|
1890
|
+
if (typeingRespondMatched.length && await sflow(typeingRespondMatched).map(async ([sendString]) => await sendMessage(ctx.messageContext, sendString, { waitForReady: false })).toCount()) return;
|
|
1891
|
+
if (conf.fatal?.some((rx) => line.match(rx))) {
|
|
1892
|
+
logger.debug(`fatal |${line}`);
|
|
1893
|
+
ctx.isFatal = true;
|
|
1894
|
+
await exitAgent();
|
|
1895
|
+
}
|
|
1896
|
+
if (conf.restartWithoutContinueArg?.some((rx) => line.match(rx))) {
|
|
1897
|
+
logger.debug(`restart-without-continue|${line}`);
|
|
1898
|
+
ctx.shouldRestartWithoutContinue = true;
|
|
1899
|
+
ctx.isFatal = true;
|
|
1900
|
+
await exitAgent();
|
|
1901
|
+
}
|
|
1902
|
+
if (cli === "codex") {
|
|
1903
|
+
const sessionId = extractSessionId(line);
|
|
1904
|
+
if (sessionId) {
|
|
1905
|
+
logger.debug(`session|captured session ID: ${sessionId}`);
|
|
1906
|
+
await storeSessionForCwd(workingDir, sessionId);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
}).by((s) => removeControlCharactersFromStdout ? s.map((e) => removeControlCharacters(e)) : s).by(createTerminatorStream(pendingExitCode.promise)).to(fromWritable(process.stdout));
|
|
1911
|
+
await saveLogFile(ctx.logPaths.logPath, terminalRender.render());
|
|
1912
|
+
const exitCode = await pendingExitCode.promise;
|
|
1913
|
+
logger.info(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
1914
|
+
await pidStore.close();
|
|
1915
|
+
await outputWriter.close();
|
|
1916
|
+
await saveDeprecatedLogFile(logFile, terminalRender.render(), verbose);
|
|
1917
|
+
return {
|
|
1918
|
+
exitCode,
|
|
1919
|
+
logs: terminalRender.render()
|
|
1920
|
+
};
|
|
1921
|
+
async function exitAgent() {
|
|
1922
|
+
ctx.robust = false;
|
|
1923
|
+
for (const cmd of cliConf.exitCommands ?? ["/exit"]) await sendMessage(ctx.messageContext, cmd);
|
|
1924
|
+
let exited = false;
|
|
1925
|
+
await Promise.race([pendingExitCode.promise.then(() => exited = true), new Promise((resolve) => setTimeout(() => {
|
|
1926
|
+
if (exited) return;
|
|
1927
|
+
shell.kill();
|
|
1928
|
+
resolve();
|
|
1929
|
+
}, 5e3))]);
|
|
1930
|
+
}
|
|
1931
|
+
function getTerminalDimensions() {
|
|
1932
|
+
if (!process.stdout.isTTY) return {
|
|
1933
|
+
cols: 80,
|
|
1934
|
+
rows: 24
|
|
1935
|
+
};
|
|
1936
|
+
return {
|
|
1937
|
+
cols: Math.max(20, process.stdout.columns),
|
|
1938
|
+
rows: process.stdout.rows
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
function sleep(ms) {
|
|
1943
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region ts/SUPPORTED_CLIS.ts
|
|
1948
|
+
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
1949
|
+
|
|
1950
|
+
//#endregion
|
|
1951
|
+
export { AgentContext as a, PidStore as c, config as i, removeControlCharacters as l, CLIS_CONFIG as n, name as o, agentYes as r, version as s, SUPPORTED_CLIS as t };
|
|
1952
|
+
//# sourceMappingURL=SUPPORTED_CLIS-BHDvBHvX.js.map
|