claude-yes 1.32.3 → 1.34.0
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/cli.js +441 -206
- package/dist/index.js +210 -40
- package/package.json +7 -5
- package/ts/cli.ts +22 -5
- package/ts/index.ts +104 -27
- package/ts/logger.ts +0 -1
- package/ts/parseCliArgs.ts +11 -0
- package/ts/pidStore.ts +135 -0
package/ts/index.ts
CHANGED
|
@@ -18,15 +18,17 @@ import { removeControlCharacters } from "./removeControlCharacters.ts";
|
|
|
18
18
|
import { acquireLock, releaseLock, shouldUseLock } from "./runningLock.ts";
|
|
19
19
|
import { logger } from "./logger.ts";
|
|
20
20
|
import { createFifoStream } from "./beta/fifo.ts";
|
|
21
|
+
import { PidStore } from "./pidStore.ts";
|
|
21
22
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
22
23
|
import winston from "winston";
|
|
23
|
-
import { mapObject, pipe } from "rambda";
|
|
24
24
|
|
|
25
25
|
export { removeControlCharacters };
|
|
26
26
|
|
|
27
27
|
export type AgentCliConfig = {
|
|
28
28
|
// cli
|
|
29
|
-
install?:
|
|
29
|
+
install?:
|
|
30
|
+
| string
|
|
31
|
+
| { powershell?: string; bash?: string; npm?: string; unix?: string; windows?: string }; // hint user for install command if not installed
|
|
30
32
|
version?: string; // hint user for version command to check if installed
|
|
31
33
|
binary?: string; // actual binary name if different from cli, e.g. cursor -> cursor-agent
|
|
32
34
|
defaultArgs?: string[]; // function to ensure certain args are present
|
|
@@ -167,6 +169,10 @@ export default async function agentYes({
|
|
|
167
169
|
});
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
// Initialize process registry
|
|
173
|
+
const pidStore = new PidStore(workingDir);
|
|
174
|
+
await pidStore.init();
|
|
175
|
+
|
|
170
176
|
process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
|
|
171
177
|
|
|
172
178
|
let isFatal = false; // when true, do not restart on crash, and exit agent
|
|
@@ -187,23 +193,10 @@ export default async function agentYes({
|
|
|
187
193
|
|
|
188
194
|
logger.debug(`Using ${ptyPackage} for pseudo terminal management.`);
|
|
189
195
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
);
|
|
196
|
+
let logPath: string | false = false;
|
|
197
|
+
let rawLogPath: string | false = false;
|
|
198
|
+
let rawLinesLogPath: string | false = false;
|
|
199
|
+
let debuggingLogsPath: string | false = false;
|
|
207
200
|
|
|
208
201
|
// Detect if running as sub-agent
|
|
209
202
|
const isSubAgent = !!process.env.CLAUDE_PPID;
|
|
@@ -336,6 +329,41 @@ export default async function agentYes({
|
|
|
336
329
|
}
|
|
337
330
|
// Determine the actual cli command to run
|
|
338
331
|
|
|
332
|
+
// Helper function to get install command based on platform/availability
|
|
333
|
+
const getInstallCommand = (
|
|
334
|
+
installConfig:
|
|
335
|
+
| string
|
|
336
|
+
| { powershell?: string; bash?: string; npm?: string; unix?: string; windows?: string },
|
|
337
|
+
): string | null => {
|
|
338
|
+
if (typeof installConfig === "string") {
|
|
339
|
+
return installConfig;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const isWindows = process.platform === "win32";
|
|
343
|
+
const platform = isWindows ? "windows" : "unix";
|
|
344
|
+
|
|
345
|
+
// Try platform-specific commands first
|
|
346
|
+
if (installConfig[platform]) {
|
|
347
|
+
return installConfig[platform];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Try shell-specific commands
|
|
351
|
+
if (isWindows && installConfig.powershell) {
|
|
352
|
+
return installConfig.powershell;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!isWindows && installConfig.bash) {
|
|
356
|
+
return installConfig.bash;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Fallback to npm if available
|
|
360
|
+
if (installConfig.npm) {
|
|
361
|
+
return installConfig.npm;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null;
|
|
365
|
+
};
|
|
366
|
+
|
|
339
367
|
const spawn = () => {
|
|
340
368
|
const cliCommand = cliConf?.binary || cli;
|
|
341
369
|
let [bin, ...args] = [...parseCommandString(cliCommand), ...cliArgs];
|
|
@@ -356,17 +384,21 @@ export default async function agentYes({
|
|
|
356
384
|
|
|
357
385
|
const isNotFound = isCommandNotFoundError(error);
|
|
358
386
|
if (cliConf?.install && isNotFound) {
|
|
359
|
-
|
|
387
|
+
const installCmd = getInstallCommand(cliConf.install);
|
|
388
|
+
if (!installCmd) {
|
|
389
|
+
logger.error(`No suitable install command found for ${cli} on this platform`);
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
logger.info(`Please install the cli by run ${installCmd}`);
|
|
360
394
|
|
|
361
395
|
if (install) {
|
|
362
396
|
logger.info(`Attempting to install ${cli}...`);
|
|
363
|
-
execaCommandSync(
|
|
397
|
+
execaCommandSync(installCmd, { stdio: "inherit" });
|
|
364
398
|
logger.info(`${cli} installed successfully. Please rerun the command.`);
|
|
365
399
|
return spawn();
|
|
366
400
|
} else {
|
|
367
|
-
logger.error(
|
|
368
|
-
`If you did not installed it yet, Please install it first: ${cliConf.install}`,
|
|
369
|
-
);
|
|
401
|
+
logger.error(`If you did not installed it yet, Please install it first: ${installCmd}`);
|
|
370
402
|
throw error;
|
|
371
403
|
}
|
|
372
404
|
}
|
|
@@ -392,6 +424,22 @@ export default async function agentYes({
|
|
|
392
424
|
},
|
|
393
425
|
spawn,
|
|
394
426
|
)();
|
|
427
|
+
|
|
428
|
+
// Register process in pidStore and compute log paths
|
|
429
|
+
await pidStore.registerProcess({ pid: shell.pid, cli, args: cliArgs, prompt });
|
|
430
|
+
logPath = pidStore.getLogPath(shell.pid);
|
|
431
|
+
rawLogPath = path.resolve(path.dirname(logPath), `${shell.pid}.raw.log`);
|
|
432
|
+
rawLinesLogPath = path.resolve(path.dirname(logPath), `${shell.pid}.lines.log`);
|
|
433
|
+
debuggingLogsPath = path.resolve(path.dirname(logPath), `${shell.pid}.debug.log`);
|
|
434
|
+
|
|
435
|
+
if (debuggingLogsPath)
|
|
436
|
+
logger.add(
|
|
437
|
+
new winston.transports.File({
|
|
438
|
+
filename: debuggingLogsPath,
|
|
439
|
+
level: "debug",
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
|
|
395
443
|
const pendingExitCode = Promise.withResolvers<number | null>();
|
|
396
444
|
|
|
397
445
|
async function onData(data: string) {
|
|
@@ -407,6 +455,10 @@ export default async function agentYes({
|
|
|
407
455
|
// Handle restart without continue args (e.g., "No conversation found to continue")
|
|
408
456
|
// logger.debug(``, { shouldRestartWithoutContinue, robust })
|
|
409
457
|
if (shouldRestartWithoutContinue) {
|
|
458
|
+
await pidStore.updateStatus(shell.pid, "exited", {
|
|
459
|
+
exitReason: "restarted",
|
|
460
|
+
exitCode: exitCode ?? undefined,
|
|
461
|
+
});
|
|
410
462
|
shouldRestartWithoutContinue = false; // reset flag
|
|
411
463
|
isFatal = false; // reset fatal flag to allow restart
|
|
412
464
|
|
|
@@ -419,6 +471,7 @@ export default async function agentYes({
|
|
|
419
471
|
logger.info(`Restarting ${cli} ${JSON.stringify([bin, ...args])}`);
|
|
420
472
|
|
|
421
473
|
shell = pty.spawn(bin!, args, getPtyOptions());
|
|
474
|
+
await pidStore.registerProcess({ pid: shell.pid, cli, args, prompt });
|
|
422
475
|
shell.onData(onData);
|
|
423
476
|
shell.onExit(onExit);
|
|
424
477
|
return;
|
|
@@ -434,8 +487,18 @@ export default async function agentYes({
|
|
|
434
487
|
);
|
|
435
488
|
return;
|
|
436
489
|
}
|
|
437
|
-
if (isFatal)
|
|
490
|
+
if (isFatal) {
|
|
491
|
+
await pidStore.updateStatus(shell.pid, "exited", {
|
|
492
|
+
exitReason: "fatal",
|
|
493
|
+
exitCode: exitCode ?? undefined,
|
|
494
|
+
});
|
|
495
|
+
return pendingExitCode.resolve(exitCode);
|
|
496
|
+
}
|
|
438
497
|
|
|
498
|
+
await pidStore.updateStatus(shell.pid, "exited", {
|
|
499
|
+
exitReason: "restarted",
|
|
500
|
+
exitCode: exitCode ?? undefined,
|
|
501
|
+
});
|
|
439
502
|
logger.info(`${cli} crashed, restarting...`);
|
|
440
503
|
|
|
441
504
|
// For codex, try to use stored session ID for this directory
|
|
@@ -452,10 +515,16 @@ export default async function agentYes({
|
|
|
452
515
|
}
|
|
453
516
|
|
|
454
517
|
shell = pty.spawn(cli, restoreArgs, getPtyOptions());
|
|
518
|
+
await pidStore.registerProcess({ pid: shell.pid, cli, args: restoreArgs, prompt });
|
|
455
519
|
shell.onData(onData);
|
|
456
520
|
shell.onExit(onExit);
|
|
457
521
|
return;
|
|
458
522
|
}
|
|
523
|
+
const exitReason = agentCrashed ? "crash" : "normal";
|
|
524
|
+
await pidStore.updateStatus(shell.pid, "exited", {
|
|
525
|
+
exitReason,
|
|
526
|
+
exitCode: exitCode ?? undefined,
|
|
527
|
+
});
|
|
459
528
|
return pendingExitCode.resolve(exitCode);
|
|
460
529
|
});
|
|
461
530
|
|
|
@@ -475,6 +544,7 @@ export default async function agentYes({
|
|
|
475
544
|
const idleWaiter = new IdleWaiter();
|
|
476
545
|
if (exitOnIdle)
|
|
477
546
|
idleWaiter.wait(exitOnIdle).then(async () => {
|
|
547
|
+
await pidStore.updateStatus(shell.pid, "idle").catch(() => null);
|
|
478
548
|
if (isStillWorkingQ()) {
|
|
479
549
|
logger.warn("[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet");
|
|
480
550
|
return;
|
|
@@ -513,9 +583,10 @@ export default async function agentYes({
|
|
|
513
583
|
// read from FIFO if available, e.g. /tmp/agent-yes-*.stdin, which can be used to send additional input from other processes
|
|
514
584
|
.by((s) => {
|
|
515
585
|
if (!useFifo) return s;
|
|
516
|
-
const fifoResult = createFifoStream(cli);
|
|
586
|
+
const fifoResult = createFifoStream(cli, pidStore.getFifoPath(shell.pid));
|
|
517
587
|
if (!fifoResult) return s;
|
|
518
588
|
pendingExitCode.promise.finally(() => fifoResult.cleanup());
|
|
589
|
+
process.stderr.write(`\n Append prompts: ${cli}-yes --append-prompt '...'\n\n`);
|
|
519
590
|
return s.merge(fifoResult.stream);
|
|
520
591
|
})
|
|
521
592
|
|
|
@@ -539,7 +610,10 @@ export default async function agentYes({
|
|
|
539
610
|
readable: shellOutputStream.readable,
|
|
540
611
|
})
|
|
541
612
|
|
|
542
|
-
.forEach(() =>
|
|
613
|
+
.forEach(() => {
|
|
614
|
+
idleWaiter.ping();
|
|
615
|
+
pidStore.updateStatus(shell.pid, "active").catch(() => null);
|
|
616
|
+
})
|
|
543
617
|
.forEach(() => nextStdout.ready())
|
|
544
618
|
|
|
545
619
|
.forkTo(async function rawLogger(f) {
|
|
@@ -695,6 +769,9 @@ export default async function agentYes({
|
|
|
695
769
|
const exitCode = await pendingExitCode.promise;
|
|
696
770
|
logger.info(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
697
771
|
|
|
772
|
+
// Final pidStore cleanup
|
|
773
|
+
await pidStore.close();
|
|
774
|
+
|
|
698
775
|
// Update task status.writable release lock
|
|
699
776
|
await outputWriter.close();
|
|
700
777
|
|
package/ts/logger.ts
CHANGED
package/ts/parseCliArgs.ts
CHANGED
|
@@ -88,6 +88,15 @@ export function parseCliArgs(argv: string[]) {
|
|
|
88
88
|
default: false,
|
|
89
89
|
alias: "c",
|
|
90
90
|
})
|
|
91
|
+
.option("append-prompt", {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Send a prompt to the active agent's FIFO stdin in current directory",
|
|
94
|
+
})
|
|
95
|
+
.option("fifo", {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: "Enable FIFO input stream for additional stdin input (Linux only)",
|
|
98
|
+
default: false,
|
|
99
|
+
})
|
|
91
100
|
.positional("cli", {
|
|
92
101
|
describe: "The AI CLI to run, e.g., claude, codex, copilot, cursor, gemini",
|
|
93
102
|
type: "string",
|
|
@@ -180,5 +189,7 @@ export function parseCliArgs(argv: string[]) {
|
|
|
180
189
|
verbose: parsedArgv.verbose,
|
|
181
190
|
resume: parsedArgv.continue, // Note: intentional use resume here to avoid preserved keyword (continue)
|
|
182
191
|
useSkills: parsedArgv.useSkills,
|
|
192
|
+
appendPrompt: parsedArgv.appendPrompt,
|
|
193
|
+
useFifo: parsedArgv.fifo,
|
|
183
194
|
};
|
|
184
195
|
}
|
package/ts/pidStore.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import Datastore from "@seald-io/nedb";
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { logger } from "./logger.ts";
|
|
5
|
+
|
|
6
|
+
export interface PidRecord {
|
|
7
|
+
pid: number;
|
|
8
|
+
cli: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
prompt?: string;
|
|
11
|
+
logFile: string;
|
|
12
|
+
fifoFile: string;
|
|
13
|
+
status: "idle" | "active" | "exited";
|
|
14
|
+
exitReason: string;
|
|
15
|
+
exitCode?: number;
|
|
16
|
+
startedAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PidStore {
|
|
21
|
+
protected db!: Datastore<PidRecord>;
|
|
22
|
+
private baseDir: string;
|
|
23
|
+
|
|
24
|
+
constructor(workingDir: string) {
|
|
25
|
+
this.baseDir = path.resolve(workingDir, ".agent-yes");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async init(): Promise<void> {
|
|
29
|
+
await mkdir(path.join(this.baseDir, "logs"), { recursive: true });
|
|
30
|
+
await mkdir(path.join(this.baseDir, "fifo"), { recursive: true });
|
|
31
|
+
|
|
32
|
+
this.db = new Datastore<PidRecord>({
|
|
33
|
+
filename: path.join(this.baseDir, "pid.jsonl"),
|
|
34
|
+
autoload: true,
|
|
35
|
+
});
|
|
36
|
+
await this.db.loadDatabaseAsync();
|
|
37
|
+
await this.cleanStaleRecords();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async registerProcess({
|
|
41
|
+
pid,
|
|
42
|
+
cli,
|
|
43
|
+
args,
|
|
44
|
+
prompt,
|
|
45
|
+
}: {
|
|
46
|
+
pid: number;
|
|
47
|
+
cli: string;
|
|
48
|
+
args: string[];
|
|
49
|
+
prompt?: string;
|
|
50
|
+
}): Promise<PidRecord> {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const record: PidRecord = {
|
|
53
|
+
pid,
|
|
54
|
+
cli,
|
|
55
|
+
args,
|
|
56
|
+
prompt,
|
|
57
|
+
logFile: this.getLogPath(pid),
|
|
58
|
+
fifoFile: this.getFifoPath(pid),
|
|
59
|
+
status: "active",
|
|
60
|
+
exitReason: "",
|
|
61
|
+
startedAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
};
|
|
64
|
+
await this.db.insertAsync(record);
|
|
65
|
+
logger.debug(`[pidStore] Registered process ${pid}`);
|
|
66
|
+
return record;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async updateStatus(
|
|
70
|
+
pid: number,
|
|
71
|
+
status: PidRecord["status"],
|
|
72
|
+
extra?: { exitReason?: string; exitCode?: number },
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const update: Partial<PidRecord> = {
|
|
75
|
+
status,
|
|
76
|
+
updatedAt: Date.now(),
|
|
77
|
+
...extra,
|
|
78
|
+
};
|
|
79
|
+
await this.db.updateAsync({ pid }, { $set: update }, {});
|
|
80
|
+
logger.debug(`[pidStore] Updated process ${pid} status=${status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getLogPath(pid: number): string {
|
|
84
|
+
return path.resolve(this.baseDir, "logs", `${pid}.log`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getFifoPath(pid: number): string {
|
|
88
|
+
return path.resolve(this.baseDir, "fifo", `${pid}.stdin`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async cleanStaleRecords(): Promise<void> {
|
|
92
|
+
const activeRecords = await this.db.findAsync({
|
|
93
|
+
status: { $ne: "exited" } as any,
|
|
94
|
+
});
|
|
95
|
+
for (const record of activeRecords) {
|
|
96
|
+
if (!this.isProcessAlive(record.pid)) {
|
|
97
|
+
await this.db.updateAsync(
|
|
98
|
+
{ pid: record.pid },
|
|
99
|
+
{
|
|
100
|
+
$set: {
|
|
101
|
+
status: "exited" as const,
|
|
102
|
+
exitReason: "stale-cleanup",
|
|
103
|
+
updatedAt: Date.now(),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{},
|
|
107
|
+
);
|
|
108
|
+
logger.debug(`[pidStore] Cleaned stale record for PID ${record.pid}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async close(): Promise<void> {
|
|
114
|
+
await this.db.compactDatafileAsync();
|
|
115
|
+
logger.debug("[pidStore] Database compacted and closed");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private isProcessAlive(pid: number): boolean {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static async findActiveFifo(workingDir: string): Promise<string | null> {
|
|
128
|
+
const store = new PidStore(workingDir);
|
|
129
|
+
await store.init();
|
|
130
|
+
const records = await store.db.findAsync({ status: { $ne: "exited" } as any });
|
|
131
|
+
await store.close();
|
|
132
|
+
const sorted = records.sort((a, b) => b.startedAt - a.startedAt);
|
|
133
|
+
return sorted[0]?.fifoFile ?? null;
|
|
134
|
+
}
|
|
135
|
+
}
|