claude-yes 1.28.0 → 1.30.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
@@ -1,17 +1,17 @@
1
+ import { execaCommand, execaCommandSync, parseCommandString } from 'execa';
1
2
  import { fromReadable, fromWritable } from 'from-node-stream';
2
- import { appendFile, mkdir, rm, writeFile } from 'fs/promises';
3
+ import { mkdir, writeFile } from 'fs/promises';
3
4
  import path from 'path';
4
5
  import DIE from 'phpdie';
5
6
  import sflow from 'sflow';
6
7
  import { TerminalTextRender } from 'terminal-render';
7
- import tsaComposer from 'tsa-composer';
8
8
  import rawConfig from '../cli-yes.config.js';
9
+ import { catcher } from './catcher.js';
9
10
  import {
10
11
  extractSessionId,
11
12
  getSessionForCwd,
12
13
  storeSessionForCwd,
13
14
  } from './codexSessionManager.js';
14
- import { defineCliYesConfig } from './defineConfig.js';
15
15
  import { IdleWaiter } from './idleWaiter';
16
16
  import { ReadyManager } from './ReadyManager';
17
17
  import { removeControlCharacters } from './removeControlCharacters';
@@ -21,10 +21,11 @@ import {
21
21
  shouldUseLock,
22
22
  updateCurrentTaskStatus,
23
23
  } from './runningLock';
24
- import { catcher } from './tryCatch';
25
- import { deepMixin } from './utils';
26
24
  import { yesLog } from './yesLog';
27
25
 
