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/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?: string; // hint user for install command if not installed
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
- const datetime = new Date().toISOString().replace(/\D/g, "").slice(0, 17);
191
- const logPath = config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.log`);
192
- const rawLogPath =
193
- config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.raw.log`);
194
- const rawLinesLogPath =
195
- config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.lines.log`);
196
- const debuggingLogsPath =
197
- config.logsDir && path.resolve(config.logsDir, `${cli}-yes-${datetime}.debug.log`);
198
-
199
- // add
200
- if (debuggingLogsPath)
201
- logger.add(
202
- new winston.transports.File({
203
- filename: debuggingLogsPath,
204
- level: "debug",
205
- }),
206
- );
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
- logger.info(`Please install the cli by run ${cliConf.install}`);
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(cliConf.install, { stdio: "inherit" });
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) return pendingExitCode.resolve(exitCode);
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(() => idleWaiter.ping())
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
@@ -19,4 +19,3 @@ export const logger = winston.createLogger({
19
19
  ],
20
20
  silent: false,
21
21
  });
22
-
@@ -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
+ }