claude-yes 1.17.1 → 1.19.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/README.md +119 -25
- package/ReadyManager.ts +2 -4
- package/cli-idle.spec.ts +1 -1
- package/cli.test.ts +7 -6
- package/cli.ts +52 -14
- package/dist/claude-yes.js +332 -0
- package/dist/cli.js +264 -11380
- package/dist/cli.js.map +5 -165
- package/dist/codex-yes.js +332 -0
- package/dist/copilot-yes.js +332 -0
- package/dist/cursor-yes.js +332 -0
- package/dist/gemini-yes.js +332 -0
- package/dist/index.js +195 -5158
- 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
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
// cli.ts
|
|
22
|
+
import enhancedMs from "enhanced-ms";
|
|
23
|
+
import yargs from "yargs";
|
|
24
|
+
import { hideBin } from "yargs/helpers";
|
|
25
|
+
|
|
26
|
+
// dist/index.js
|
|
27
|
+
import { fromReadable, fromWritable } from "from-node-stream";
|
|
28
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
29
|
+
import path from "path";
|
|
30
|
+
import DIE from "phpdie";
|
|
31
|
+
import sflow from "sflow";
|
|
32
|
+
import { TerminalTextRender } from "terminal-render";
|
|
33
|
+
class IdleWaiter {
|
|
34
|
+
lastActivityTime = Date.now();
|
|
35
|
+
checkInterval = 100;
|
|
36
|
+
constructor() {
|
|
37
|
+
this.ping();
|
|
38
|
+
}
|
|
39
|
+
ping() {
|
|
40
|
+
this.lastActivityTime = Date.now();
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
async wait(ms) {
|
|
44
|
+
while (this.lastActivityTime >= Date.now() - ms)
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, this.checkInterval));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class ReadyManager {
|
|
50
|
+
isReady = false;
|
|
51
|
+
readyQueue = [];
|
|
52
|
+
wait() {
|
|
53
|
+
if (this.isReady)
|
|
54
|
+
return;
|
|
55
|
+
return new Promise((resolve) => this.readyQueue.push(resolve));
|
|
56
|
+
}
|
|
57
|
+
unready() {
|
|
58
|
+
this.isReady = false;
|
|
59
|
+
}
|
|
60
|
+
ready() {
|
|
61
|
+
this.isReady = true;
|
|
62
|
+
if (!this.readyQueue.length)
|
|
63
|
+
return;
|
|
64
|
+
this.readyQueue.splice(0).map((resolve) => resolve());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function removeControlCharacters(str) {
|
|
68
|
+
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
69
|
+
}
|
|
70
|
+
var CLI_CONFIGURES = {
|
|
71
|
+
claude: {
|
|
72
|
+
ready: /^> /,
|
|
73
|
+
enter: [/❯ 1. Yes/, /❯ 1. Dark mode✔/, /Press Enter to continue…/],
|
|
74
|
+
fatal: [
|
|
75
|
+
/No conversation found to continue/,
|
|
76
|
+
/⎿ {2}Claude usage limit reached\./
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
gemini: {
|
|
80
|
+
ready: /Type your message/,
|
|
81
|
+
enter: [/│ ● 1. Yes, allow once/],
|
|
82
|
+
fatal: []
|
|
83
|
+
},
|
|
84
|
+
codex: {
|
|
85
|
+
ready: /⏎ send/,
|
|
86
|
+
enter: [/ > 1. Approve/, /> 1. Yes, allow Codex to work in this folder/],
|
|
87
|
+
fatal: [/Error: The cursor position could not be read within/],
|
|
88
|
+
ensureArgs: (args) => {
|
|
89
|
+
if (!args.includes("--search"))
|
|
90
|
+
return ["--search", ...args];
|
|
91
|
+
return args;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
copilot: {
|
|
95
|
+
ready: /^ > /,
|
|
96
|
+
enter: [/ │ ❯ 1. Yes, proceed/, /❯ 1. Yes/],
|
|
97
|
+
fatal: []
|
|
98
|
+
},
|
|
99
|
+
cursor: {
|
|
100
|
+
binary: "cursor-agent",
|
|
101
|
+
ready: /\/ commands/,
|
|
102
|
+
enter: [/→ Run \(once\) \(y\) \(enter\)/, /▶ \[a\] Trust this workspace/],
|
|
103
|
+
fatal: []
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
async function claudeYes({
|
|
107
|
+
cli = "claude",
|
|
108
|
+
cliArgs = [],
|
|
109
|
+
prompt,
|
|
110
|
+
continueOnCrash,
|
|
111
|
+
cwd,
|
|
112
|
+
env,
|
|
113
|
+
exitOnIdle,
|
|
114
|
+
logFile,
|
|
115
|
+
removeControlCharactersFromStdout = false,
|
|
116
|
+
verbose = false
|
|
117
|
+
} = {}) {
|
|
118
|
+
const continueArgs = {
|
|
119
|
+
codex: "resume --last".split(" "),
|
|
120
|
+
claude: "--continue".split(" "),
|
|
121
|
+
gemini: []
|
|
122
|
+
};
|
|
123
|
+
process.stdin.setRawMode?.(true);
|
|
124
|
+
let isFatal = false;
|
|
125
|
+
const stdinReady = new ReadyManager;
|
|
126
|
+
const shellOutputStream = new TransformStream;
|
|
127
|
+
const outputWriter = shellOutputStream.writable.getWriter();
|
|
128
|
+
const pty = await import("node-pty").catch(async () => await import("bun-pty")).catch(async () => DIE("Please install node-pty or bun-pty, run this: bun install bun-pty"));
|
|
129
|
+
const getPtyOptions = () => ({
|
|
130
|
+
name: "xterm-color",
|
|
131
|
+
...getTerminalDimensions(),
|
|
132
|
+
cwd: cwd ?? process.cwd(),
|
|
133
|
+
env: env ?? process.env
|
|
134
|
+
});
|
|
135
|
+
const cliConf = CLI_CONFIGURES[cli] || {};
|
|
136
|
+
cliArgs = cliConf.ensureArgs?.(cliArgs) ?? cliArgs;
|
|
137
|
+
const cliCommand = cliConf?.binary || cli;
|
|
138
|
+
let shell = pty.spawn(cliCommand, cliArgs, getPtyOptions());
|
|
139
|
+
const pendingExitCode = Promise.withResolvers();
|
|
140
|
+
let pendingExitCodeValue = null;
|
|
141
|
+
async function onData(data) {
|
|
142
|
+
await outputWriter.write(data);
|
|
143
|
+
}
|
|
144
|
+
shell.onData(onData);
|
|
145
|
+
shell.onExit(function onExit({ exitCode: exitCode2 }) {
|
|
146
|
+
stdinReady.unready();
|
|
147
|
+
const agentCrashed = exitCode2 !== 0;
|
|
148
|
+
const continueArg = continueArgs[cli];
|
|
149
|
+
if (agentCrashed && continueOnCrash && continueArg) {
|
|
150
|
+
if (!continueArg) {
|
|
151
|
+
return console.warn(`continueOnCrash is only supported for ${Object.keys(continueArgs).join(", ")} currently, not ${cli}`);
|
|
152
|
+
}
|
|
153
|
+
if (isFatal) {
|
|
154
|
+
console.log(`${cli} crashed with "No conversation found to continue", exiting...`);
|
|
155
|
+
return pendingExitCode.resolve(pendingExitCodeValue = exitCode2);
|
|
156
|
+
}
|
|
157
|
+
console.log(`${cli} crashed, restarting...`);
|
|
158
|
+
shell = pty.spawn(cli, continueArg, getPtyOptions());
|
|
159
|
+
shell.onData(onData);
|
|
160
|
+
shell.onExit(onExit);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
return pendingExitCode.resolve(pendingExitCodeValue = exitCode2);
|
|
164
|
+
});
|
|
165
|
+
process.stdout.on("resize", () => {
|
|
166
|
+
const { cols, rows } = getTerminalDimensions();
|
|
167
|
+
shell.resize(cols, rows);
|
|
168
|
+
});
|
|
169
|
+
const terminalRender = new TerminalTextRender;
|
|
170
|
+
const isStillWorkingQ = () => terminalRender.render().replace(/\s+/g, " ").match(/esc to interrupt|to run in background/);
|
|
171
|
+
const idleWaiter = new IdleWaiter;
|
|
172
|
+
if (exitOnIdle)
|
|
173
|
+
idleWaiter.wait(exitOnIdle).then(async () => {
|
|
174
|
+
if (isStillWorkingQ()) {
|
|
175
|
+
console.log("[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log("[${cli}-yes] ${cli} is idle, exiting...");
|
|
179
|
+
await exitAgent();
|
|
180
|
+
});
|
|
181
|
+
sflow(fromReadable(process.stdin)).map((buffer) => buffer.toString()).by({
|
|
182
|
+
writable: new WritableStream({
|
|
183
|
+
write: async (data) => {
|
|
184
|
+
await stdinReady.wait();
|
|
185
|
+
shell.write(data);
|
|
186
|
+
}
|
|
187
|
+
}),
|
|
188
|
+
readable: shellOutputStream.readable
|
|
189
|
+
}).forEach(() => idleWaiter.ping()).forEach((text) => {
|
|
190
|
+
terminalRender.write(text);
|
|
191
|
+
if (process.stdin.isTTY)
|
|
192
|
+
return;
|
|
193
|
+
if (text.includes("\x1B[6n"))
|
|
194
|
+
return;
|
|
195
|
+
const rendered = terminalRender.render();
|
|
196
|
+
const row = rendered.split(`
|
|
197
|
+
`).length + 1;
|
|
198
|
+
const col = (rendered.split(`
|
|
199
|
+
`).slice(-1)[0]?.length || 0) + 1;
|
|
200
|
+
shell.write(`\x1B[${row};${col}R`);
|
|
201
|
+
}).forkTo((e) => e.map((e2) => removeControlCharacters(e2)).map((e2) => e2.replaceAll("\r", "")).lines({ EOL: "NONE" }).forEach(async (e2, i) => {
|
|
202
|
+
const conf = CLI_CONFIGURES[cli] || null;
|
|
203
|
+
if (!conf)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
if (conf.ready) {
|
|
207
|
+
if (cli === "gemini" && conf.ready instanceof RegExp) {
|
|
208
|
+
if (e2.match(conf.ready) && i > 80)
|
|
209
|
+
return stdinReady.ready();
|
|
210
|
+
} else if (e2.match(conf.ready)) {
|
|
211
|
+
return stdinReady.ready();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (conf.enter && Array.isArray(conf.enter)) {
|
|
215
|
+
for (const rx of conf.enter) {
|
|
216
|
+
if (e2.match(rx))
|
|
217
|
+
return await sendEnter();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (conf.fatal && Array.isArray(conf.fatal)) {
|
|
221
|
+
for (const rx of conf.fatal) {
|
|
222
|
+
if (e2.match(rx))
|
|
223
|
+
return isFatal = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}).run()).map((e) => removeControlCharactersFromStdout ? removeControlCharacters(e) : e).to(fromWritable(process.stdout)).then(() => null);
|
|
230
|
+
if (prompt)
|
|
231
|
+
(async () => {
|
|
232
|
+
await sendMessage(prompt);
|
|
233
|
+
})();
|
|
234
|
+
const exitCode = await pendingExitCode.promise;
|
|
235
|
+
console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
236
|
+
if (logFile) {
|
|
237
|
+
verbose && console.log(`[${cli}-yes] Writing rendered logs to ${logFile}`);
|
|
238
|
+
const logFilePath = path.resolve(logFile);
|
|
239
|
+
await mkdir(path.dirname(logFilePath), { recursive: true }).catch(() => null);
|
|
240
|
+
await writeFile(logFilePath, terminalRender.render());
|
|
241
|
+
}
|
|
242
|
+
return { exitCode, logs: terminalRender.render() };
|
|
243
|
+
async function sendEnter(waitms = 1000) {
|
|
244
|
+
const st = Date.now();
|
|
245
|
+
await idleWaiter.wait(waitms);
|
|
246
|
+
const et = Date.now();
|
|
247
|
+
process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
|
|
248
|
+
shell.write("\r");
|
|
249
|
+
}
|
|
250
|
+
async function sendMessage(message) {
|
|
251
|
+
await stdinReady.wait();
|
|
252
|
+
shell.write(message);
|
|
253
|
+
idleWaiter.ping();
|
|
254
|
+
await sendEnter();
|
|
255
|
+
}
|
|
256
|
+
async function exitAgent() {
|
|
257
|
+
continueOnCrash = false;
|
|
258
|
+
await sendMessage("/exit");
|
|
259
|
+
let exited = false;
|
|
260
|
+
await Promise.race([
|
|
261
|
+
pendingExitCode.promise.then(() => exited = true),
|
|
262
|
+
new Promise((resolve) => setTimeout(() => {
|
|
263
|
+
if (exited)
|
|
264
|
+
return;
|
|
265
|
+
shell.kill();
|
|
266
|
+
resolve();
|
|
267
|
+
}, 5000))
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
function getTerminalDimensions() {
|
|
271
|
+
return {
|
|
272
|
+
cols: Math.max(process.stdout.columns, 80),
|
|
273
|
+
rows: process.stdout.rows
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// cli.ts
|
|
279
|
+
var argv = yargs(hideBin(process.argv)).usage("Usage: $0 [options] [claude args] [--] [prompts...]").example('$0 --exit-on-idle=30s --continue-on-crash "help me solve all todos in my codebase"', "Run Claude with a 30 seconds idle timeout and continue on crash").option("continue-on-crash", {
|
|
280
|
+
type: "boolean",
|
|
281
|
+
default: true,
|
|
282
|
+
description: "spawn Claude with --continue if it crashes, only works for claude"
|
|
283
|
+
}).option("log-file", {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Log file to write to"
|
|
286
|
+
}).option("cli", {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: 'Claude CLI command, e.g. "claude,gemini,codex,cursor,copilot", default is "claude"'
|
|
289
|
+
}).option("prompt", {
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Prompt to send to Claude",
|
|
292
|
+
alias: "p"
|
|
293
|
+
}).option("verbose", {
|
|
294
|
+
type: "boolean",
|
|
295
|
+
description: "Enable verbose logging",
|
|
296
|
+
default: false
|
|
297
|
+
}).option("exit-on-idle", {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: 'Exit after a period of inactivity, e.g., "5s" or "1m"'
|
|
300
|
+
}).parserConfiguration({
|
|
301
|
+
"unknown-options-as-args": true,
|
|
302
|
+
"halt-at-non-option": true
|
|
303
|
+
}).parseSync();
|
|
304
|
+
if (!argv.cli) {
|
|
305
|
+
const cliName = process.argv[1]?.split("/").pop()?.split("-")[0];
|
|
306
|
+
argv.cli = cliName || "claude";
|
|
307
|
+
}
|
|
308
|
+
var rawArgs = process.argv.slice(2);
|
|
309
|
+
var dashIndex = rawArgs.indexOf("--");
|
|
310
|
+
var promptFromDash = undefined;
|
|
311
|
+
var cliArgsForSpawn = [];
|
|
312
|
+
if (dashIndex !== -1) {
|
|
313
|
+
const after = rawArgs.slice(dashIndex + 1);
|
|
314
|
+
promptFromDash = after.join(" ");
|
|
315
|
+
cliArgsForSpawn = rawArgs.slice(0, dashIndex).map(String);
|
|
316
|
+
} else {
|
|
317
|
+
cliArgsForSpawn = argv._.map((e) => String(e));
|
|
318
|
+
}
|
|
319
|
+
console.clear();
|
|
320
|
+
var { exitCode, logs } = await claudeYes({
|
|
321
|
+
cli: argv.cli,
|
|
322
|
+
prompt: argv.prompt || promptFromDash,
|
|
323
|
+
exitOnIdle: argv.exitOnIdle ? enhancedMs(argv.exitOnIdle) : undefined,
|
|
324
|
+
cliArgs: cliArgsForSpawn,
|
|
325
|
+
continueOnCrash: argv.continueOnCrash,
|
|
326
|
+
logFile: argv.logFile,
|
|
327
|
+
verbose: argv.verbose
|
|
328
|
+
});
|
|
329
|
+
process.exit(exitCode ?? 1);
|
|
330
|
+
|
|
331
|
+
//# debugId=0E9D5C1D34AAD47764756E2164756E21
|
|
332
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
// cli.ts
|
|
22
|
+
import enhancedMs from "enhanced-ms";
|
|
23
|
+
import yargs from "yargs";
|
|
24
|
+
import { hideBin } from "yargs/helpers";
|
|
25
|
+
|
|
26
|
+
// dist/index.js
|
|
27
|
+
import { fromReadable, fromWritable } from "from-node-stream";
|
|
28
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
29
|
+
import path from "path";
|
|
30
|
+
import DIE from "phpdie";
|
|
31
|
+
import sflow from "sflow";
|
|
32
|
+
import { TerminalTextRender } from "terminal-render";
|
|
33
|
+
class IdleWaiter {
|
|
34
|
+
lastActivityTime = Date.now();
|
|
35
|
+
checkInterval = 100;
|
|
36
|
+
constructor() {
|
|
37
|
+
this.ping();
|
|
38
|
+
}
|
|
39
|
+
ping() {
|
|
40
|
+
this.lastActivityTime = Date.now();
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
async wait(ms) {
|
|
44
|
+
while (this.lastActivityTime >= Date.now() - ms)
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, this.checkInterval));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class ReadyManager {
|
|
50
|
+
isReady = false;
|
|
51
|
+
readyQueue = [];
|
|
52
|
+
wait() {
|
|
53
|
+
if (this.isReady)
|
|
54
|
+
return;
|
|
55
|
+
return new Promise((resolve) => this.readyQueue.push(resolve));
|
|
56
|
+
}
|
|
57
|
+
unready() {
|
|
58
|
+
this.isReady = false;
|
|
59
|
+
}
|
|
60
|
+
ready() {
|
|
61
|
+
this.isReady = true;
|
|
62
|
+
if (!this.readyQueue.length)
|
|
63
|
+
return;
|
|
64
|
+
this.readyQueue.splice(0).map((resolve) => resolve());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function removeControlCharacters(str) {
|
|
68
|
+
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
69
|
+
}
|
|
70
|
+
var CLI_CONFIGURES = {
|
|
71
|
+
claude: {
|
|
72
|
+
ready: /^> /,
|
|
73
|
+
enter: [/❯ 1. Yes/, /❯ 1. Dark mode✔/, /Press Enter to continue…/],
|
|
74
|
+
fatal: [
|
|
75
|
+
/No conversation found to continue/,
|
|
76
|
+
/⎿ {2}Claude usage limit reached\./
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
gemini: {
|
|
80
|
+
ready: /Type your message/,
|
|
81
|
+
enter: [/│ ● 1. Yes, allow once/],
|
|
82
|
+
fatal: []
|
|
83
|
+
},
|
|
84
|
+
codex: {
|
|
85
|
+
ready: /⏎ send/,
|
|
86
|
+
enter: [/ > 1. Approve/, /> 1. Yes, allow Codex to work in this folder/],
|
|
87
|
+
fatal: [/Error: The cursor position could not be read within/],
|
|
88
|
+
ensureArgs: (args) => {
|
|
89
|
+
if (!args.includes("--search"))
|
|
90
|
+
return ["--search", ...args];
|
|
91
|
+
return args;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
copilot: {
|
|
95
|
+
ready: /^ > /,
|
|
96
|
+
enter: [/ │ ❯ 1. Yes, proceed/, /❯ 1. Yes/],
|
|
97
|
+
fatal: []
|
|
98
|
+
},
|
|
99
|
+
cursor: {
|
|
100
|
+
binary: "cursor-agent",
|
|
101
|
+
ready: /\/ commands/,
|
|
102
|
+
enter: [/→ Run \(once\) \(y\) \(enter\)/, /▶ \[a\] Trust this workspace/],
|
|
103
|
+
fatal: []
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
async function claudeYes({
|
|
107
|
+
cli = "claude",
|
|
108
|
+
cliArgs = [],
|
|
109
|
+
prompt,
|
|
110
|
+
continueOnCrash,
|
|
111
|
+
cwd,
|
|
112
|
+
env,
|
|
113
|
+
exitOnIdle,
|
|
114
|
+
logFile,
|
|
115
|
+
removeControlCharactersFromStdout = false,
|
|
116
|
+
verbose = false
|
|
117
|
+
} = {}) {
|
|
118
|
+
const continueArgs = {
|
|
119
|
+
codex: "resume --last".split(" "),
|
|
120
|
+
claude: "--continue".split(" "),
|
|
121
|
+
gemini: []
|
|
122
|
+
};
|
|
123
|
+
process.stdin.setRawMode?.(true);
|
|
124
|
+
let isFatal = false;
|
|
125
|
+
const stdinReady = new ReadyManager;
|
|
126
|
+
const shellOutputStream = new TransformStream;
|
|
127
|
+
const outputWriter = shellOutputStream.writable.getWriter();
|
|
128
|
+
const pty = await import("node-pty").catch(async () => await import("bun-pty")).catch(async () => DIE("Please install node-pty or bun-pty, run this: bun install bun-pty"));
|
|
129
|
+
const getPtyOptions = () => ({
|
|
130
|
+
name: "xterm-color",
|
|
131
|
+
...getTerminalDimensions(),
|
|
132
|
+
cwd: cwd ?? process.cwd(),
|
|
133
|
+
env: env ?? process.env
|
|
134
|
+
});
|
|
135
|
+
const cliConf = CLI_CONFIGURES[cli] || {};
|
|
136
|
+
cliArgs = cliConf.ensureArgs?.(cliArgs) ?? cliArgs;
|
|
137
|
+
const cliCommand = cliConf?.binary || cli;
|
|
138
|
+
let shell = pty.spawn(cliCommand, cliArgs, getPtyOptions());
|
|
139
|
+
const pendingExitCode = Promise.withResolvers();
|
|
140
|
+
let pendingExitCodeValue = null;
|
|
141
|
+
async function onData(data) {
|
|
142
|
+
await outputWriter.write(data);
|
|
143
|
+
}
|
|
144
|
+
shell.onData(onData);
|
|
145
|
+
shell.onExit(function onExit({ exitCode: exitCode2 }) {
|
|
146
|
+
stdinReady.unready();
|
|
147
|
+
const agentCrashed = exitCode2 !== 0;
|
|
148
|
+
const continueArg = continueArgs[cli];
|
|
149
|
+
if (agentCrashed && continueOnCrash && continueArg) {
|
|
150
|
+
if (!continueArg) {
|
|
151
|
+
return console.warn(`continueOnCrash is only supported for ${Object.keys(continueArgs).join(", ")} currently, not ${cli}`);
|
|
152
|
+
}
|
|
153
|
+
if (isFatal) {
|
|
154
|
+
console.log(`${cli} crashed with "No conversation found to continue", exiting...`);
|
|
155
|
+
return pendingExitCode.resolve(pendingExitCodeValue = exitCode2);
|
|
156
|
+
}
|
|
157
|
+
console.log(`${cli} crashed, restarting...`);
|
|
158
|
+
shell = pty.spawn(cli, continueArg, getPtyOptions());
|
|
159
|
+
shell.onData(onData);
|
|
160
|
+
shell.onExit(onExit);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
return pendingExitCode.resolve(pendingExitCodeValue = exitCode2);
|
|
164
|
+
});
|
|
165
|
+
process.stdout.on("resize", () => {
|
|
166
|
+
const { cols, rows } = getTerminalDimensions();
|
|
167
|
+
shell.resize(cols, rows);
|
|
168
|
+
});
|
|
169
|
+
const terminalRender = new TerminalTextRender;
|
|
170
|
+
const isStillWorkingQ = () => terminalRender.render().replace(/\s+/g, " ").match(/esc to interrupt|to run in background/);
|
|
171
|
+
const idleWaiter = new IdleWaiter;
|
|
172
|
+
if (exitOnIdle)
|
|
173
|
+
idleWaiter.wait(exitOnIdle).then(async () => {
|
|
174
|
+
if (isStillWorkingQ()) {
|
|
175
|
+
console.log("[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log("[${cli}-yes] ${cli} is idle, exiting...");
|
|
179
|
+
await exitAgent();
|
|
180
|
+
});
|
|
181
|
+
sflow(fromReadable(process.stdin)).map((buffer) => buffer.toString()).by({
|
|
182
|
+
writable: new WritableStream({
|
|
183
|
+
write: async (data) => {
|
|
184
|
+
await stdinReady.wait();
|
|
185
|
+
shell.write(data);
|
|
186
|
+
}
|
|
187
|
+
}),
|
|
188
|
+
readable: shellOutputStream.readable
|
|
189
|
+
}).forEach(() => idleWaiter.ping()).forEach((text) => {
|
|
190
|
+
terminalRender.write(text);
|
|
191
|
+
if (process.stdin.isTTY)
|
|
192
|
+
return;
|
|
193
|
+
if (text.includes("\x1B[6n"))
|
|
194
|
+
return;
|
|
195
|
+
const rendered = terminalRender.render();
|
|
196
|
+
const row = rendered.split(`
|
|
197
|
+
`).length + 1;
|
|
198
|
+
const col = (rendered.split(`
|
|
199
|
+
`).slice(-1)[0]?.length || 0) + 1;
|
|
200
|
+
shell.write(`\x1B[${row};${col}R`);
|
|
201
|
+
}).forkTo((e) => e.map((e2) => removeControlCharacters(e2)).map((e2) => e2.replaceAll("\r", "")).lines({ EOL: "NONE" }).forEach(async (e2, i) => {
|
|
202
|
+
const conf = CLI_CONFIGURES[cli] || null;
|
|
203
|
+
if (!conf)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
if (conf.ready) {
|
|
207
|
+
if (cli === "gemini" && conf.ready instanceof RegExp) {
|
|
208
|
+
if (e2.match(conf.ready) && i > 80)
|
|
209
|
+
return stdinReady.ready();
|
|
210
|
+
} else if (e2.match(conf.ready)) {
|
|
211
|
+
return stdinReady.ready();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (conf.enter && Array.isArray(conf.enter)) {
|
|
215
|
+
for (const rx of conf.enter) {
|
|
216
|
+
if (e2.match(rx))
|
|
217
|
+
return await sendEnter();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (conf.fatal && Array.isArray(conf.fatal)) {
|
|
221
|
+
for (const rx of conf.fatal) {
|
|
222
|
+
if (e2.match(rx))
|
|
223
|
+
return isFatal = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}).run()).map((e) => removeControlCharactersFromStdout ? removeControlCharacters(e) : e).to(fromWritable(process.stdout)).then(() => null);
|
|
230
|
+
if (prompt)
|
|
231
|
+
(async () => {
|
|
232
|
+
await sendMessage(prompt);
|
|
233
|
+
})();
|
|
234
|
+
const exitCode = await pendingExitCode.promise;
|
|
235
|
+
console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
236
|
+
if (logFile) {
|
|
237
|
+
verbose && console.log(`[${cli}-yes] Writing rendered logs to ${logFile}`);
|
|
238
|
+
const logFilePath = path.resolve(logFile);
|
|
239
|
+
await mkdir(path.dirname(logFilePath), { recursive: true }).catch(() => null);
|
|
240
|
+
await writeFile(logFilePath, terminalRender.render());
|
|
241
|
+
}
|
|
242
|
+
return { exitCode, logs: terminalRender.render() };
|
|
243
|
+
async function sendEnter(waitms = 1000) {
|
|
244
|
+
const st = Date.now();
|
|
245
|
+
await idleWaiter.wait(waitms);
|
|
246
|
+
const et = Date.now();
|
|
247
|
+
process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
|
|
248
|
+
shell.write("\r");
|
|
249
|
+
}
|
|
250
|
+
async function sendMessage(message) {
|
|
251
|
+
await stdinReady.wait();
|
|
252
|
+
shell.write(message);
|
|
253
|
+
idleWaiter.ping();
|
|
254
|
+
await sendEnter();
|
|
255
|
+
}
|
|
256
|
+
async function exitAgent() {
|
|
257
|
+
continueOnCrash = false;
|
|
258
|
+
await sendMessage("/exit");
|
|
259
|
+
let exited = false;
|
|
260
|
+
await Promise.race([
|
|
261
|
+
pendingExitCode.promise.then(() => exited = true),
|
|
262
|
+
new Promise((resolve) => setTimeout(() => {
|
|
263
|
+
if (exited)
|
|
264
|
+
return;
|
|
265
|
+
shell.kill();
|
|
266
|
+
resolve();
|
|
267
|
+
}, 5000))
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
function getTerminalDimensions() {
|
|
271
|
+
return {
|
|
272
|
+
cols: Math.max(process.stdout.columns, 80),
|
|
273
|
+
rows: process.stdout.rows
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// cli.ts
|
|
279
|
+
var argv = yargs(hideBin(process.argv)).usage("Usage: $0 [options] [claude args] [--] [prompts...]").example('$0 --exit-on-idle=30s --continue-on-crash "help me solve all todos in my codebase"', "Run Claude with a 30 seconds idle timeout and continue on crash").option("continue-on-crash", {
|
|
280
|
+
type: "boolean",
|
|
281
|
+
default: true,
|
|
282
|
+
description: "spawn Claude with --continue if it crashes, only works for claude"
|
|
283
|
+
}).option("log-file", {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Log file to write to"
|
|
286
|
+
}).option("cli", {
|
|
287
|
+
type: "string",
|
|
288
|
+
description: 'Claude CLI command, e.g. "claude,gemini,codex,cursor,copilot", default is "claude"'
|
|
289
|
+
}).option("prompt", {
|
|
290
|
+
type: "string",
|
|
291
|
+
description: "Prompt to send to Claude",
|
|
292
|
+
alias: "p"
|
|
293
|
+
}).option("verbose", {
|
|
294
|
+
type: "boolean",
|
|
295
|
+
description: "Enable verbose logging",
|
|
296
|
+
default: false
|
|
297
|
+
}).option("exit-on-idle", {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: 'Exit after a period of inactivity, e.g., "5s" or "1m"'
|
|
300
|
+
}).parserConfiguration({
|
|
301
|
+
"unknown-options-as-args": true,
|
|
302
|
+
"halt-at-non-option": true
|
|
303
|
+
}).parseSync();
|
|
304
|
+
if (!argv.cli) {
|
|
305
|
+
const cliName = process.argv[1]?.split("/").pop()?.split("-")[0];
|
|
306
|
+
argv.cli = cliName || "claude";
|
|
307
|
+
}
|
|
308
|
+
var rawArgs = process.argv.slice(2);
|
|
309
|
+
var dashIndex = rawArgs.indexOf("--");
|
|
310
|
+
var promptFromDash = undefined;
|
|
311
|
+
var cliArgsForSpawn = [];
|
|
312
|
+
if (dashIndex !== -1) {
|
|
313
|
+
const after = rawArgs.slice(dashIndex + 1);
|
|
314
|
+
promptFromDash = after.join(" ");
|
|
315
|
+
cliArgsForSpawn = rawArgs.slice(0, dashIndex).map(String);
|
|
316
|
+
} else {
|
|
317
|
+
cliArgsForSpawn = argv._.map((e) => String(e));
|
|
318
|
+
}
|
|
319
|
+
console.clear();
|
|
320
|
+
var { exitCode, logs } = await claudeYes({
|
|
321
|
+
cli: argv.cli,
|
|
322
|
+
prompt: argv.prompt || promptFromDash,
|
|
323
|
+
exitOnIdle: argv.exitOnIdle ? enhancedMs(argv.exitOnIdle) : undefined,
|
|
324
|
+
cliArgs: cliArgsForSpawn,
|
|
325
|
+
continueOnCrash: argv.continueOnCrash,
|
|
326
|
+
logFile: argv.logFile,
|
|
327
|
+
verbose: argv.verbose
|
|
328
|
+
});
|
|
329
|
+
process.exit(exitCode ?? 1);
|
|
330
|
+
|
|
331
|
+
//# debugId=0E9D5C1D34AAD47764756E2164756E21
|
|
332
|
+
//# sourceMappingURL=cli.js.map
|