26
+ export { parseCliArgs } from './parseCliArgs';
27
+ export { removeControlCharacters };
28
+
28
29
  export type AgentCliConfig = {
29
30
  install?: string; // hint user for install command if not installed
30
31
  version?: string; // hint user for version command to check if installed
@@ -36,6 +37,7 @@ export type AgentCliConfig = {
36
37
  defaultArgs?: string[]; // function to ensure certain args are present
37
38
  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
38
39
  promptArg?: (string & {}) | 'first-arg' | 'last-arg'; // argument name to pass the prompt, e.g. --prompt, or first-arg for positional arg
40
+ bunx?: boolean; // if true, use bunx to run the binary
39
41
  };
40
42
  export type CliYesConfig = {
41
43
  clis: { [key: string]: AgentCliConfig };
@@ -91,6 +93,7 @@ export default async function cliYes({
91
93
  removeControlCharactersFromStdout = false, // = !process.stdout.isTTY,
92
94
  verbose = false,
93
95
  queue = true,
96
+ install = false,
94
97
  }: {
95
98
  cli: SUPPORTED_CLIS;
96
99
  cliArgs?: string[];
@@ -103,6 +106,7 @@ export default async function cliYes({
103
106
  removeControlCharactersFromStdout?: boolean;
104
107
  verbose?: boolean;
105
108
  queue?: boolean;
109
+ install?: boolean; // if true, install the cli tool if not installed, e.g. will run `npm install -g cursor-agent`
106
110
  }) {
107
111
  // those overrides seems only works in bun
108
112
  // await Promise.allSettled([
@@ -118,7 +122,11 @@ export default async function cliYes({
118
122
  // });
119
123
 
120
124
  if (!cli) throw new Error(`cli is required`);
121
- const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}`);
125
+ const conf =
126
+ CLIS_CONFIG[cli] ||
127
+ DIE(
128
+ `Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(' ')}`,
129
+ );
122
130
 
123
131
  // Acquire lock before starting agent (if in git repo or same cwd and lock is not disabled)
124
132
  const workingDir = cwd ?? process.cwd();
@@ -157,17 +165,29 @@ export default async function cliYes({
157
165
  // const pty = await import('node-pty');
158
166
 
159
167
  // its recommened to use bun-pty in windows
160
- const pty = await import('node-pty')
161
- .catch(async () => await import('bun-pty'))
162
- .catch(async () =>
163
- DIE('Please install node-pty or bun-pty, run this: bun install bun-pty'),
168
+ const pty = await (globalThis.Bun
169
+ ? import('bun-pty')
170
+ : import('node-pty')
171
+ ).catch(async () =>
172
+ DIE('Please install node-pty or bun-pty, run this: bun install bun-pty'),
173
+ );
174
+
175
+ // Detect if running as sub-agent
176
+ const isSubAgent = !!process.env.CLAUDE_PPID;
177
+ if (isSubAgent) {
178
+ console.log(
179
+ `[${cli}-yes] Running as sub-agent (CLAUDE_PPID=${process.env.CLAUDE_PPID})`,
164
180
  );
181
+ }
165
182
 
166
183
  const getPtyOptions = () => ({
167
184
  name: 'xterm-color',
168
185
  ...getTerminalDimensions(),
169
186
  cwd: cwd ?? process.cwd(),
170
- env: env ?? (process.env as Record<string, string>),
187
+ env: {
188
+ ...(env ?? (process.env as Record<string, string>)),
189
+ CLAUDE_PPID: String(process.ppid),
190
+ },
171
191
  });
172
192
 
173
193
  // Apply CLI specific configurations (moved to CLI_CONFIGURES)
@@ -211,34 +231,60 @@ export default async function cliYes({
211
231
  }
212
232
  const cliCommand = cliConf?.binary || cli;
213
233
 
234
+ const spawn = () => {
235
+ // const [bin, ...args] = [...parseCommandString((cliConf.bunx ? 'bunx --bun ' : '') + cliCommand), ...(cliArgs)];
236
+ // console.log(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
237
+ // return pty.spawn(bin!, args, getPtyOptions());
238
+ return pty.spawn(cliCommand, cliArgs, getPtyOptions());
239
+ };
214
240
  let shell = catcher(
215
- (error: unknown) => {
241
+ // error handler
242
+ (error: unknown, fn, ...args) => {
216
243
  console.error(`Fatal: Failed to start ${cliCommand}.`);
244
+
217
245
  if (cliConf?.install && isCommandNotFoundError(error))
246
+ if (install) {
247
+ console.log(`Attempting to install ${cli}...`);
248
+ execaCommandSync(cliConf.install, { stdio: 'inherit' });
249
+ console.log(
250
+ `${cli} installed successfully. Please rerun the command.`,
251
+ );
252
+ return spawn();
253
+ }
254
+ console.error(
255
+ `If you did not installed it yet, Please install it first: ${cliConf.install}`,
256
+ );
257
+
258
+ if (
259
+ globalThis.Bun &&
260
+ error instanceof Error &&
261
+ error.stack?.includes('bun-pty')
262
+ ) {
263
+ // try to fix bun-pty issues
218
264
  console.error(
219
- `If you did not installed it yet, Please install it first: ${cliConf.install}`,
265
+ `Detected bun-pty issue, attempted to fix it. Please try again.`,
220
266
  );
267
+ require('./fix-pty.js');
268
+ // unable to retry with same process, so exit here.
269
+ }
221
270
  throw error;
222
271
 
223
272
  function isCommandNotFoundError(e: unknown) {
224
273
  if (e instanceof Error) {
225
274
  return (
226
- e.message.includes('command not found') ||
227
- e.message.includes('ENOENT') ||
275
+ e.message.includes('command not found') || // unix
276
+ e.message.includes('ENOENT') || // unix
228
277
  e.message.includes('spawn') // windows
229
278
  );
230
279
  }
231
280
  return false;
232
281
  }
233
282
  },
234
- () => pty.spawn(cliCommand, cliArgs, getPtyOptions()),
283
+ spawn,
235
284
  )();
236
285
  const pendingExitCode = Promise.withResolvers<number | null>();
237
286
  let pendingExitCodeValue = null;
238
287
 
239
- // TODO handle error if claude is not installed, show msg:
240
- // npm install -g @anthropic-ai/claude-code
241
-
242
288
  async function onData(data: string) {
243
289
  // append data to the buffer, so we can process it later
244
290
  await outputWriter.write(data);
@@ -496,5 +542,3 @@ export default async function cliYes({
496
542
  };
497
543
  }
498
544
  }
499
-
500
- export { removeControlCharacters };
@@ -1,4 +1,4 @@
1
- import enhancedMs from 'enhanced-ms';
1
+ import ms from 'ms';
2
2
  import yargs from 'yargs';
3
3
  import { hideBin } from 'yargs/helpers';
4
4
  import { SUPPORTED_CLIS } from '.';
@@ -12,7 +12,7 @@ export function parseCliArgs(argv: string[]) {
12
12
  const scriptName = argv[1]?.split(/[\/\\]/).pop();
13
13
  const cliName = ((e?: string) => {
14
14
  if (e === 'cli' || e === 'cli.ts' || e === 'cli.js') return undefined;
15
- return e?.replace(/-yes$/, '');
15
+ return e?.replace(/-yes(\.[jt]s)?$/, '');
16
16
  })(scriptName);
17
17
 
18
18
  // Parse args with yargs (same logic as cli.ts:16-73)
@@ -87,12 +87,14 @@ export function parseCliArgs(argv: string[]) {
87
87
  ? rawArgs.slice(cliArgIndex ?? 0, dashIndex ?? undefined)
88
88
  : [];
89
89
  const dashPrompt: string | undefined =
90
- dashIndex !== undefined
91
- ? rawArgs.slice(dashIndex + 1).join(' ')
92
- : undefined;
90
+ dashIndex === undefined
91
+ ? undefined
92
+ : rawArgs.slice(dashIndex + 1).join(' ');
93
93
 
94
94
  // Return the config object that would be passed to cliYes (same logic as cli.ts:99-121)
95
95
  return {
96
+ cwd: process.cwd(),
97
+ env: process.env as Record<string, string>,
96
98
  cli: (cliName ||
97
99
  parsedArgv.cli ||
98
100
  parsedArgv._[0]
@@ -103,7 +105,7 @@ export function parseCliArgs(argv: string[]) {
103
105
  [parsedArgv.prompt, dashPrompt].filter(Boolean).join(' ') || undefined,
104
106
  exitOnIdle: Number(
105
107
  (parsedArgv.idle || parsedArgv.exitOnIdle)?.replace(/.*/, (e) =>
106
- String(enhancedMs(e)),
108
+ String(ms(e as ms.StringValue)),
107
109
  ) || 0,
108
110
  ),
109
111
  queue: parsedArgv.queue,
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { removeControlCharacters } from './removeControlCharacters';
3
+
4
+ describe('removeControlCharacters', () => {
5
+ it('should remove ANSI escape sequences', () => {
6
+ const input = '\u001b[31mRed text\u001b[0m';
7
+ const expected = 'Red text';
8
+ expect(removeControlCharacters(input)).toBe(expected);
9
+ });
10
+
11
+ it('should remove cursor positioning codes', () => {
12
+ const input = '\u001b[1;1HHello\u001b[2;1HWorld';
13
+ const expected = 'HelloWorld';
14
+ expect(removeControlCharacters(input)).toBe(expected);
15
+ });
16
+
17
+ it('should remove color codes', () => {
18
+ const input = '\u001b[32mGreen\u001b[0m \u001b[31mRed\u001b[0m';
19
+ const expected = 'Green Red';
20
+ expect(removeControlCharacters(input)).toBe(expected);
21
+ });
22
+
23
+ it('should remove complex ANSI sequences', () => {
24
+ const input = '\u001b[1;33;40mYellow on black\u001b[0m';
25
+ const expected = 'Yellow on black';
26
+ expect(removeControlCharacters(input)).toBe(expected);
27
+ });
28
+
29
+ it('should handle empty string', () => {
30
+ expect(removeControlCharacters('')).toBe('');
31
+ });
32
+
33
+ it('should handle string with no control characters', () => {
34
+ const input = 'Plain text with no escape sequences';
35
+ expect(removeControlCharacters(input)).toBe(input);
36
+ });
37
+
38
+ it('should remove CSI sequences with multiple parameters', () => {
39
+ const input = '\u001b[38;5;196mBright red\u001b[0m';
40
+ const expected = 'Bright red';
41
+ expect(removeControlCharacters(input)).toBe(expected);
42
+ });
43
+
44
+ it('should remove C1 control characters', () => {
45
+ const input = '\u009b[32mGreen text\u009b[0m';
46
+ const expected = 'Green text';
47
+ expect(removeControlCharacters(input)).toBe(expected);
48
+ });
49
+
50
+ it('should handle mixed control and regular characters', () => {
51
+ const input =
52
+ 'Start\u001b[1mBold\u001b[0mMiddle\u001b[4mUnderline\u001b[0mEnd';
53
+ const expected = 'StartBoldMiddleUnderlineEnd';
54
+ expect(removeControlCharacters(input)).toBe(expected);
55
+ });
56
+
57
+ it('should preserve spaces and newlines', () => {
58
+ const input = 'Line 1\u001b[31m\nRed Line 2\u001b[0m\n\nLine 4';
59
+ const expected = 'Line 1\nRed Line 2\n\nLine 4';
60
+ expect(removeControlCharacters(input)).toBe(expected);
61
+ });
62
+
63
+ it('should handle cursor movement sequences', () => {
64
+ const input = '\u001b[2AUp\u001b[3BDown\u001b[4CRight\u001b[5DLeft';
65
+ const expected = 'UpDownRightLeft';
66
+ expect(removeControlCharacters(input)).toBe(expected);
67
+ });
68
+
69
+ it('should handle erase sequences', () => {
70
+ const input = 'Text\u001b[2JClear\u001b[KLine';
71
+ const expected = 'TextClearLine';
72
+ expect(removeControlCharacters(input)).toBe(expected);
73
+ });
74
+ });
@@ -0,0 +1,169 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { type DeepPartial, deepMixin, sleepms } from './utils';
3
+
4
+ describe('utils', () => {
5
+ describe('sleepms', () => {
6
+ it('should return a promise', () => {
7
+ const result = sleepms(100);
8
+ expect(result).toBeInstanceOf(Promise);
9
+ });
10
+
11
+ it('should resolve after some time', async () => {
12
+ const start = Date.now();
13
+ await sleepms(10);
14
+ const end = Date.now();
15
+ expect(end - start).toBeGreaterThanOrEqual(5); // Allow some margin
16
+ });
17
+
18
+ it('should handle zero milliseconds', async () => {
19
+ const start = Date.now();
20
+ await sleepms(0);
21
+ const end = Date.now();
22
+ expect(end - start).toBeLessThan(50); // Should be quick
23
+ });
24
+ });
25
+
26
+ describe('deepMixin', () => {
27
+ it('should merge simple properties', () => {
28
+ const target = { a: 1, b: 2 };
29
+ const source = { b: 3, c: 4 };
30
+ const result = deepMixin(target, source);
31
+
32
+ expect(result).toEqual({ a: 1, b: 3, c: 4 });
33
+ expect(result).toBe(target); // Should modify original object
34
+ });
35
+
36
+ it('should merge nested objects', () => {
37
+ const target = {
38
+ user: { name: 'John', age: 30 },
39
+ settings: { theme: 'dark' },
40
+ };
41
+ const source: DeepPartial<typeof target> = {
42
+ user: { age: 31 },
43
+ settings: { language: 'en' } as any,
44
+ };
45
+
46
+ deepMixin(target, source);
47
+
48
+ expect(target).toEqual({
49
+ user: { name: 'John', age: 31 },
50
+ settings: { theme: 'dark', language: 'en' },
51
+ });
52
+ });
53
+
54
+ it('should create nested objects when target property is null', () => {
55
+ const target: any = { config: null };
56
+ const source = { config: { enabled: true } };
57
+
58
+ deepMixin(target, source);
59
+
60
+ expect(target).toEqual({
61
+ config: { enabled: true },
62
+ });
63
+ });
64
+
65
+ it('should create nested objects when target property is primitive', () => {
66
+ const target: any = { config: 'string' };
67
+ const source = { config: { enabled: true } };
68
+
69
+ deepMixin(target, source);
70
+
71
+ expect(target).toEqual({
72
+ config: { enabled: true },
73
+ });
74
+ });
75
+
76
+ it('should handle arrays by replacing them', () => {
77
+ const target = { items: [1, 2, 3] };
78
+ const source = { items: [4, 5] };
79
+
80
+ deepMixin(target, source);
81
+
82
+ expect(target).toEqual({ items: [4, 5] });
83
+ });
84
+
85
+ it('should ignore undefined values', () => {
86
+ const target = { a: 1, b: 2 };
87
+ const source = { a: undefined, c: 3 };
88
+
89
+ deepMixin(target, source);
90
+
91
+ expect(target).toEqual({ a: 1, b: 2, c: 3 });
92
+ });
93
+
94
+ it('should handle null values', () => {
95
+ const target = { a: 1, b: 2 };
96
+ const source = { a: null, c: 3 };
97
+
98
+ deepMixin(target, source);
99
+
100
+ expect(target).toEqual({ a: null, b: 2, c: 3 });
101
+ });
102
+
103
+ it('should handle deeply nested structures', () => {
104
+ const target = {
105
+ level1: {
106
+ level2: {
107
+ level3: { value: 'old' },
108
+ },
109
+ },
110
+ };
111
+ const source = {
112
+ level1: {
113
+ level2: {
114
+ level3: { value: 'new', extra: 'added' },
115
+ },
116
+ },
117
+ };
118
+
119
+ deepMixin(target, source);
120
+
121
+ expect(target).toEqual({
122
+ level1: {
123
+ level2: {
124
+ level3: { value: 'new', extra: 'added' },
125
+ },
126
+ },
127
+ });
128
+ });
129
+
130
+ it('should handle empty objects', () => {
131
+ const target = {};
132
+ const source = {};
133
+
134
+ const result = deepMixin(target, source);
135
+
136
+ expect(result).toEqual({});
137
+ expect(result).toBe(target);
138
+ });
139
+
140
+ it('should handle complex mixed types', () => {
141
+ const target: any = {
142
+ string: 'value',
143
+ number: 42,
144
+ boolean: true,
145
+ object: { nested: 'value' },
146
+ array: [1, 2, 3],
147
+ };
148
+ const source: any = {
149
+ string: 'new value',
150
+ number: 100,
151
+ boolean: false,
152
+ object: { nested: 'new value', added: 'property' },
153
+ array: [4, 5],
154
+ newProp: 'added',
155
+ };
156
+
157
+ deepMixin(target, source);
158
+
159
+ expect(target).toEqual({
160
+ string: 'new value',
161
+ number: 100,
162
+ boolean: false,
163
+ object: { nested: 'new value', added: 'property' },
164
+ array: [4, 5],
165
+ newProp: 'added',
166
+ });
167
+ });
168
+ });
169
+ });
@@ -0,0 +1,74 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ describe('yesLog', () => {
4
+ const originalVerbose = process.env.VERBOSE;
5
+
6
+ beforeEach(() => {
7
+ // Reset modules to ensure fresh state
8
+ delete require.cache[require.resolve('./yesLog')];
9
+ });
10
+
11
+ afterEach(() => {
12
+ // Restore original VERBOSE setting
13
+ if (originalVerbose !== undefined) {
14
+ process.env.VERBOSE = originalVerbose;
15
+ } else {
16
+ delete process.env.VERBOSE;
17
+ }
18
+ });
19
+
20
+ it('should not crash when VERBOSE is not set', async () => {
21
+ delete process.env.VERBOSE;
22
+
23
+ const { yesLog } = await import('./yesLog');
24
+
25
+ // Should not throw and returns undefined
26
+ const result = yesLog`Test message`;
27
+ expect(result).toBeUndefined();
28
+ });
29
+
30
+ it('should be callable with template literals', async () => {
31
+ delete process.env.VERBOSE;
32
+
33
+ const { yesLog } = await import('./yesLog');
34
+
35
+ // Should not throw with variables
36
+ const variable = 'test value';
37
+ const result = yesLog`Message with ${variable}`;
38
+ expect(result).toBeUndefined();
39
+ });
40
+
41
+ it('should handle multiple calls', async () => {
42
+ delete process.env.VERBOSE;
43
+
44
+ const { yesLog } = await import('./yesLog');
45
+
46
+ // Multiple calls should not throw
47
+ expect(yesLog`First message`).toBeUndefined();
48
+ expect(yesLog`Second message`).toBeUndefined();
49
+ expect(yesLog`Third message`).toBeUndefined();
50
+ });
51
+
52
+ it('should work when VERBOSE is set', async () => {
53
+ process.env.VERBOSE = '1';
54
+
55
+ const { yesLog } = await import('./yesLog');
56
+
57
+ // Should not throw even when verbose
58
+ expect(yesLog`Verbose message`).toBeUndefined();
59
+ });
60
+
61
+ it('should handle template literals with different types', async () => {
62
+ delete process.env.VERBOSE;
63
+
64
+ const { yesLog } = await import('./yesLog');
65
+
66
+ const number = 42;
67
+ const object = { key: 'value' };
68
+ const array = [1, 2, 3];
69
+
70
+ expect(yesLog`Number: ${number}`).toBeUndefined();
71
+ expect(yesLog`Object: ${object}`).toBeUndefined();
72
+ expect(yesLog`Array: ${array}`).toBeUndefined();
73
+ });
74
+ });
package/ts/yesLog.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { appendFileSync, rmSync } from 'node:fs';
2
2
  import tsaComposer from 'tsa-composer';
3
- import { catcher } from './tryCatch';
3
+ import { catcher } from './catcher';
4
4
 
5
5
  let initial = true;
6
6
 
package/ts/tryCatch.ts DELETED
@@ -1,25 +0,0 @@
1
- // curried overload
2
- export function catcher<F extends (...args: any[]) => any, R>(
3
- catchFn: (error: unknown) => R,
4
- ): (fn: F) => (...args: Parameters<F>) => ReturnType<F> | R;
5
-
6
- // direct overload
7
- export function catcher<F extends (...args: any[]) => any, R>(
8
- catchFn: (error: unknown) => R,
9
- fn: F,
10
- ): (...args: Parameters<F>) => ReturnType<F> | R;
11
-
12
- // implementation
13
- export function catcher<F extends (...args: any[]) => any, R>(
14
- catchFn: (error: unknown) => R,
15
- fn?: F,
16
- ) {
17
- if (!fn) return (fn: F) => catcher(catchFn, fn) as any;
18
- return (...args: Parameters<F>) => {
19
- try {
20
- return fn(...args);
21
- } catch (error) {
22
- return catchFn(error);
23
- }
24
- };
25
- }