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.
- package/dist/claude-yes.js +12641 -4371
- package/dist/cli.js +12641 -4371
- package/dist/cli.js.map +205 -18
- package/dist/codex-yes.js +12641 -4371
- package/dist/copilot-yes.js +12641 -4371
- package/dist/cursor-yes.js +12641 -4371
- package/dist/gemini-yes.js +12641 -4371
- package/dist/grok-yes.js +12641 -4371
- package/dist/index.js +8387 -1729
- package/dist/index.js.map +157 -11
- package/dist/qwen-yes.js +12641 -4371
- package/package.json +15 -6
- package/ts/{tryCatch.spec.ts → catcher.spec.ts} +74 -21
- package/ts/catcher.ts +35 -0
- package/ts/cli.ts +23 -3
- package/ts/codex-resume.spec.ts +0 -4
- package/ts/codexSessionManager.test.ts +259 -0
- package/ts/codexSessionManager.ts +190 -10
- package/ts/index.ts +50 -16
- package/ts/parseCliArgs.ts +3 -3
- package/ts/yesLog.ts +1 -1
- package/ts/tryCatch.ts +0 -25
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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(
|
|
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(
|
|
41
|
-
await writeFile(
|
|
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 =
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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);
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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(
|
|
108
|
+
String(ms(e as ms.StringValue)),
|
|
109
109
|
) || 0,
|
|
110
110
|
),
|
|
111
111
|
queue: parsedArgv.queue,
|
package/ts/yesLog.ts
CHANGED
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
|
-
}
|