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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-yes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -50,13 +50,15 @@
|
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
52
|
"ts/*.ts",
|
|
53
|
-
"dist"
|
|
53
|
+
"dist",
|
|
54
|
+
"scripts"
|
|
54
55
|
],
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "bun run build:index && bun run build:cli",
|
|
57
58
|
"postbuild": "bun ./ts/postbuild.ts",
|
|
58
|
-
"build:cli": "bun build ts/cli.ts --outdir=dist --target=node --sourcemap --external=
|
|
59
|
-
"build:index": "bun build ts/index.ts --outdir=dist --target=node --sourcemap --external=
|
|
59
|
+
"build:cli": "bun build ts/cli.ts --outdir=dist --target=node --sourcemap --external=bun-pty --external=node-pty --external=from-node-stream --external=bun",
|
|
60
|
+
"build:index": "bun build ts/index.ts --outdir=dist --target=node --sourcemap --external=bun-pty --external=node-pty --external=from-node-stream --external=bun",
|
|
61
|
+
"demo": "bun run build && bun link && claude-yes -- demo",
|
|
60
62
|
"dev": "bun ts/index.ts",
|
|
61
63
|
"fmt": "bunx @biomejs/biome check --fix && bunx sort-package-json",
|
|
62
64
|
"prepack": "bun run build",
|
|
@@ -89,6 +91,7 @@
|
|
|
89
91
|
]
|
|
90
92
|
},
|
|
91
93
|
"dependencies": {
|
|
94
|
+
"bun": "^1.3.1",
|
|
92
95
|
"bun-pty": "^0.3.2",
|
|
93
96
|
"from-node-stream": "^0.0.11"
|
|
94
97
|
},
|
|
@@ -100,13 +103,14 @@
|
|
|
100
103
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
101
104
|
"@types/bun": "^1.2.18",
|
|
102
105
|
"@types/jest": "^30.0.0",
|
|
106
|
+
"@types/ms": "^2.1.0",
|
|
103
107
|
"@types/node": "^24.0.10",
|
|
104
108
|
"@types/yargs": "^17.0.33",
|
|
105
109
|
"cpu-wait": "^0.0.10",
|
|
106
|
-
"enhanced-ms": "^4.1.0",
|
|
107
110
|
"execa": "^9.6.0",
|
|
108
111
|
"husky": "^9.1.7",
|
|
109
112
|
"lint-staged": "^16.1.4",
|
|
113
|
+
"ms": "^2.1.3",
|
|
110
114
|
"p-map": "^7.0.3",
|
|
111
115
|
"phpdie": "^1.7.0",
|
|
112
116
|
"rambda": "^10.3.2",
|
|
@@ -119,7 +123,12 @@
|
|
|
119
123
|
"yargs": "^18.0.0"
|
|
120
124
|
},
|
|
121
125
|
"peerDependencies": {
|
|
122
|
-
"node-pty": "^1.
|
|
126
|
+
"node-pty": "^1.1.0-beta38",
|
|
123
127
|
"typescript": "^5.8.3"
|
|
128
|
+
},
|
|
129
|
+
"peerDependenciesMeta": {
|
|
130
|
+
"node-pty": {
|
|
131
|
+
"optional": true
|
|
132
|
+
}
|
|
124
133
|
}
|
|
125
134
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { catcher } from './
|
|
2
|
+
import { catcher } from './catcher';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('catcher', () => {
|
|
5
5
|
describe('curried overload', () => {
|
|
6
6
|
it('should return a function when called with only catchFn', () => {
|
|
7
7
|
const catchFn = () => 'error';
|
|
@@ -9,10 +9,14 @@ describe('tryCatch', () => {
|
|
|
9
9
|
expect(typeof result).toBe('function');
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it('should catch errors and call catchFn', () => {
|
|
12
|
+
it('should catch errors and call catchFn with error, function, and args', () => {
|
|
13
13
|
let catchedError: unknown;
|
|
14
|
-
|
|
14
|
+
let catchedFn: unknown;
|
|
15
|
+
let catchedArgs: unknown[];
|
|
16
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
15
17
|
catchedError = error;
|
|
18
|
+
catchedFn = fn;
|
|
19
|
+
catchedArgs = args;
|
|
16
20
|
return 'caught';
|
|
17
21
|
};
|
|
18
22
|
|
|
@@ -27,6 +31,8 @@ describe('tryCatch', () => {
|
|
|
27
31
|
|
|
28
32
|
expect(result).toBe('caught');
|
|
29
33
|
expect(catchedError).toBeInstanceOf(Error);
|
|
34
|
+
expect(catchedFn).toBe(errorFn);
|
|
35
|
+
expect(catchedArgs).toEqual(['arg1', 'arg2']);
|
|
30
36
|
expect(calledArgs).toEqual(['arg1', 'arg2']);
|
|
31
37
|
});
|
|
32
38
|
|
|
@@ -53,10 +59,14 @@ describe('tryCatch', () => {
|
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
describe('direct overload', () => {
|
|
56
|
-
it('should catch errors and call catchFn directly', () => {
|
|
62
|
+
it('should catch errors and call catchFn with error, function, and args directly', () => {
|
|
57
63
|
let catchedError: unknown;
|
|
58
|
-
|
|
64
|
+
let catchedFn: unknown;
|
|
65
|
+
let catchedArgs: unknown[];
|
|
66
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
59
67
|
catchedError = error;
|
|
68
|
+
catchedFn = fn;
|
|
69
|
+
catchedArgs = args;
|
|
60
70
|
return 'caught';
|
|
61
71
|
};
|
|
62
72
|
|
|
@@ -71,6 +81,8 @@ describe('tryCatch', () => {
|
|
|
71
81
|
|
|
72
82
|
expect(result).toBe('caught');
|
|
73
83
|
expect(catchedError).toBeInstanceOf(Error);
|
|
84
|
+
expect(catchedFn).toBe(errorFn);
|
|
85
|
+
expect(catchedArgs).toEqual(['arg1', 'arg2']);
|
|
74
86
|
expect(calledArgs).toEqual(['arg1', 'arg2']);
|
|
75
87
|
});
|
|
76
88
|
|
|
@@ -97,40 +109,52 @@ describe('tryCatch', () => {
|
|
|
97
109
|
});
|
|
98
110
|
|
|
99
111
|
describe('error handling', () => {
|
|
100
|
-
it('should handle different error types', () => {
|
|
112
|
+
it('should handle different error types and pass function context', () => {
|
|
101
113
|
const results: unknown[] = [];
|
|
102
|
-
const
|
|
114
|
+
const functions: unknown[] = [];
|
|
115
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
103
116
|
results.push(error);
|
|
117
|
+
functions.push(fn);
|
|
104
118
|
return 'handled';
|
|
105
119
|
};
|
|
106
120
|
|
|
107
121
|
// String error
|
|
108
|
-
const stringErrorFn =
|
|
122
|
+
const stringErrorFn = () => {
|
|
109
123
|
throw 'string error';
|
|
110
|
-
}
|
|
111
|
-
|
|
124
|
+
};
|
|
125
|
+
const wrappedStringFn = catcher(catchFn, stringErrorFn);
|
|
126
|
+
expect(wrappedStringFn()).toBe('handled');
|
|
112
127
|
expect(results[0]).toBe('string error');
|
|
128
|
+
expect(functions[0]).toBe(stringErrorFn);
|
|
113
129
|
|
|
114
130
|
// Object error
|
|
115
131
|
const objectError = { message: 'object error' };
|
|
116
|
-
const objectErrorFn =
|
|
132
|
+
const objectErrorFn = () => {
|
|
117
133
|
throw objectError;
|
|
118
|
-
}
|
|
119
|
-
|
|
134
|
+
};
|
|
135
|
+
const wrappedObjectFn = catcher(catchFn, objectErrorFn);
|
|
136
|
+
expect(wrappedObjectFn()).toBe('handled');
|
|
120
137
|
expect(results[1]).toBe(objectError);
|
|
138
|
+
expect(functions[1]).toBe(objectErrorFn);
|
|
121
139
|
|
|
122
140
|
// null error
|
|
123
|
-
const nullErrorFn =
|
|
141
|
+
const nullErrorFn = () => {
|
|
124
142
|
throw null;
|
|
125
|
-
}
|
|
126
|
-
|
|
143
|
+
};
|
|
144
|
+
const wrappedNullFn = catcher(catchFn, nullErrorFn);
|
|
145
|
+
expect(wrappedNullFn()).toBe('handled');
|
|
127
146
|
expect(results[2]).toBe(null);
|
|
147
|
+
expect(functions[2]).toBe(nullErrorFn);
|
|
128
148
|
});
|
|
129
149
|
|
|
130
|
-
it('should preserve function parameters', () => {
|
|
150
|
+
it('should preserve function parameters and pass them to catchFn', () => {
|
|
131
151
|
let caughtError: unknown;
|
|
132
|
-
|
|
152
|
+
let caughtFn: unknown;
|
|
153
|
+
let caughtArgs: unknown[];
|
|
154
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
133
155
|
caughtError = error;
|
|
156
|
+
caughtFn = fn;
|
|
157
|
+
caughtArgs = args;
|
|
134
158
|
return 'caught';
|
|
135
159
|
};
|
|
136
160
|
|
|
@@ -151,12 +175,18 @@ describe('tryCatch', () => {
|
|
|
151
175
|
expect(wrappedFn(10, 'error', false)).toBe('caught');
|
|
152
176
|
expect(testArgs).toEqual([10, 'error', false]);
|
|
153
177
|
expect(caughtError).toBeInstanceOf(Error);
|
|
178
|
+
expect(caughtFn).toBe(testFn);
|
|
179
|
+
expect(caughtArgs).toEqual([10, 'error', false]);
|
|
154
180
|
});
|
|
155
181
|
|
|
156
182
|
it('should handle functions with no parameters', () => {
|
|
157
183
|
let caughtError: unknown;
|
|
158
|
-
|
|
184
|
+
let caughtFn: unknown;
|
|
185
|
+
let caughtArgs: unknown[];
|
|
186
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
159
187
|
caughtError = error;
|
|
188
|
+
caughtFn = fn;
|
|
189
|
+
caughtArgs = args;
|
|
160
190
|
return 'no params caught';
|
|
161
191
|
};
|
|
162
192
|
|
|
@@ -172,6 +202,8 @@ describe('tryCatch', () => {
|
|
|
172
202
|
expect(result).toBe('no params caught');
|
|
173
203
|
expect(called).toBe(true);
|
|
174
204
|
expect(caughtError).toBeInstanceOf(Error);
|
|
205
|
+
expect(caughtFn).toBe(noParamsFn);
|
|
206
|
+
expect(caughtArgs).toEqual([]);
|
|
175
207
|
});
|
|
176
208
|
|
|
177
209
|
it('should handle functions returning different types', () => {
|
|
@@ -194,7 +226,8 @@ describe('tryCatch', () => {
|
|
|
194
226
|
|
|
195
227
|
describe('type safety', () => {
|
|
196
228
|
it('should maintain function signature', () => {
|
|
197
|
-
const catchFn = (error: unknown) =>
|
|
229
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) =>
|
|
230
|
+
'error';
|
|
198
231
|
const originalFn = (a: number, b: string): string => `${a}-${b}`;
|
|
199
232
|
|
|
200
233
|
const wrappedFn = catcher(catchFn, originalFn);
|
|
@@ -203,5 +236,25 @@ describe('tryCatch', () => {
|
|
|
203
236
|
const result: string = wrappedFn(1, 'test');
|
|
204
237
|
expect(result).toBe('1-test');
|
|
205
238
|
});
|
|
239
|
+
|
|
240
|
+
it('should pass function reference and arguments to catchFn', () => {
|
|
241
|
+
let capturedFn: unknown;
|
|
242
|
+
let capturedArgs: unknown[];
|
|
243
|
+
const catchFn = (error: unknown, fn: unknown, ...args: unknown[]) => {
|
|
244
|
+
capturedFn = fn;
|
|
245
|
+
capturedArgs = args;
|
|
246
|
+
return 'handled';
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const testFn = (x: number, y: string) => {
|
|
250
|
+
throw new Error('test');
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const wrappedFn = catcher(catchFn, testFn);
|
|
254
|
+
wrappedFn(42, 'hello');
|
|
255
|
+
|
|
256
|
+
expect(capturedFn).toBe(testFn);
|
|
257
|
+
expect(capturedArgs).toEqual([42, 'hello']);
|
|
258
|
+
});
|
|
206
259
|
});
|
|
207
260
|
});
|
package/ts/catcher.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// curried overload
|
|
2
|
+
export function catcher<F extends (...args: any[]) => any, R>(
|
|
3
|
+
catchFn: (error: unknown, fn: F, ...args: Parameters<F>) => 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, fn: F, ...args: Parameters<F>) => R,
|
|
9
|
+
fn: F,
|
|
10
|
+
): (...args: Parameters<F>) => ReturnType<F> | R;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A utility function to wrap another function with a try-catch block.
|
|
14
|
+
* If an error occurs during the execution of the function, the provided
|
|
15
|
+
* catchFn is called with the error, the original function, and its arguments.
|
|
16
|
+
*
|
|
17
|
+
* This function supports both direct invocation and curried usage.
|
|
18
|
+
*
|
|
19
|
+
* @param catchFn - The function to call when an error occurs.
|
|
20
|
+
* @param fn - The function to wrap (optional for curried usage).
|
|
21
|
+
* @returns A new function that wraps the original function with error handling.
|
|
22
|
+
*/
|
|
23
|
+
export function catcher<F extends (...args: any[]) => any, R>(
|
|
24
|
+
catchFn: (error: unknown, fn: F, ...args: Parameters<F>) => R,
|
|
25
|
+
fn?: F,
|
|
26
|
+
) {
|
|
27
|
+
if (!fn) return (fn: F) => catcher(catchFn, fn) as any;
|
|
28
|
+
return (...args: Parameters<F>) => {
|
|
29
|
+
try {
|
|
30
|
+
return fn(...args);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return catchFn(error, fn, ...args);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
package/ts/cli.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import DIE from 'phpdie';
|
|
3
|
-
import
|
|
3
|
+
import cliYesConfig from '../cli-yes.config';
|
|
4
4
|
|
|
5
|
-
// if
|
|
5
|
+
// if node-pty is not installed, re-run with bun
|
|
6
|
+
const hasNodePty = !!(await import('node-pty').catch(() => null));
|
|
7
|
+
if (!globalThis.Bun && !hasNodePty) {
|
|
8
|
+
// run with same arguments in Bun if not already
|
|
9
|
+
console.log('No node-pty installed. Re-running with Bun...', process.argv);
|
|
10
|
+
(await import('child_process')).spawnSync(
|
|
11
|
+
'node_modules/.bin/bun',
|
|
12
|
+
[process.argv[1]!, '--', ...process.argv.slice(2)],
|
|
13
|
+
{ stdio: 'inherit' },
|
|
14
|
+
);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
// check and fix bun-pty on some systems
|
|
18
|
+
if (globalThis.Bun) console.log('Bun detected, using bun-pty');
|
|
19
|
+
// await import("./fix-pty.js")
|
|
20
|
+
|
|
21
|
+
// console.log('Running', process.argv);
|
|
22
|
+
|
|
23
|
+
// Import the CLI module
|
|
24
|
+
const { default: cliYes, parseCliArgs } = await import('./');
|
|
6
25
|
|
|
7
26
|
// Parse CLI arguments
|
|
8
27
|
const config = parseCliArgs(process.argv);
|
|
9
28
|
|
|
10
29
|
// Validate CLI name
|
|
11
|
-
if (!config.cli)
|
|
30
|
+
if (!config.cli)
|
|
31
|
+
DIE`missing cli def, available clis: ${Object.keys((await cliYesConfig).clis).join(', ')}`;
|
|
12
32
|
|
|
13
33
|
if (config.verbose) {
|
|
14
34
|
process.env.VERBOSE = 'true'; // enable verbose logging in yesLog.ts
|
package/ts/codex-resume.spec.ts
CHANGED
|
@@ -68,10 +68,6 @@ describe('Codex Session Restoration', () => {
|
|
|
68
68
|
// Create test directories
|
|
69
69
|
await mkdir(cwd1, { recursive: true });
|
|
70
70
|
await mkdir(cwd2, { recursive: true });
|
|
71
|
-
|
|
72
|
-
// Build the project first
|
|
73
|
-
const buildResult = await runCodexYes(['--help'], process.cwd(), 10000);
|
|
74
|
-
console.log('Build check:', buildResult.exitCode === 0 ? 'OK' : 'FAILED');
|
|
75
71
|
});
|
|
76
72
|
|
|
77
73
|
afterAll(async () => {
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mkdir, rm, writeFile } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
type CodexSession,
|
|
7
|
+
extractSessionId,
|
|
8
|
+
extractSessionIdFromSessionMeta,
|
|
9
|
+
getAllWorkingDirectories,
|
|
10
|
+
getRecentSessionsForCwd,
|
|
11
|
+
getSessionForCwd,
|
|
12
|
+
storeSessionForCwd,
|
|
13
|
+
} from './codexSessionManager';
|
|
14
|
+
|
|
15
|
+
// Create a temporary test directory
|
|
16
|
+
const testDir = join(tmpdir(), 'claude-yes-test-' + Date.now());
|
|
17
|
+
const testCodexDir = join(testDir, '.codex', 'sessions');
|
|
18
|
+
const testConfigDir = join(testDir, '.config', 'cli-yes');
|
|
19
|
+
|
|
20
|
+
// Store original environment
|
|
21
|
+
const originalTestHome = process.env.CLI_YES_TEST_HOME;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
// Set up test directories
|
|
25
|
+
await mkdir(testCodexDir, { recursive: true });
|
|
26
|
+
await mkdir(testConfigDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Set test home directory
|
|
29
|
+
process.env.CLI_YES_TEST_HOME = testDir;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
// Clean up
|
|
34
|
+
process.env.CLI_YES_TEST_HOME = originalTestHome;
|
|
35
|
+
await rm(testDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Helper function to create a mock codex session file
|
|
39
|
+
async function createMockSessionFile(sessionData: {
|
|
40
|
+
id: string;
|
|
41
|
+
timestamp: string;
|
|
42
|
+
cwd: string;
|
|
43
|
+
git?: any;
|
|
44
|
+
}) {
|
|
45
|
+
const year = new Date(sessionData.timestamp).getFullYear();
|
|
46
|
+
const month = String(new Date(sessionData.timestamp).getMonth() + 1).padStart(
|
|
47
|
+
2,
|
|
48
|
+
'0',
|
|
49
|
+
);
|
|
50
|
+
const day = String(new Date(sessionData.timestamp).getDate()).padStart(
|
|
51
|
+
2,
|
|
52
|
+
'0',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const sessionDir = join(testCodexDir, String(year), month, day);
|
|
56
|
+
await mkdir(sessionDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const filename = `test-session-${sessionData.id}.jsonl`;
|
|
59
|
+
const filePath = join(sessionDir, filename);
|
|
60
|
+
|
|
61
|
+
const sessionMeta = {
|
|
62
|
+
timestamp: sessionData.timestamp,
|
|
63
|
+
type: 'session_meta',
|
|
64
|
+
payload: {
|
|
65
|
+
id: sessionData.id,
|
|
66
|
+
timestamp: sessionData.timestamp,
|
|
67
|
+
cwd: sessionData.cwd,
|
|
68
|
+
originator: 'codex_cli_rs',
|
|
69
|
+
cli_version: '0.42.0',
|
|
70
|
+
instructions: null,
|
|
71
|
+
git: sessionData.git,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const content = JSON.stringify(sessionMeta) + '\n';
|
|
76
|
+
await writeFile(filePath, content);
|
|
77
|
+
|
|
78
|
+
return filePath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('codexSessionManager', () => {
|
|
82
|
+
describe('extractSessionId', () => {
|
|
83
|
+
it('should extract valid session IDs from output', () => {
|
|
84
|
+
const output1 = 'Session ID: 019a4877-5f3c-7763-b573-513cc2d5d291';
|
|
85
|
+
const output2 =
|
|
86
|
+
'Starting session 019a4877-5f3c-7763-b573-513cc2d5d291 for user';
|
|
87
|
+
const output3 = 'No session ID here';
|
|
88
|
+
|
|
89
|
+
expect(extractSessionId(output1)).toBe(
|
|
90
|
+
'019a4877-5f3c-7763-b573-513cc2d5d291',
|
|
91
|
+
);
|
|
92
|
+
expect(extractSessionId(output2)).toBe(
|
|
93
|
+
'019a4877-5f3c-7763-b573-513cc2d5d291',
|
|
94
|
+
);
|
|
95
|
+
expect(extractSessionId(output3)).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('extractSessionIdFromSessionMeta', () => {
|
|
100
|
+
it('should extract session ID from valid session metadata', () => {
|
|
101
|
+
const sessionContent = JSON.stringify({
|
|
102
|
+
timestamp: '2025-11-03T06:46:14.123Z',
|
|
103
|
+
type: 'session_meta',
|
|
104
|
+
payload: {
|
|
105
|
+
id: '019a4877-5f3c-7763-b573-513cc2d5d291',
|
|
106
|
+
cwd: '/test/path',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(extractSessionIdFromSessionMeta(sessionContent)).toBe(
|
|
111
|
+
'019a4877-5f3c-7763-b573-513cc2d5d291',
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should fall back to regex extraction for invalid JSON', () => {
|
|
116
|
+
const invalidContent =
|
|
117
|
+
'Invalid JSON but contains 019a4877-5f3c-7763-b573-513cc2d5d291';
|
|
118
|
+
|
|
119
|
+
expect(extractSessionIdFromSessionMeta(invalidContent)).toBe(
|
|
120
|
+
'019a4877-5f3c-7763-b573-513cc2d5d291',
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('session storage and retrieval', () => {
|
|
126
|
+
it('should store and retrieve session IDs for directories', async () => {
|
|
127
|
+
const cwd = '/test/project';
|
|
128
|
+
const sessionId = '019a4877-5f3c-7763-b573-513cc2d5d291';
|
|
129
|
+
|
|
130
|
+
await storeSessionForCwd(cwd, sessionId);
|
|
131
|
+
const retrieved = await getSessionForCwd(cwd);
|
|
132
|
+
|
|
133
|
+
expect(retrieved).toBe(sessionId);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should return null for non-existent directories', async () => {
|
|
137
|
+
const result = await getSessionForCwd('/non/existent');
|
|
138
|
+
expect(result).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('codex session file parsing', () => {
|
|
143
|
+
it('should read sessions from actual codex files', async () => {
|
|
144
|
+
const sessionData = {
|
|
145
|
+
id: '019a4877-5f3c-7763-b573-513cc2d5d291',
|
|
146
|
+
timestamp: '2025-11-03T06:46:14.123Z',
|
|
147
|
+
cwd: '/v1/code/snomiao/claude-yes/tree/main',
|
|
148
|
+
git: {
|
|
149
|
+
commit_hash: 'abc123',
|
|
150
|
+
branch: 'main',
|
|
151
|
+
repository_url: 'git@github.com:snomiao/claude-yes.git',
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await createMockSessionFile(sessionData);
|
|
156
|
+
|
|
157
|
+
const retrieved = await getSessionForCwd(sessionData.cwd);
|
|
158
|
+
expect(retrieved).toBe(sessionData.id);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should get recent sessions for a directory', async () => {
|
|
162
|
+
const cwd = '/test/project';
|
|
163
|
+
const sessions = [
|
|
164
|
+
{
|
|
165
|
+
id: 'session-1',
|
|
166
|
+
timestamp: '2025-11-03T10:00:00.000Z',
|
|
167
|
+
cwd,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'session-2',
|
|
171
|
+
timestamp: '2025-11-03T09:00:00.000Z',
|
|
172
|
+
cwd,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'session-3',
|
|
176
|
+
timestamp: '2025-11-03T08:00:00.000Z',
|
|
177
|
+
cwd,
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const session of sessions) {
|
|
182
|
+
await createMockSessionFile(session);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const recent = await getRecentSessionsForCwd(cwd, 2);
|
|
186
|
+
expect(recent).toHaveLength(2);
|
|
187
|
+
expect(recent[0].id).toBe('session-1'); // Most recent first
|
|
188
|
+
expect(recent[1].id).toBe('session-2');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should get all working directories with counts', async () => {
|
|
192
|
+
const sessions = [
|
|
193
|
+
{
|
|
194
|
+
id: 'session-1',
|
|
195
|
+
timestamp: '2025-11-03T10:00:00.000Z',
|
|
196
|
+
cwd: '/project-a',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'session-2',
|
|
200
|
+
timestamp: '2025-11-03T09:00:00.000Z',
|
|
201
|
+
cwd: '/project-a',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'session-3',
|
|
205
|
+
timestamp: '2025-11-03T08:00:00.000Z',
|
|
206
|
+
cwd: '/project-b',
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
for (const session of sessions) {
|
|
211
|
+
await createMockSessionFile(session);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const directories = await getAllWorkingDirectories();
|
|
215
|
+
expect(directories).toHaveLength(2);
|
|
216
|
+
|
|
217
|
+
const projectA = directories.find((d) => d.cwd === '/project-a');
|
|
218
|
+
const projectB = directories.find((d) => d.cwd === '/project-b');
|
|
219
|
+
|
|
220
|
+
expect(projectA?.count).toBe(2);
|
|
221
|
+
expect(projectB?.count).toBe(1);
|
|
222
|
+
|
|
223
|
+
// Should be sorted by last session time (most recent first)
|
|
224
|
+
expect(directories[0].cwd).toBe('/project-a');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('fallback behavior', () => {
|
|
229
|
+
it('should fall back to stored mapping when no codex files exist', async () => {
|
|
230
|
+
const cwd = '/fallback/test';
|
|
231
|
+
const sessionId = 'fallback-session-id';
|
|
232
|
+
|
|
233
|
+
// Store in mapping but don't create codex file
|
|
234
|
+
await storeSessionForCwd(cwd, sessionId);
|
|
235
|
+
|
|
236
|
+
const retrieved = await getSessionForCwd(cwd);
|
|
237
|
+
expect(retrieved).toBe(sessionId);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should prefer codex files over stored mapping', async () => {
|
|
241
|
+
const cwd = '/preference/test';
|
|
242
|
+
const storedSessionId = 'stored-session';
|
|
243
|
+
const codexSessionId = 'codex-session';
|
|
244
|
+
|
|
245
|
+
// Store in mapping first
|
|
246
|
+
await storeSessionForCwd(cwd, storedSessionId);
|
|
247
|
+
|
|
248
|
+
// Create codex file with different session ID
|
|
249
|
+
await createMockSessionFile({
|
|
250
|
+
id: codexSessionId,
|
|
251
|
+
timestamp: '2025-11-03T10:00:00.000Z',
|
|
252
|
+
cwd,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const retrieved = await getSessionForCwd(cwd);
|
|
256
|
+
expect(retrieved).toBe(codexSessionId); // Should prefer codex file
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|