claude-yes 1.24.2 → 1.26.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
@@ -5,6 +5,8 @@ import DIE from 'phpdie';
5
5
  import sflow from 'sflow';
6
6
  import { TerminalTextRender } from 'terminal-render';
7
7
  import tsaComposer from 'tsa-composer';
8
+ import rawConfig from '../cli-yes.config.js';
9
+ import { defineCliYesConfig } from './defineConfig.js';
8
10
  import { IdleWaiter } from './idleWaiter';
9
11
  import { ReadyManager } from './ReadyManager';
10
12
  import { removeControlCharacters } from './removeControlCharacters';
@@ -14,73 +16,36 @@ import {
14
16
  shouldUseLock,
15
17
  updateCurrentTaskStatus,
16
18
  } from './runningLock';
17
- // const yesLog = tsaComposer()(async function yesLog(msg: string) {
18
- // // await rm('agent-yes.log').catch(() => null); // ignore error if file doesn't exist
19
- // await appendFile("agent-yes.log", `${msg}\n`).catch(() => null);
20
- // });
21
-
22
- export const CLI_CONFIGURES: Record<
23
- string,
24
- {
25
- install?: string; // hint user for install command if not installed
26
- binary?: string; // actual binary name if different from cli
27
- ready?: RegExp[]; // regex matcher for stdin ready, or line index for gemini
28
- enter?: RegExp[]; // array of regex to match for sending Enter
29
- fatal?: RegExp[]; // array of regex to match for fatal errors
30
- ensureArgs?: (args: string[]) => string[]; // function to ensure certain args are present
31
- }
32
- > = {
33
- grok: {
34
- install: 'npm install -g @vibe-kit/grok-cli',
35
- ready: [/^ │ ❯ /],
36
- enter: [/^ 1. Yes/],
37
- },
38
- claude: {
39
- install: 'npm install -g @anthropic-ai/claude-code',
40
- // ready: [/^> /], // regex matcher for stdin ready
41
- ready: [/\? for shortcuts/], // regex matcher for stdin ready
42
- enter: [/❯ 1. Yes/, /❯ 1. Dark mode✔/, /Press Enter to continue…/],
43
- fatal: [
44
- /No conversation found to continue/,
45
- /⎿ Claude usage limit reached\./,
46
- ],
47
- },
48
- gemini: {
49
- install: 'npm install -g @google/gemini-cli',
50
- // match the agent prompt after initial lines; handled by index logic using line index
51
- ready: [/Type your message/], // used with line index check
52
- enter: [/│ ● 1. Yes, allow once/],
53
- fatal: [],
54
- },
55
- codex: {
56
- install: 'npm install -g @openai/codex-cli',
57
- ready: [/⏎ send/],
58
- enter: [
59
- /> 1. Yes, allow Codex to work in this folder/,
60
- /> 1. Approve and run now/,
61
- ],
62
- fatal: [/Error: The cursor position could not be read within/],
63
- // add to codex --search by default when not provided by the user
64
- ensureArgs: (args: string[]) => {
65
- if (!args.includes('--search')) return ['--search', ...args];
66
- return args;
67
- },
68
- },
69
- copilot: {
70
- install: 'npm install -g @github/copilot',
71
- ready: [/^ +> /, /Ctrl\+c Exit/],
72
- enter: [/ │ ❯ 1. Yes, proceed/, /❯ 1. Yes/],
73
- fatal: [],
74
- },
75
- cursor: {
76
- install: 'open https://cursor.com/ja/docs/cli/installation',
77
- // map logical "cursor" cli name to actual binary name
78
- binary: 'cursor-agent',
79
- ready: [/\/ commands/],
80
- enter: [/→ Run \(once\) \(y\) \(enter\)/, /▶ \[a\] Trust this workspace/],
81
- fatal: [/^ Error: You've hit your usage limit/],
82
- },
19
+ import { catcher } from './tryCatch';
20
+ import { deepMixin } from './utils';
21
+ import { yesLog } from './yesLog';
22
+
23
+ export type AgentCliConfig = {
24
+ install?: string; // hint user for install command if not installed
25
+ version?: string; // hint user for version command to check if installed
26
+ binary?: string; // actual binary name if different from cli, e.g. cursor -> cursor-agent
27
+ ready?: RegExp[]; // regex matcher for stdin ready, or line index for gemini
28
+ enter?: RegExp[]; // array of regex to match for sending Enter
29
+ fatal?: RegExp[]; // array of regex to match for fatal errors
30
+ restoreArgs?: string[]; // arguments to continue the session when crashed
31
+ defaultArgs?: string[]; // function to ensure certain args are present
32
+ noEOL?: boolean; // if true, do not split lines by \n, used for codex, which uses cursor-move csi code instead of \n to move lines
33
+ promptArg?: (string & {}) | 'first-arg' | 'last-arg'; // argument name to pass the prompt, e.g. --prompt, or first-arg for positional arg
34
+ };
35
+ export type CliYesConfig = {
36
+ clis: { [key: string]: AgentCliConfig };
83
37
  };
38
+
39
+ // load user config from cli-yes.config.ts if exists
40
+ export const config = await rawConfig;
41
+
42
+ export const CLIS_CONFIG = config.clis as Record<
43
+ keyof Awaited<typeof config>['clis'],
44
+ AgentCliConfig
45
+ >;
46
+ export type SUPPORTED_CLIS = keyof typeof CLIS_CONFIG;
47
+ export const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG) as SUPPORTED_CLIS[];
48
+
84
49
  /**
85
50
  * Main function to run agent-cli with automatic yes/no responses
86
51
  * @param options Configuration options
@@ -96,77 +61,86 @@ export const CLI_CONFIGURES: Record<
96
61
  *
97
62
  * @example
98
63
  * ```typescript
99
- * import claudeYes from 'claude-yes';
100
- * await claudeYes({
64
+ * import cliYes from 'cli-yes';
65
+ * await cliYes({
101
66
  * prompt: 'help me solve all todos in my codebase',
102
67
  *
103
68
  * // optional
104
- * cli: 'claude',
105
- * cliArgs: ['--verbose'], // additional args to pass to claude
69
+ * cliArgs: ['--verbose'], // additional args to pass to agent-cli
106
70
  * exitOnIdle: 30000, // exit after 30 seconds of idle
107
- * continueOnCrash: true, // restart if claude crashes, default is true
71
+ * robust: true, // auto restart with --continue if claude crashes, default is true
108
72
  * logFile: 'claude.log', // save logs to file
109
73
  * disableLock: false, // disable running lock (default is false)
110
74
  * });
111
75
  * ```
112
76
  */
113
- export default async function claudeYes({
114
- cli = 'claude',
77
+ export default async function cliYes({
78
+ cli,
115
79
  cliArgs = [],
116
80
  prompt,
117
- // continueOnCrash,
81
+ robust = true,
118
82
  cwd,
119
83
  env,
120
84
  exitOnIdle,
121
85
  logFile,
122
86
  removeControlCharactersFromStdout = false, // = !process.stdout.isTTY,
123
87
  verbose = false,
124
- disableLock = false,
88
+ queue = true,
125
89
  }: {
126
- cli?: (string & {}) | keyof typeof CLI_CONFIGURES;
90
+ cli: SUPPORTED_CLIS;
127
91
  cliArgs?: string[];
128
92
  prompt?: string;
129
- // continueOnCrash?: boolean;
93
+ robust?: boolean;
130
94
  cwd?: string;
131
95
  env?: Record<string, string>;
132
96
  exitOnIdle?: number;
133
97
  logFile?: string;
134
98
  removeControlCharactersFromStdout?: boolean;
135
99
  verbose?: boolean;
136
- disableLock?: boolean;
137
- } = {}) {
138
- const continueArgs = {
139
- codex: 'resume --last'.split(' '),
140
- claude: '--continue'.split(' '),
141
- gemini: [], // not possible yet
142
- };
100
+ queue?: boolean;
101
+ }) {
102
+ // those overrides seems only works in bun
103
+ // await Promise.allSettled([
104
+ // import(path.join(process.cwd(), "cli-yes.config")),
105
+ // ])
106
+ // .then((e) => e.flatMap((e) => (e.status === "fulfilled" ? [e.value] : [])))
107
+ // .then(e=>e.at(0))
108
+ // .then((e) => e.default as ReturnType<typeof defineCliYesConfig>)
109
+ // .then(async (override) => deepMixin(config, override || {}))
110
+ // .catch((error) => {
111
+ // if (process.env.VERBOSE)
112
+ // console.warn("Fail to load cli-yes.config.ts", error);
113
+ // });
114
+
115
+ if (!cli) throw new Error(`cli is required`);
116
+ const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}`);
143
117
 
144
118
  // Acquire lock before starting agent (if in git repo or same cwd and lock is not disabled)
145
119
  const workingDir = cwd ?? process.cwd();
146
- if (!disableLock && shouldUseLock(workingDir)) {
147
- await acquireLock(workingDir, prompt ?? 'Interactive session');
148
- }
149
-
150
- // Register cleanup handlers for lock release
151
- const cleanupLock = async () => {
152
- if (!disableLock && shouldUseLock(workingDir)) {
153
- await releaseLock().catch(() => null); // Ignore errors during cleanup
120
+ if (queue) {
121
+ if (queue && shouldUseLock(workingDir)) {
122
+ await acquireLock(workingDir, prompt ?? 'Interactive session');
154
123
  }
155
- };
156
124
 
157
- process.on('exit', () => {
158
- if (!disableLock) {
159
- releaseLock().catch(() => null);
160
- }
161
- });
162
- process.on('SIGINT', async () => {
163
- await cleanupLock();
164
- process.exit(130);
165
- });
166
- process.on('SIGTERM', async () => {
167
- await cleanupLock();
168
- process.exit(143);
169
- });
125
+ // Register cleanup handlers for lock release
126
+ const cleanupLock = async () => {
127
+ if (queue && shouldUseLock(workingDir)) {
128
+ await releaseLock().catch(() => null); // Ignore errors during cleanup
129
+ }
130
+ };
131
+
132
+ process.on('exit', () => {
133
+ if (queue) releaseLock().catch(() => null);
134
+ });
135
+ process.on('SIGINT', async (code) => {
136
+ await cleanupLock();
137
+ process.exit(code);
138
+ });
139
+ process.on('SIGTERM', async (code) => {
140
+ await cleanupLock();
141
+ process.exit(code);
142
+ });
143
+ }
170
144
 
171
145
  process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
172
146
  let isFatal = false; // when true, do not restart on crash, and exit agent
@@ -192,21 +166,49 @@ export default async function claudeYes({
192
166
  });
193
167
 
194
168
  // Apply CLI specific configurations (moved to CLI_CONFIGURES)
195
- const cliConf = (CLI_CONFIGURES as Record<string, any>)[cli] || {};
196
- cliArgs = cliConf.ensureArgs?.(cliArgs) ?? cliArgs;
169
+ const cliConf = (CLIS_CONFIG as Record<string, AgentCliConfig>)[cli] || {};
170
+ cliArgs = cliConf.defaultArgs
171
+ ? [...cliConf.defaultArgs, ...cliArgs]
172
+ : cliArgs;
173
+
174
+ if (prompt && cliConf.promptArg) {
175
+ if (cliConf.promptArg === 'first-arg') {
176
+ cliArgs = [prompt, ...cliArgs];
177
+ prompt = undefined; // clear prompt to avoid sending later
178
+ } else if (cliConf.promptArg === 'last-arg') {
179
+ cliArgs = [...cliArgs, prompt];
180
+ prompt = undefined; // clear prompt to avoid sending later
181
+ } else if (cliConf.promptArg.startsWith('--')) {
182
+ cliArgs = [cliConf.promptArg, prompt, ...cliArgs];
183
+ prompt = undefined; // clear prompt to avoid sending later
184
+ } else {
185
+ console.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
186
+ }
187
+ }
197
188
  const cliCommand = cliConf?.binary || cli;
198
189
 
199
- let shell = tryCatch(
200
- () => pty.spawn(cliCommand, cliArgs, getPtyOptions()),
190
+ let shell = catcher(
201
191
  (error: unknown) => {
202
192
  console.error(`Fatal: Failed to start ${cliCommand}.`);
203
- if (cliConf?.install)
193
+ if (cliConf?.install && isCommandNotFoundError(error))
204
194
  console.error(
205
195
  `If you did not installed it yet, Please install it first: ${cliConf.install}`,
206
196
  );
207
197
  throw error;
198
+
199
+ function isCommandNotFoundError(e: unknown) {
200
+ if (e instanceof Error) {
201
+ return (
202
+ e.message.includes('command not found') ||
203
+ e.message.includes('ENOENT') ||
204
+ e.message.includes('spawn') // windows
205
+ );
206
+ }
207
+ return false;
208
+ }
208
209
  },
209
- );
210
+ () => pty.spawn(cliCommand, cliArgs, getPtyOptions()),
211
+ )();
210
212
  const pendingExitCode = Promise.withResolvers<number | null>();
211
213
  let pendingExitCodeValue = null;
212
214
 
@@ -214,35 +216,35 @@ export default async function claudeYes({
214
216
  // npm install -g @anthropic-ai/claude-code
215
217
 
216
218
  async function onData(data: string) {
217
- nextStdout.ready();
218
219
  // append data to the buffer, so we can process it later
219
220
  await outputWriter.write(data);
220
221
  }
221
222
 
222
223
  shell.onData(onData);
223
224
  shell.onExit(function onExit({ exitCode }) {
224
- nextStdout.ready();
225
225
  stdinReady.unready(); // start buffer stdin
226
226
  const agentCrashed = exitCode !== 0;
227
- const continueArg = (continueArgs as Record<string, string[]>)[cli];
228
-
229
- // if (agentCrashed && continueOnCrash && continueArg) {
230
- // if (!continueArg) {
231
- // return console.warn(
232
- // `continueOnCrash is only supported for ${Object.keys(continueArgs).join(", ")} currently, not ${cli}`
233
- // );
234
- // }
235
- // if (isFatal) {
236
- // return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
237
- // }
238
-
239
- // console.log(`${cli} crashed, restarting...`);
240
-
241
- // shell = pty.spawn(cli, continueArg, getPtyOptions());
242
- // shell.onData(onData);
243
- // shell.onExit(onExit);
244
- // return;
245
- // }
227
+
228
+ if (agentCrashed && robust && conf?.restoreArgs) {
229
+ if (!conf.restoreArgs) {
230
+ return console.warn(
231
+ `robust is only supported for ${Object.entries(CLIS_CONFIG)
232
+ .filter(([_, v]) => v.restoreArgs)
233
+ .map(([k]) => k)
234
+ .join(', ')} currently, not ${cli}`,
235
+ );
236
+ }
237
+ if (isFatal) {
238
+ return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
239
+ }
240
+
241
+ console.log(`${cli} crashed, restarting...`);
242
+
243
+ shell = pty.spawn(cli, conf.restoreArgs, getPtyOptions());
244
+ shell.onData(onData);
245
+ shell.onExit(onExit);
246
+ return;
247
+ }
246
248
  return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
247
249
  });
248
250
 
@@ -294,7 +296,8 @@ export default async function claudeYes({
294
296
  readable: shellOutputStream.readable,
295
297
  })
296
298
  .forEach(() => idleWaiter.ping())
297
- .forEach((text) => {
299
+ .forEach(() => nextStdout.ready())
300
+ .forEach(async (text) => {
298
301
  terminalRender.write(text);
299
302
  // todo: .onStatus((msg)=> shell.write(msg))
300
303
  if (process.stdin.isTTY) return; // only handle it when stdin is not tty
@@ -306,7 +309,7 @@ export default async function claudeYes({
306
309
  // const rendered = terminalRender.render();
307
310
  const { col, row } = terminalRender.getCursorPosition();
308
311
  shell.write(`\u001b[${row};${col}R`); // reply cli when getting cursor position
309
- // await yesLog(`cursor|respond position: row=${row}, col=${col}`);
312
+ await yesLog`cursor|respond position: row=${String(row)}, col=${String(col)}`;
310
313
  // const row = rendered.split('\n').length + 1;
311
314
  // const col = (rendered.split('\n').slice(-1)[0]?.length || 0) + 1;
312
315
  })
@@ -317,33 +320,29 @@ export default async function claudeYes({
317
320
  .map((e) => removeControlCharacters(e))
318
321
  .map((e) => e.replaceAll('\r', '')) // remove carriage return
319
322
  .by((s) => {
320
- if (cli === 'codex') return s; // codex use cursor-move csi code insteadof \n to move lines, so the output have no \n at all, this hack prevents stuck on unended line
323
+ if (conf.noEOL) return s; // codex use cursor-move csi code insteadof \n to move lines, so the output have no \n at all, this hack prevents stuck on unended line
321
324
  return s.lines({ EOL: 'NONE' }); // other clis use ink, which is rerendering the block based on \n lines
322
325
  })
323
- // .forEach((e) => yesLog`output|${e}`) // for debugging
326
+ .forEach((e) => yesLog`output|${e}`) // for debugging
324
327
  // Generic auto-response handler driven by CLI_CONFIGURES
325
328
  .forEach(async (e, i) => {
326
- const conf =
327
- CLI_CONFIGURES[cli as keyof typeof CLI_CONFIGURES] || null;
328
- if (!conf) return;
329
-
330
329
  // ready matcher: if matched, mark stdin ready
331
330
  if (conf.ready?.some((rx: RegExp) => e.match(rx))) {
332
- // await yesLog`ready |${e}`;
331
+ await yesLog`ready |${e}`;
333
332
  if (cli === 'gemini' && i <= 80) return; // gemini initial noise, only after many lines
334
333
  stdinReady.ready();
335
334
  }
336
335
 
337
336
  // enter matchers: send Enter when any enter regex matches
338
337
  if (conf.enter?.some((rx: RegExp) => e.match(rx))) {
339
- // await yesLog`enter |${e}`;
338
+ await yesLog`enter |${e}`;
340
339
  await sendEnter(300); // send Enter after 300ms idle wait
341
340
  return;
342
341
  }
343
342
 
344
343
  // fatal matchers: set isFatal flag when matched
345
344
  if (conf.fatal?.some((rx: RegExp) => e.match(rx))) {
346
- // await yesLog`fatal |${e}`;
345
+ await yesLog`fatal |${e}`;
347
346
  isFatal = true;
348
347
  await exitAgent();
349
348
  }
@@ -364,7 +363,7 @@ export default async function claudeYes({
364
363
  console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
365
364
 
366
365
  // Update task status and release lock
367
- if (!disableLock && shouldUseLock(workingDir)) {
366
+ if (queue && shouldUseLock(workingDir)) {
368
367
  await updateCurrentTaskStatus(
369
368
  exitCode === 0 ? 'completed' : 'failed',
370
369
  ).catch(() => null);
@@ -388,24 +387,40 @@ export default async function claudeYes({
388
387
  await idleWaiter.wait(waitms);
389
388
  const et = Date.now();
390
389
  // process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
391
- // await yesLog`sendEn| idleWaiter.wait(${String(waitms)}) took ${String(et - st)}ms`;
392
-
390
+ await yesLog`sendEn| idleWaiter.wait(${String(waitms)}) took ${String(et - st)}ms`;
391
+ nextStdout.unready();
393
392
  shell.write('\r');
393
+ // retry once if not received any output in 1 second after sending Enter
394
+ await Promise.race([
395
+ nextStdout.wait(),
396
+ new Promise<void>((resolve) =>
397
+ setTimeout(() => {
398
+ if (!nextStdout.ready) {
399
+ shell.write('\r');
400
+ }
401
+ resolve();
402
+ }, 1000),
403
+ ),
404
+ ]);
394
405
  }
395
406
 
396
407
  async function sendMessage(message: string) {
397
408
  await stdinReady.wait();
398
409
  // show in-place message: write msg and move cursor back start
399
- // await yesLog`send |${message}`;
400
- shell.write(message);
410
+ yesLog`send |${message}`;
401
411
  nextStdout.unready();
412
+ shell.write(message);
402
413
  idleWaiter.ping(); // just sent a message, wait for echo
414
+ yesLog`waiting next stdout|${message}`;
403
415
  await nextStdout.wait();
404
- await sendEnter();
416
+ yesLog`sending enter`;
417
+ await sendEnter(1000);
418
+ yesLog`sent enter`;
405
419
  }
406
420
 
407
421
  async function exitAgent() {
408
- // continueOnCrash = false;
422
+ robust = false; // disable robust to avoid auto restart
423
+
409
424
  // send exit command to the shell, must sleep a bit to avoid claude treat it as pasted input
410
425
  await sendMessage('/exit');
411
426
 
@@ -437,11 +452,3 @@ export default async function claudeYes({
437
452
  }
438
453
 
439
454
  export { removeControlCharacters };
440
-
441
- function tryCatch<T, R>(fn: () => T, catchFn: (error: unknown) => R): T | R {
442
- try {
443
- return fn();
444
- } catch (error) {
445
- return catchFn(error);
446
- }
447
- }