claude-yes 0.0.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/index.ts ADDED
@@ -0,0 +1,376 @@
1
+ import { fromReadable, fromWritable } from "from-node-stream";
2
+ import { mkdir, writeFile } from "fs/promises";
3
+ import path from "path";
4
+ import DIE from "phpdie";
5
+ import sflow from "sflow";
6
+ import { TerminalTextRender } from "terminal-render";
7
+ import { IdleWaiter } from "./idleWaiter";
8
+ import { ReadyManager } from "./ReadyManager";
9
+ import { removeControlCharacters } from "./removeControlCharacters";
10
+
11
+ export const CLI_CONFIGURES = {
12
+ claude: {
13
+ ready: '', // regex matcher for stdin ready,
14
+ enter: [
15
+ // regexs
16
+ ]
17
+ },
18
+ gemini: {
19
+ ready: '', // regex matcher for stdin ready,
20
+ enter: [
21
+ // regexs
22
+ ]
23
+ },
24
+ codex: {
25
+ ready: '', // regex matcher for stdin ready,
26
+ enter: [
27
+ //regexs
28
+ ],
29
+ // add to codex --search by default when not provided by the user
30
+ ensureArgs: (args: string[]) => {
31
+ if (!args.includes('--search')) return ['--search', ...args];
32
+ return args;
33
+ },
34
+ },
35
+ copilot: {
36
+ // todo
37
+ },
38
+ cursor: {
39
+ // map logical "cursor" cli name to actual binary name
40
+ binary: 'cursor-agent',
41
+ },
42
+ };
43
+ /**
44
+ * Main function to run Claude with automatic yes/no responses
45
+ * @param options Configuration options
46
+ * @param options.continueOnCrash - If true, automatically restart Claude when it crashes:
47
+ * 1. Shows message 'Claude crashed, restarting..'
48
+ * 2. Spawns a new 'claude --continue' process
49
+ * 3. Re-attaches the new process to the shell stdio (pipes new process stdin/stdout)
50
+ * 4. If it crashes with "No conversation found to continue", exits the process
51
+ * @param options.exitOnIdle - Exit when Claude is idle. Boolean or timeout in milliseconds, recommended 5000 - 60000, default is false
52
+ * @param options.claudeArgs - Additional arguments to pass to the Claude CLI
53
+ * @param options.removeControlCharactersFromStdout - Remove ANSI control characters from stdout. Defaults to !process.stdout.isTTY
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * import claudeYes from 'claude-yes';
58
+ * await claudeYes({
59
+ * prompt: 'help me solve all todos in my codebase',
60
+ *
61
+ * // optional
62
+ * cli: 'claude',
63
+ * cliArgs: ['--verbose'], // additional args to pass to claude
64
+ * exitOnIdle: 30000, // exit after 30 seconds of idle
65
+ * continueOnCrash: true, // restart if claude crashes, default is true
66
+ * logFile: 'claude.log', // save logs to file
67
+ * });
68
+ * ```
69
+ */
70
+ export default async function claudeYes({
71
+ cli = "claude",
72
+ cliArgs = [],
73
+ prompt,
74
+ continueOnCrash,
75
+ cwd,
76
+ env,
77
+ exitOnIdle,
78
+ logFile,
79
+ removeControlCharactersFromStdout = false, // = !process.stdout.isTTY,
80
+ verbose = false,
81
+ }: {
82
+ cli?: (string & {}) | keyof typeof CLI_CONFIGURES;
83
+ cliArgs?: string[];
84
+ prompt?: string;
85
+ continueOnCrash?: boolean;
86
+ cwd?: string;
87
+ env?: Record<string, string>;
88
+ exitOnIdle?: number;
89
+ logFile?: string;
90
+ removeControlCharactersFromStdout?: boolean;
91
+ verbose?: boolean;
92
+ } = {}) {
93
+ const continueArgs = {
94
+ codex: "resume --last".split(" "),
95
+ claude: "--continue".split(" "),
96
+ gemini: [], // not possible yet
97
+ };
98
+
99
+ // if (verbose) {
100
+ // console.log('calling claudeYes: ', {
101
+ // cli,
102
+ // continueOnCrash,
103
+ // exitOnIdle,
104
+ // cliArgs,
105
+ // cwd,
106
+ // removeControlCharactersFromStdout,
107
+ // logFile,
108
+ // verbose,
109
+ // });
110
+ // }
111
+ // console.log(
112
+ // `⭐ Starting ${cli}, automatically responding to yes/no prompts...`
113
+ // );
114
+ // console.log(
115
+ // '⚠️ Important Security Warning: Only run this on trusted repositories. This tool automatically responds to prompts and can execute commands without user confirmation. Be aware of potential prompt injection attacks where malicious code or instructions could be embedded in files or user inputs to manipulate the automated responses.'
116
+ // );
117
+
118
+ process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
119
+ let isFatal = false; // match 'No conversation found to continue'
120
+ const stdinReady = new ReadyManager();
121
+
122
+ const shellOutputStream = new TransformStream<string, string>();
123
+ const outputWriter = shellOutputStream.writable.getWriter();
124
+ // const pty = await import('node-pty');
125
+
126
+ // its recommened to use bun-pty in windows
127
+ const pty = await import("node-pty")
128
+ .catch(async () => await import("bun-pty"))
129
+ .catch(async () =>
130
+ DIE("Please install node-pty or bun-pty, run this: bun install bun-pty")
131
+ );
132
+
133
+ const getPtyOptions = () => ({
134
+ name: "xterm-color",
135
+ ...getTerminalDimensions(),
136
+ cwd: cwd ?? process.cwd(),
137
+ env: env ?? (process.env as Record<string, string>),
138
+ });
139
+
140
+ // Apply CLI specific configurations (moved to CLI_CONFIGURES)
141
+ const cliConf = (CLI_CONFIGURES as Record<string, any>)[cli] || {};
142
+ cliArgs = cliConf.ensureArgs?.(cliArgs) ?? cliArgs;
143
+ const cliCommand = cliConf?.binary || cli;
144
+
145
+ let shell = pty.spawn(cliCommand, cliArgs, getPtyOptions());
146
+ const pendingExitCode = Promise.withResolvers<number | null>();
147
+ let pendingExitCodeValue = null;
148
+
149
+ // TODO handle error if claude is not installed, show msg:
150
+ // npm install -g @anthropic-ai/claude-code
151
+
152
+ async function onData(data: string) {
153
+ // append data to the buffer, so we can process it later
154
+ await outputWriter.write(data);
155
+ }
156
+
157
+ shell.onData(onData);
158
+ shell.onExit(function onExit({ exitCode }) {
159
+ stdinReady.unready(); // start buffer stdin
160
+ const agentCrashed = exitCode !== 0;
161
+ const continueArg = (continueArgs as Record<string, string[]>)[cli];
162
+
163
+ if (agentCrashed && continueOnCrash && continueArg) {
164
+ if (!continueArg) {
165
+ return console.warn(
166
+ `continueOnCrash is only supported for ${Object.keys(continueArgs).join(", ")} currently, not ${cli}`
167
+ );
168
+ }
169
+ if (isFatal) {
170
+ console.log(
171
+ `${cli} crashed with "No conversation found to continue", exiting...`
172
+ );
173
+ return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
174
+ }
175
+ console.log(`${cli} crashed, restarting...`);
176
+
177
+ shell = pty.spawn(cli, continueArg, getPtyOptions());
178
+ shell.onData(onData);
179
+ shell.onExit(onExit);
180
+ return;
181
+ }
182
+ return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
183
+ });
184
+
185
+ // when current tty resized, resize the pty
186
+ process.stdout.on("resize", () => {
187
+ const { cols, rows } = getTerminalDimensions(); // minimum 80 columns to avoid layout issues
188
+ shell.resize(cols, rows); // minimum 80 columns to avoid layout issues
189
+ });
190
+
191
+ const terminalRender = new TerminalTextRender();
192
+ const isStillWorkingQ = () =>
193
+ terminalRender
194
+ .render()
195
+ .replace(/\s+/g, " ")
196
+ .match(/esc to interrupt|to run in background/);
197
+
198
+ const idleWaiter = new IdleWaiter();
199
+ if (exitOnIdle)
200
+ idleWaiter.wait(exitOnIdle).then(async () => {
201
+ if (isStillWorkingQ()) {
202
+ console.log(
203
+ "[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet"
204
+ );
205
+ return;
206
+ }
207
+
208
+ console.log("[${cli}-yes] ${cli} is idle, exiting...");
209
+ await exitAgent();
210
+ });
211
+
212
+ // Message streaming
213
+ sflow(fromReadable<Buffer>(process.stdin))
214
+ .map((buffer) => buffer.toString())
215
+ .map((e) => e.replaceAll("\x1a", "")) // remove ctrl+z from user's input
216
+ // .forEach(e => appendFile('.cache/io.log', "input |" + JSON.stringify(e) + '\n')) // for debugging
217
+ // pipe
218
+ .by({
219
+ writable: new WritableStream<string>({
220
+ write: async (data) => {
221
+ // await stdinReady.wait();
222
+ await idleWaiter.wait(200); // wait for idle for 200ms to avoid messing up claude's input
223
+ shell.write(data);
224
+ },
225
+ }),
226
+ readable: shellOutputStream.readable,
227
+ })
228
+ .forEach(() => idleWaiter.ping())
229
+ .forEach((text) => terminalRender.write(text))
230
+ .forEach((txt) => {
231
+ // xterm replies CSI row; column R if asked cursor position
232
+ // https://en.wikipedia.org/wiki/ANSI_escape_code#:~:text=citation%20needed%5D-,xterm%20replies,-CSI%20row%C2%A0%3B
233
+ if (process.stdin.isTTY) return; // only handle it when stdin is not tty
234
+ if (txt.includes("\u001b[6n")) return; // only asked
235
+ const rendered = terminalRender.render();
236
+ // when asking position, respond with row; col
237
+ const row = rendered.split("\n").length + 1;
238
+ const col = (rendered.split("\n").slice(-1)[0]?.length || 0) + 1;
239
+ // shell.write(`\u001b[${row};${col}R`);
240
+ })
241
+
242
+ // auto-response
243
+ .forkTo((e) =>
244
+ e
245
+ .map((e) => removeControlCharacters(e))
246
+ .map((e) => e.replaceAll("\r", "")) // remove carriage return
247
+ .lines({ EOL: "NONE" })
248
+ .forEach(async (e) => {
249
+ if (cli !== "claude") return;
250
+
251
+ if (e.match(/^> /)) return stdinReady.ready();
252
+ if (e.match(/❯ 1. Yes/)) return await sendEnter();
253
+ if (e.match(/❯ 1. Dark mode✔|Press Enter to continue…/))
254
+ return await sendEnter();
255
+ if (e.match(/No conversation found to continue/))
256
+ return (isFatal = true); // set flag to true if error message is found;
257
+ if (e.match(/⎿ {2}Claude usage limit reached./))
258
+ return (isFatal = true); // set flag to true if error message is found;
259
+ // reached limit, exiting...
260
+ })
261
+ .forEach(async (e, i) => {
262
+ if (cli !== "gemini") return;
263
+ if (e.match(/ > {3}Type your message/) && i > 80) {
264
+ // wait until 80 lines to avoid the initial prompt
265
+ return stdinReady.ready();
266
+ }
267
+ if (e.match(/│ ● 1. Yes, allow once/)) return await sendEnter();
268
+ })
269
+ .forEach(async (e) => {
270
+ if (cli === "codex") {
271
+ if (e.match(/ > 1. Approve/)) return await sendEnter();
272
+ if (e.match(/Error: The cursor position could not be read within/))
273
+ return (isFatal = true);
274
+ if (e.match(/> 1. Yes, allow Codex to work in this folder/))
275
+ return await sendEnter();
276
+ if (e.match(/⏎ send/)) return stdinReady.ready();
277
+ return;
278
+ }
279
+ if (cli === "cursor") {
280
+ if (e.match(/\/ commands/)) return stdinReady.ready();
281
+ if (e.match(/→ Run \(once\) \(y\) \(enter\)/))
282
+ return await sendEnter();
283
+ if (e.match(/▶ \[a\] Trust this workspace/))
284
+ return await sendEnter();
285
+ }
286
+ })
287
+ // .forEach(e => appendFile('.cache/io.log', "output|" + JSON.stringify(e) + '\n')) // for debugging
288
+ .run()
289
+ )
290
+ .map((e) =>
291
+ removeControlCharactersFromStdout ? removeControlCharacters(e) : e
292
+ )
293
+ .to(fromWritable(process.stdout))
294
+ .then(() => null); // run it immediately without await
295
+
296
+ // wait for cli ready and send prompt if provided
297
+ if (prompt)
298
+ (async () => {
299
+ // console.log(`[${cli}-yes] Ready to send prompt to ${cli}: ${prompt}`);
300
+ // idleWaiter.ping();
301
+ // console.log(
302
+ // 'await idleWaiter.wait(1000); // wait a bit for claude to start'
303
+ // );
304
+ // await idleWaiter.wait(1000); // wait a bit for claude to start
305
+ // console.log('await stdinReady.wait();');
306
+ // await stdinReady.wait();
307
+ // console.log(`[${cli}-yes] Waiting for ${cli} to be ready...`);
308
+ // console.log('await idleWaiter.wait(200);');
309
+ // await idleWaiter.wait(200);
310
+ // console.log(`[${cli}-yes] Sending prompt to ${cli}: ${prompt}`);
311
+ await sendMessage(prompt);
312
+ })();
313
+
314
+ const exitCode = await pendingExitCode.promise; // wait for the shell to exit
315
+ console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
316
+
317
+ if (logFile) {
318
+ verbose && console.log(`[${cli}-yes] Writing rendered logs to ${logFile}`);
319
+ const logFilePath = path.resolve(logFile);
320
+ await mkdir(path.dirname(logFilePath), { recursive: true }).catch(
321
+ () => null
322
+ );
323
+ await writeFile(logFilePath, terminalRender.render());
324
+ }
325
+
326
+ return { exitCode, logs: terminalRender.render() };
327
+
328
+ async function sendEnter(waitms = 1000) {
329
+ // wait for idle for a bit to let agent cli finish rendering
330
+ const st = Date.now();
331
+
332
+ await idleWaiter.wait(waitms);
333
+ const et = Date.now();
334
+ process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
335
+
336
+ shell.write("\r");
337
+ }
338
+
339
+ async function sendMessage(message: string) {
340
+ await stdinReady.wait();
341
+ // show in-place message: write msg and move cursor back start
342
+ shell.write(message);
343
+ idleWaiter.ping(); // just sent a message, wait for echo
344
+ await sendEnter();
345
+ }
346
+
347
+ async function exitAgent() {
348
+ continueOnCrash = false;
349
+ // send exit command to the shell, must sleep a bit to avoid claude treat it as pasted input
350
+ await sendMessage("/exit");
351
+
352
+ // wait for shell to exit or kill it with a timeout
353
+ let exited = false;
354
+ await Promise.race([
355
+ pendingExitCode.promise.then(() => (exited = true)), // resolve when shell exits
356
+
357
+ // if shell doesn't exit in 5 seconds, kill it
358
+ new Promise<void>((resolve) =>
359
+ setTimeout(() => {
360
+ if (exited) return; // if shell already exited, do nothing
361
+ shell.kill(); // kill the shell process if it doesn't exit in time
362
+ resolve();
363
+ }, 5000)
364
+ ), // 5 seconds timeout
365
+ ]);
366
+ }
367
+
368
+ function getTerminalDimensions() {
369
+ return {
370
+ cols: Math.max(process.stdout.columns, 80),
371
+ rows: process.stdout.rows,
372
+ };
373
+ }
374
+ }
375
+
376
+ export { removeControlCharacters };
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "claude-yes",
3
+ "version": "0.0.0",
4
+ "description": "A wrapper tool that automates interactions with the Claude CLI by automatically handling common prompts and responses.",
5
+ "keywords": [
6
+ "claude",
7
+ "ai",
8
+ "automation",
9
+ "cli",
10
+ "wrapper",
11
+ "assistant",
12
+ "anthropic",
13
+ "auto-response"
14
+ ],
15
+ "homepage": "https://github.com/snomiao/claude-yes#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/snomiao/claude-yes/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/snomiao/claude-yes.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "snomiao <snomiao@gmail.com>",
25
+ "type": "module",
26
+ "exports": {
27
+ "import": "./dist/index.js",
28
+ "types": "./index.ts"
29
+ },
30
+ "main": "dist/index.js",
31
+ "module": "index.ts",
32
+ "types": "./index.ts",
33
+ "bin": {
34
+ "claude-yes": "dist/claude-yes.js",
35
+ "codex-yes": "dist/codex-yes.js",
36
+ "gemini-yes": "dist/gemini-yes.js",
37
+ "cursor-yes": "dist/cursor-yes.js",
38
+ "copilot-yes": "dist/copilot-yes.js"
39
+ },
40
+ "directories": {
41
+ "doc": "docs"
42
+ },
43
+ "files": [
44
+ "*.ts",
45
+ "dist"
46
+ ],
47
+ "scripts": {
48
+ "build": "bun build index.ts cli.ts --packages=external --outdir=dist --target=node --sourcemap",
49
+ "postbuild": "bun ./postbuild.ts",
50
+ "dev": "tsx index.ts",
51
+ "fmt": "bunx @biomejs/biome check --fix",
52
+ "prepack": "bun run build",
53
+ "prepare": "bunx husky",
54
+ "test": "vitest"
55
+ },
56
+ "lint-staged": {
57
+ "*.{ts,js,json,md}": [
58
+ "bunx @biomejs/biome check --fix"
59
+ ]
60
+ },
61
+ "devDependencies": {
62
+ "@biomejs/biome": "^2.2.5",
63
+ "@types/bun": "^1.2.18",
64
+ "@types/jest": "^30.0.0",
65
+ "@types/node": "^24.0.10",
66
+ "@types/yargs": "^17.0.33",
67
+ "enhanced-ms": "^4.1.0",
68
+ "execa": "^9.6.0",
69
+ "from-node-stream": "^0.0.11",
70
+ "husky": "^9.1.7",
71
+ "lint-staged": "^16.1.4",
72
+ "rambda": "^10.3.2",
73
+ "semantic-release": "^24.2.6",
74
+ "sflow": "^1.20.2",
75
+ "strip-ansi-control-characters": "^2.0.0",
76
+ "terminal-render": "^1.1.0",
77
+ "tsx": "^4.20.3",
78
+ "vitest": "^3.2.4",
79
+ "yargs": "^18.0.0"
80
+ },
81
+ "peerDependencies": {
82
+ "typescript": "^5.8.3",
83
+ "node-pty": "^1.0.0"
84
+ },
85
+ "dependencies": {
86
+ "bun-pty": "^0.3.2",
87
+ "p-map": "^7.0.3",
88
+ "phpdie": "^1.7.0"
89
+ }
90
+ }
package/postbuild.ts ADDED
@@ -0,0 +1,6 @@
1
+ #! /usr/bin/env bun
2
+ import { copyFile } from 'fs/promises';
3
+ import * as pkg from './package.json';
4
+
5
+ const src = 'dist/cli.js';
6
+ await Promise.all(Object.values(pkg.bin).map((dst) => copyFile(src, dst)));
@@ -0,0 +1,7 @@
1
+ export function removeControlCharacters(str: string): string {
2
+ // Matches control characters in the C0 and C1 ranges, including Delete (U+007F)
3
+ return str.replace(
4
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
5
+ '',
6
+ );
7
+ }
package/sleep.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function sleep(ms: number) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
package/utils.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function sleepms(ms: number) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }