claude-yes 1.29.2 → 1.31.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.
@@ -1,13 +1,22 @@
1
- import { mkdir, readFile, writeFile } from 'fs/promises';
1
+ import { mkdir, readdir, readFile, writeFile } from 'fs/promises';
2
2
  import { homedir } from 'os';
3
3
  import path from 'path';
4
4
 
5
- const SESSIONS_FILE = path.join(
6
- homedir(),
7
- '.config',
8
- 'cli-yes',
9
- 'codex-sessions.json',
10
- );
5
+ // Allow overriding for testing
6
+ export const getSessionsFile = () =>
7
+ process.env.CLI_YES_TEST_HOME
8
+ ? path.join(
9
+ process.env.CLI_YES_TEST_HOME,
10
+ '.config',
11
+ 'cli-yes',
12
+ 'codex-sessions.json',
13
+ )
14
+ : path.join(homedir(), '.config', 'cli-yes', 'codex-sessions.json');
15
+
16
+ export const getCodexSessionsDir = () =>
17
+ process.env.CLI_YES_TEST_HOME
18
+ ? path.join(process.env.CLI_YES_TEST_HOME, '.codex', 'sessions')
19
+ : path.join(homedir(), '.codex', 'sessions');
11
20
 
12
21
  export interface CodexSessionMap {
13
22
  [cwd: string]: {
@@ -16,12 +25,24 @@ export interface CodexSessionMap {
16
25
  };
17
26
  }
18
27
 
28
+ export interface CodexSession {
29
+ id: string;
30
+ timestamp: string;
31
+ cwd: string;
32
+ filePath: string;
33
+ git?: {
34
+ commit_hash: string;
35
+ branch: string;
36
+ repository_url: string;
37
+ };
38
+ }
39
+
19
40
  /**
20
41
  * Load the session map from the config file
21
42
  */
22
43
  export async function loadSessionMap(): Promise<CodexSessionMap> {
23
44
  try {
24
- const content = await readFile(SESSIONS_FILE, 'utf-8');
45
+ const content = await readFile(getSessionsFile(), 'utf-8');
25
46
  return JSON.parse(content);
26
47
  } catch (error) {
27
48
  // File doesn't exist or is invalid, return empty map
@@ -36,9 +57,10 @@ export async function saveSessionMap(
36
57
  sessionMap: CodexSessionMap,
37
58
  ): Promise<void> {
38
59
  try {
60
+ const sessionsFile = getSessionsFile();
39
61
  // Ensure the directory exists
40
- await mkdir(path.dirname(SESSIONS_FILE), { recursive: true });
41
- await writeFile(SESSIONS_FILE, JSON.stringify(sessionMap, null, 2));
62
+ await mkdir(path.dirname(sessionsFile), { recursive: true });
63
+ await writeFile(sessionsFile, JSON.stringify(sessionMap, null, 2));
42
64
  } catch (error) {
43
65
  console.warn('Failed to save codex session map:', error);
44
66
  }
@@ -59,10 +81,122 @@ export async function storeSessionForCwd(
59
81
  await saveSessionMap(sessionMap);
60
82
  }
61
83
 
84
+ /**
85
+ * Parse a codex session file to extract session metadata
86
+ */
87
+ async function parseCodexSessionFile(
88
+ filePath: string,
89
+ ): Promise<CodexSession | null> {
90
+ try {
91
+ const content = await readFile(filePath, 'utf-8');
92
+ const lines = content.trim().split('\n');
93
+
94
+ // Find the session_meta line
95
+ for (const line of lines) {
96
+ if (!line.trim()) continue;
97
+
98
+ const data = JSON.parse(line);
99
+ if (data.type === 'session_meta' && data.payload) {
100
+ const payload = data.payload;
101
+ return {
102
+ id: payload.id,
103
+ timestamp: payload.timestamp || data.timestamp,
104
+ cwd: payload.cwd,
105
+ filePath,
106
+ git: payload.git,
107
+ };
108
+ }
109
+ }
110
+
111
+ return null;
112
+ } catch (error) {
113
+ // Ignore files that can't be parsed
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get all codex sessions from the .codex/sessions directory
120
+ */
121
+ async function getAllCodexSessions(): Promise<CodexSession[]> {
122
+ const sessions: CodexSession[] = [];
123
+ const codexSessionsDir = getCodexSessionsDir();
124
+
125
+ try {
126
+ // Walk through year/month/day structure
127
+ const years = await readdir(codexSessionsDir);
128
+
129
+ for (const year of years) {
130
+ const yearPath = path.join(codexSessionsDir, year);
131
+ try {
132
+ const months = await readdir(yearPath);
133
+
134
+ for (const month of months) {
135
+ const monthPath = path.join(yearPath, month);
136
+ try {
137
+ const days = await readdir(monthPath);
138
+
139
+ for (const day of days) {
140
+ const dayPath = path.join(monthPath, day);
141
+ try {
142
+ const files = await readdir(dayPath);
143
+
144
+ for (const file of files) {
145
+ if (file.endsWith('.jsonl')) {
146
+ const sessionPath = path.join(dayPath, file);
147
+ const session = await parseCodexSessionFile(sessionPath);
148
+ if (session) {
149
+ sessions.push(session);
150
+ }
151
+ }
152
+ }
153
+ } catch (error) {
154
+ // Skip directories we can't read
155
+ }
156
+ }
157
+ } catch (error) {
158
+ // Skip directories we can't read
159
+ }
160
+ }
161
+ } catch (error) {
162
+ // Skip directories we can't read
163
+ }
164
+ }
165
+ } catch (error) {
166
+ // .codex/sessions directory doesn't exist or can't be read
167
+ return [];
168
+ }
169
+
170
+ return sessions.sort(
171
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Get the most recent session for a specific working directory from actual codex files
177
+ */
178
+ async function getMostRecentCodexSessionForCwd(
179
+ targetCwd: string,
180
+ ): Promise<CodexSession | null> {
181
+ const allSessions = await getAllCodexSessions();
182
+ const sessionsForCwd = allSessions.filter(
183
+ (session) => session.cwd === targetCwd,
184
+ );
185
+ return sessionsForCwd[0] || null;
186
+ }
187
+
62
188
  /**
63
189
  * Get the last session ID for a specific working directory
190
+ * Now checks actual codex session files first, falls back to stored mapping
64
191
  */
65
192
  export async function getSessionForCwd(cwd: string): Promise<string | null> {
193
+ // First try to get the most recent session from actual codex files
194
+ const recentSession = await getMostRecentCodexSessionForCwd(cwd);
195
+ if (recentSession) {
196
+ return recentSession.id;
197
+ }
198
+
199
+ // Fall back to stored mapping
66
200
  const sessionMap = await loadSessionMap();
67
201
  return sessionMap[cwd]?.sessionId || null;
68
202
  }
@@ -101,6 +235,52 @@ export function extractSessionIdFromSessionMeta(
101
235
  return extractSessionId(sessionContent);
102
236
  }
103
237
 
238
+ /**
239
+ * Get recent sessions for a specific working directory from actual codex files
240
+ */
241
+ export async function getRecentSessionsForCwd(
242
+ targetCwd: string,
243
+ limit = 5,
244
+ ): Promise<CodexSession[]> {
245
+ const allSessions = await getAllCodexSessions();
246
+ const sessionsForCwd = allSessions.filter(
247
+ (session) => session.cwd === targetCwd,
248
+ );
249
+ return sessionsForCwd.slice(0, limit);
250
+ }
251
+
252
+ /**
253
+ * Get all working directories with session counts from actual codex files
254
+ */
255
+ export async function getAllWorkingDirectories(): Promise<
256
+ { cwd: string; count: number; lastSession: string }[]
257
+ > {
258
+ const allSessions = await getAllCodexSessions();
259
+ const cwdMap = new Map<string, { count: number; lastSession: string }>();
260
+
261
+ for (const session of allSessions) {
262
+ const existing = cwdMap.get(session.cwd);
263
+ if (existing) {
264
+ existing.count++;
265
+ if (new Date(session.timestamp) > new Date(existing.lastSession)) {
266
+ existing.lastSession = session.timestamp;
267
+ }
268
+ } else {
269
+ cwdMap.set(session.cwd, {
270
+ count: 1,
271
+ lastSession: session.timestamp,
272
+ });
273
+ }
274
+ }
275
+
276
+ return Array.from(cwdMap.entries())
277
+ .map(([cwd, data]) => ({ cwd, ...data }))
278
+ .sort(
279
+ (a, b) =>
280
+ new Date(b.lastSession).getTime() - new Date(a.lastSession).getTime(),
281
+ );
282
+ }
283
+
104
284
  /**
105
285
  * Clean up old sessions (keep only the most recent 10 per directory)
106
286
  */
package/ts/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { execaCommand, execaCommandSync, parseCommandString } from 'execa';
1
2
  import { fromReadable, fromWritable } from 'from-node-stream';
2
3
  import { mkdir, writeFile } from 'fs/promises';
3
4
  import path from 'path';
@@ -5,6 +6,7 @@ import DIE from 'phpdie';
5
6
  import sflow from 'sflow';
6
7
  import { TerminalTextRender } from 'terminal-render';
7
8
  import rawConfig from '../cli-yes.config.js';
9
+ import { catcher } from './catcher.js';
8
10
  import {
9
11
  extractSessionId,
10
12
  getSessionForCwd,
@@ -19,7 +21,6 @@ import {
19
21
  shouldUseLock,
20
22
  updateCurrentTaskStatus,
21
23
  } from './runningLock';
22
- import { catcher } from './tryCatch';
23
24
  import { yesLog } from './yesLog';
24
25
 
25
26
  export { parseCliArgs } from './parseCliArgs';
@@ -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,12 +165,12 @@ 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 (globalThis.Bun ? import('bun-pty') : import('node-pty'))
161
- // .catch(async () => await import('node-pty'))
162
- .catch(async () =>
163
- DIE('Please install node-pty or bun-pty, run this: bun install bun-pty'),
164
- );
165
- console.log(globalThis.Bun);
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
+ );
166
174
 
167
175
  // Detect if running as sub-agent
168
176
  const isSubAgent = !!process.env.CLAUDE_PPID;
@@ -223,34 +231,60 @@ export default async function cliYes({
223
231
  }
224
232
  const cliCommand = cliConf?.binary || cli;
225
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
+ };
226
240
  let shell = catcher(
227
- (error: unknown) => {
241
+ // error handler
242
+ (error: unknown, fn, ...args) => {
228
243
  console.error(`Fatal: Failed to start ${cliCommand}.`);
244
+
229
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
230
264
  console.error(
231
- `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.`,
232
266
  );
267
+ require('./fix-pty.js');
268
+ // unable to retry with same process, so exit here.
269
+ }
233
270
  throw error;
234
271
 
235
272
  function isCommandNotFoundError(e: unknown) {
236
273
  if (e instanceof Error) {
237
274
  return (
238
- e.message.includes('command not found') ||
239
- e.message.includes('ENOENT') ||
275
+ e.message.includes('command not found') || // unix
276
+ e.message.includes('ENOENT') || // unix
240
277
  e.message.includes('spawn') // windows
241
278
  );
242
279
  }
243
280
  return false;
244
281
  }
245
282
  },
246
- () => pty.spawn(cliCommand, cliArgs, getPtyOptions()),
283
+ spawn,
247
284
  )();
248
285
  const pendingExitCode = Promise.withResolvers<number | null>();
249
286
  let pendingExitCodeValue = null;
250
287
 
251
- // TODO handle error if claude is not installed, show msg:
252
- // npm install -g @anthropic-ai/claude-code
253
-
254
288
  async function onData(data: string) {
255
289
  // append data to the buffer, so we can process it later
256
290
  await outputWriter.write(data);
@@ -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)
@@ -105,7 +105,7 @@ export function parseCliArgs(argv: string[]) {
105
105
  [parsedArgv.prompt, dashPrompt].filter(Boolean).join(' ') || undefined,
106
106
  exitOnIdle: Number(
107
107
  (parsedArgv.idle || parsedArgv.exitOnIdle)?.replace(/.*/, (e) =>
108
- String(enhancedMs(e)),
108
+ String(ms(e as ms.StringValue)),
109
109
  ) || 0,
110
110
  ),
111
111
  queue: parsedArgv.queue,
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
- }