agent-yes 1.31.41
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/LICENSE +21 -0
- package/README.md +504 -0
- package/dist/agent-yes.js +2 -0
- package/dist/amp-yes.js +2 -0
- package/dist/auggie-yes.js +2 -0
- package/dist/claude-yes.js +2 -0
- package/dist/cli.js +31474 -0
- package/dist/cli.js.map +483 -0
- package/dist/codex-yes.js +2 -0
- package/dist/copilot-yes.js +2 -0
- package/dist/cursor-yes.js +2 -0
- package/dist/gemini-yes.js +2 -0
- package/dist/grok-yes.js +2 -0
- package/dist/index.js +25148 -0
- package/dist/index.js.map +435 -0
- package/dist/qwen-yes.js +2 -0
- package/package.json +145 -0
- package/ts/ReadyManager.spec.ts +72 -0
- package/ts/ReadyManager.ts +16 -0
- package/ts/SUPPORTED_CLIS.ts +5 -0
- package/ts/catcher.spec.ts +259 -0
- package/ts/catcher.ts +35 -0
- package/ts/cli-idle.spec.ts +20 -0
- package/ts/cli.ts +30 -0
- package/ts/defineConfig.ts +12 -0
- package/ts/idleWaiter.spec.ts +55 -0
- package/ts/idleWaiter.ts +31 -0
- package/ts/index.ts +783 -0
- package/ts/logger.ts +17 -0
- package/ts/parseCliArgs.spec.ts +231 -0
- package/ts/parseCliArgs.ts +182 -0
- package/ts/postbuild.ts +29 -0
- package/ts/pty-fix.ts +155 -0
- package/ts/pty.ts +18 -0
- package/ts/removeControlCharacters.spec.ts +73 -0
- package/ts/removeControlCharacters.ts +8 -0
- package/ts/runningLock.spec.ts +485 -0
- package/ts/runningLock.ts +362 -0
- package/ts/session-integration.spec.ts +93 -0
- package/ts/sleep.ts +3 -0
- package/ts/utils.spec.ts +169 -0
- package/ts/utils.ts +23 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
export interface Task {
|
|
8
|
+
cwd: string;
|
|
9
|
+
gitRoot?: string;
|
|
10
|
+
task: string;
|
|
11
|
+
pid: number;
|
|
12
|
+
status: "running" | "queued" | "completed" | "failed";
|
|
13
|
+
startedAt: number;
|
|
14
|
+
lockedAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LockFile {
|
|
18
|
+
tasks: Task[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LockCheckResult {
|
|
22
|
+
isLocked: boolean;
|
|
23
|
+
blockingTasks: Task[];
|
|
24
|
+
lockKey: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getLockDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
|
|
28
|
+
const getLockFile = () => path.join(getLockDir(), "running.lock.json");
|
|
29
|
+
const MAX_RETRIES = 5;
|
|
30
|
+
const RETRY_DELAYS = [50, 100, 200, 400, 800]; // exponential backoff in ms
|
|
31
|
+
const POLL_INTERVAL = 2000; // 2 seconds
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a process is running
|
|
35
|
+
*/
|
|
36
|
+
function isProcessRunning(pid: number): boolean {
|
|
37
|
+
try {
|
|
38
|
+
// Sending signal 0 checks if process exists without killing it
|
|
39
|
+
process.kill(pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get git repository root for a directory
|
|
48
|
+
*/
|
|
49
|
+
function getGitRoot(cwd: string): string | null {
|
|
50
|
+
try {
|
|
51
|
+
const result = execSync("git rev-parse --show-toplevel", {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
55
|
+
});
|
|
56
|
+
return result.trim();
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if directory is in a git repository
|
|
64
|
+
*/
|
|
65
|
+
function isGitRepo(cwd: string): boolean {
|
|
66
|
+
try {
|
|
67
|
+
const gitRoot = getGitRoot(cwd);
|
|
68
|
+
return gitRoot !== null;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve path to real path (handling symlinks)
|
|
76
|
+
*/
|
|
77
|
+
function resolveRealPath(p: string): string {
|
|
78
|
+
try {
|
|
79
|
+
return path.resolve(p);
|
|
80
|
+
} catch {
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sleep for a given number of milliseconds
|
|
87
|
+
*/
|
|
88
|
+
function sleep(ms: number): Promise<void> {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Read lock file with retry logic and stale lock cleanup
|
|
94
|
+
*/
|
|
95
|
+
async function readLockFile(): Promise<LockFile> {
|
|
96
|
+
try {
|
|
97
|
+
const lockDir = getLockDir();
|
|
98
|
+
const lockFilePath = getLockFile();
|
|
99
|
+
await mkdir(lockDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
if (!existsSync(lockFilePath)) {
|
|
102
|
+
return { tasks: [] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const content = await readFile(lockFilePath, "utf8");
|
|
106
|
+
const lockFile = JSON.parse(content) as LockFile;
|
|
107
|
+
|
|
108
|
+
// Clean stale locks while reading
|
|
109
|
+
lockFile.tasks = lockFile.tasks.filter((task) => {
|
|
110
|
+
if (isProcessRunning(task.pid)) return true;
|
|
111
|
+
return false;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return lockFile;
|
|
115
|
+
} catch {
|
|
116
|
+
// If file is corrupted or doesn't exist, return empty lock file
|
|
117
|
+
return { tasks: [] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Write lock file atomically with retry logic
|
|
123
|
+
*/
|
|
124
|
+
async function writeLockFile(lockFile: LockFile, retryCount = 0): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
const lockDir = getLockDir();
|
|
127
|
+
const lockFilePath = getLockFile();
|
|
128
|
+
await mkdir(lockDir, { recursive: true });
|
|
129
|
+
|
|
130
|
+
const tempFile = `${lockFilePath}.tmp.${process.pid}`;
|
|
131
|
+
await writeFile(tempFile, JSON.stringify(lockFile, null, 2), "utf8");
|
|
132
|
+
|
|
133
|
+
// Atomic rename
|
|
134
|
+
await rename(tempFile, lockFilePath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (retryCount < MAX_RETRIES) {
|
|
137
|
+
// Exponential backoff retry
|
|
138
|
+
await sleep(RETRY_DELAYS[retryCount] || 800);
|
|
139
|
+
return writeLockFile(lockFile, retryCount + 1);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if lock exists for the current working directory
|
|
147
|
+
*/
|
|
148
|
+
async function checkLock(cwd: string, _prompt: string): Promise<LockCheckResult> {
|
|
149
|
+
const resolvedCwd = resolveRealPath(cwd);
|
|
150
|
+
const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
|
|
151
|
+
const lockKey = gitRoot || resolvedCwd;
|
|
152
|
+
|
|
153
|
+
const lockFile = await readLockFile();
|
|
154
|
+
|
|
155
|
+
// Find running tasks for this location
|
|
156
|
+
const blockingTasks = lockFile.tasks.filter((task) => {
|
|
157
|
+
if (!isProcessRunning(task.pid)) return false; // Skip stale locks
|
|
158
|
+
if (task.status !== "running") return false; // Only check running tasks
|
|
159
|
+
|
|
160
|
+
if (gitRoot && task.gitRoot) {
|
|
161
|
+
// In git repo: check by git root
|
|
162
|
+
return task.gitRoot === gitRoot;
|
|
163
|
+
} else {
|
|
164
|
+
// Not in git repo: exact cwd match
|
|
165
|
+
return task.cwd === lockKey;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
isLocked: blockingTasks.length > 0,
|
|
171
|
+
blockingTasks,
|
|
172
|
+
lockKey,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Add a task to the lock file
|
|
178
|
+
*/
|
|
179
|
+
async function addTask(task: Task): Promise<void> {
|
|
180
|
+
const lockFile = await readLockFile();
|
|
181
|
+
|
|
182
|
+
// Remove any existing task with same PID (shouldn't happen, but be safe)
|
|
183
|
+
lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== task.pid);
|
|
184
|
+
|
|
185
|
+
lockFile.tasks.push(task);
|
|
186
|
+
await writeLockFile(lockFile);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update task status
|
|
191
|
+
*/
|
|
192
|
+
async function updateTaskStatus(pid: number, status: Task["status"]): Promise<void> {
|
|
193
|
+
const lockFile = await readLockFile();
|
|
194
|
+
const task = lockFile.tasks.find((t) => t.pid === pid);
|
|
195
|
+
|
|
196
|
+
if (task) {
|
|
197
|
+
task.status = status;
|
|
198
|
+
await writeLockFile(lockFile);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Remove a task from the lock file
|
|
204
|
+
*/
|
|
205
|
+
async function removeTask(pid: number): Promise<void> {
|
|
206
|
+
const lockFile = await readLockFile();
|
|
207
|
+
lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== pid);
|
|
208
|
+
await writeLockFile(lockFile);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Wait for lock to be released
|
|
213
|
+
*/
|
|
214
|
+
async function waitForUnlock(blockingTasks: Task[], currentTask: Task): Promise<void> {
|
|
215
|
+
const blockingTask = blockingTasks[0];
|
|
216
|
+
if (!blockingTask) return;
|
|
217
|
+
console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
|
|
218
|
+
console.log(` Press 'b' to bypass queue, 'k' to kill previous instance`);
|
|
219
|
+
|
|
220
|
+
// Add current task as 'queued'
|
|
221
|
+
await addTask({ ...currentTask, status: "queued" });
|
|
222
|
+
|
|
223
|
+
// Set up keyboard input handling
|
|
224
|
+
const stdin = process.stdin;
|
|
225
|
+
const wasRaw = stdin.isRaw;
|
|
226
|
+
stdin.setRawMode?.(true);
|
|
227
|
+
stdin.resume();
|
|
228
|
+
|
|
229
|
+
let bypassed = false;
|
|
230
|
+
let killed = false;
|
|
231
|
+
|
|
232
|
+
const keyHandler = (key: Buffer) => {
|
|
233
|
+
const char = key.toString();
|
|
234
|
+
if (char === "b" || char === "B") {
|
|
235
|
+
console.log("\n⚡ Bypassing queue...");
|
|
236
|
+
bypassed = true;
|
|
237
|
+
} else if (char === "k" || char === "K") {
|
|
238
|
+
console.log("\n🔪 Killing previous instance...");
|
|
239
|
+
killed = true;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
stdin.on("data", keyHandler);
|
|
244
|
+
|
|
245
|
+
let dots = 0;
|
|
246
|
+
while (true) {
|
|
247
|
+
if (bypassed) {
|
|
248
|
+
// Force bypass - update status to running immediately
|
|
249
|
+
await updateTaskStatus(currentTask.pid, "running");
|
|
250
|
+
console.log("✓ Queue bypassed, starting task...");
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (killed && blockingTask) {
|
|
255
|
+
// Kill the blocking task's process
|
|
256
|
+
try {
|
|
257
|
+
process.kill(blockingTask.pid, "SIGTERM");
|
|
258
|
+
console.log(`✓ Killed process ${blockingTask.pid}`);
|
|
259
|
+
// Wait a bit for the process to be killed
|
|
260
|
+
await sleep(1000);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.log(`⚠️ Could not kill process ${blockingTask.pid}: ${err}`);
|
|
263
|
+
}
|
|
264
|
+
killed = false; // Reset flag after attempting kill
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await sleep(POLL_INTERVAL);
|
|
268
|
+
|
|
269
|
+
const lockCheck = await checkLock(currentTask.cwd, currentTask.task);
|
|
270
|
+
|
|
271
|
+
if (!lockCheck.isLocked) {
|
|
272
|
+
// Lock released, update status to running
|
|
273
|
+
await updateTaskStatus(currentTask.pid, "running");
|
|
274
|
+
console.log(`\n✓ Lock released, starting task...`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Show progress indicator
|
|
279
|
+
dots = (dots + 1) % 4;
|
|
280
|
+
process.stdout.write(`\r⏳ Queueing${".".repeat(dots)}${" ".repeat(3 - dots)}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Clean up keyboard handler
|
|
284
|
+
stdin.off("data", keyHandler);
|
|
285
|
+
stdin.setRawMode?.(wasRaw);
|
|
286
|
+
if (!wasRaw) stdin.pause();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Clean stale locks from the lock file
|
|
291
|
+
*/
|
|
292
|
+
export async function cleanStaleLocks(): Promise<void> {
|
|
293
|
+
const lockFile = await readLockFile();
|
|
294
|
+
|
|
295
|
+
const before = lockFile.tasks.length;
|
|
296
|
+
lockFile.tasks = lockFile.tasks.filter((task) => {
|
|
297
|
+
if (isProcessRunning(task.pid)) return true;
|
|
298
|
+
|
|
299
|
+
console.log(`🧹 Cleaned stale lock for PID ${task.pid}`);
|
|
300
|
+
return false;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (lockFile.tasks.length !== before) {
|
|
304
|
+
await writeLockFile(lockFile);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Acquire lock or wait if locked
|
|
310
|
+
*/
|
|
311
|
+
export async function acquireLock(
|
|
312
|
+
cwd: string,
|
|
313
|
+
prompt: string = "no prompt provided",
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const resolvedCwd = resolveRealPath(cwd);
|
|
316
|
+
const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
|
|
317
|
+
|
|
318
|
+
const task: Task = {
|
|
319
|
+
cwd: resolvedCwd,
|
|
320
|
+
gitRoot: gitRoot || undefined,
|
|
321
|
+
task: prompt.substring(0, 100), // Limit task description length
|
|
322
|
+
pid: process.pid,
|
|
323
|
+
status: "running",
|
|
324
|
+
startedAt: Date.now(),
|
|
325
|
+
lockedAt: Date.now(),
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const lockCheck = await checkLock(resolvedCwd, prompt);
|
|
329
|
+
|
|
330
|
+
if (lockCheck.isLocked) {
|
|
331
|
+
await waitForUnlock(lockCheck.blockingTasks, task);
|
|
332
|
+
} else {
|
|
333
|
+
await addTask(task);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Release lock for current process
|
|
339
|
+
*/
|
|
340
|
+
export async function releaseLock(pid: number = process.pid): Promise<void> {
|
|
341
|
+
await removeTask(pid);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Update status of current task
|
|
346
|
+
*/
|
|
347
|
+
export async function updateCurrentTaskStatus(
|
|
348
|
+
status: Task["status"],
|
|
349
|
+
pid: number = process.pid,
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
await updateTaskStatus(pid, status);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check if we should use locking for this directory
|
|
356
|
+
* Only use locking if we're in a git repository
|
|
357
|
+
*/
|
|
358
|
+
export function shouldUseLock(_cwd: string): boolean {
|
|
359
|
+
// Only use lock if in git repo OR if explicitly requested
|
|
360
|
+
// For now, use lock for all cases to handle same-dir conflicts
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { extractSessionId, extractSessionIdFromSessionMeta } from "./resume/codexSessionManager";
|
|
3
|
+
|
|
4
|
+
describe("Session Extraction Test", () => {
|
|
5
|
+
it("should extract session IDs from various codex output formats", async () => {
|
|
6
|
+
console.log("\n=== Session ID Extraction Test ===");
|
|
7
|
+
|
|
8
|
+
// Test different formats where session IDs might appear
|
|
9
|
+
const testCases = [
|
|
10
|
+
{
|
|
11
|
+
name: "Direct UUID in output",
|
|
12
|
+
output: "Session started with ID: 0199e659-0e5f-7843-8876-5a65c64e77c0",
|
|
13
|
+
expected: "0199e659-0e5f-7843-8876-5a65c64e77c0",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "UUID in brackets",
|
|
17
|
+
output: "Using session [0199e659-0e5f-7843-8876-5a65c64e77c0] for this conversation",
|
|
18
|
+
expected: "0199e659-0e5f-7843-8876-5a65c64e77c0",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "Mixed case UUID",
|
|
22
|
+
output: "SESSION_ID: 0199E659-0E5F-7843-8876-5A65C64E77C0",
|
|
23
|
+
expected: "0199E659-0E5F-7843-8876-5A65C64E77C0",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "No UUID present",
|
|
27
|
+
output: "Welcome to codex! Type your message and press enter.",
|
|
28
|
+
expected: null,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Multiple UUIDs (should get first)",
|
|
32
|
+
output:
|
|
33
|
+
"Old: 1111e659-0e5f-7843-8876-5a65c64e77c0 New: 2222e659-0e5f-7843-8876-5a65c64e77c0",
|
|
34
|
+
expected: "1111e659-0e5f-7843-8876-5a65c64e77c0",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const testCase of testCases) {
|
|
39
|
+
console.log(`Testing: ${testCase.name}`);
|
|
40
|
+
const result = extractSessionId(testCase.output);
|
|
41
|
+
console.log(` Input: ${testCase.output}`);
|
|
42
|
+
console.log(` Expected: ${testCase.expected}`);
|
|
43
|
+
console.log(` Got: ${result}`);
|
|
44
|
+
expect(result).toBe(testCase.expected);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log("✅ All session extraction tests passed!\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should extract session ID from session metadata JSON", async () => {
|
|
51
|
+
console.log("\n=== Session Metadata Extraction Test ===");
|
|
52
|
+
|
|
53
|
+
const sessionMetaJson = `{"timestamp":"2025-10-15T05:30:20.265Z","type":"session_meta","payload":{"id":"0199e659-0e5f-7843-8876-5a65c64e77c0","timestamp":"2025-10-15T05:30:20.127Z","cwd":"/v1/code/project","originator":"codex_cli_rs"}}
|
|
54
|
+
{"timestamp":"2025-10-15T05:30:20.415Z","type":"response_item","payload":{"type":"message","role":"user"}}`;
|
|
55
|
+
|
|
56
|
+
const sessionId = extractSessionIdFromSessionMeta(sessionMetaJson);
|
|
57
|
+
console.log(`Extracted session ID: ${sessionId}`);
|
|
58
|
+
expect(sessionId).toBe("0199e659-0e5f-7843-8876-5a65c64e77c0");
|
|
59
|
+
|
|
60
|
+
console.log("✅ Session metadata extraction test passed!\n");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should demonstrate session tracking workflow", async () => {
|
|
64
|
+
console.log("\n=== Session Tracking Workflow Demo ===");
|
|
65
|
+
|
|
66
|
+
// Simulate codex output that would contain session information
|
|
67
|
+
const mockCodexOutputs = [
|
|
68
|
+
{
|
|
69
|
+
directory: "logs/cwd1",
|
|
70
|
+
output: "Starting new conversation... Session ID: aaaa1111-2222-3333-4444-bbbbccccdddd",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
directory: "logs/cwd2",
|
|
74
|
+
output: "Resuming session... Using ID: bbbb2222-3333-4444-5555-ccccddddeeee",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
console.log("Simulating session capture from codex output:");
|
|
79
|
+
|
|
80
|
+
for (const mock of mockCodexOutputs) {
|
|
81
|
+
const sessionId = extractSessionId(mock.output);
|
|
82
|
+
console.log(`Directory: ${mock.directory}`);
|
|
83
|
+
console.log(`Output: ${mock.output}`);
|
|
84
|
+
console.log(`Captured Session ID: ${sessionId}`);
|
|
85
|
+
console.log("---");
|
|
86
|
+
|
|
87
|
+
expect(sessionId).toBeTruthy();
|
|
88
|
+
expect(sessionId).toMatch(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log("✅ Workflow demonstration completed!\n");
|
|
92
|
+
});
|
|
93
|
+
});
|
package/ts/sleep.ts
ADDED
package/ts/utils.spec.ts
ADDED
|
@@ -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
|
+
});
|
package/ts/utils.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function sleepms(ms: number) {
|
|
2
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
}
|
|
4
|
+
export function deepMixin<T>(target: T, source: DeepPartial<T>, ...more: DeepPartial<T>[]): T {
|
|
5
|
+
for (const key in source) {
|
|
6
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
7
|
+
if (!target[key] || typeof target[key] !== "object") {
|
|
8
|
+
(target as any)[key] = {};
|
|
9
|
+
}
|
|
10
|
+
deepMixin(target[key], source[key] as any);
|
|
11
|
+
} else if (source[key] !== undefined) {
|
|
12
|
+
(target as any)[key] = source[key];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const moreSource of more) {
|
|
17
|
+
deepMixin(target, moreSource);
|
|
18
|
+
}
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
export type DeepPartial<T> = {
|
|
22
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
23
|
+
};
|