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