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.
@@ -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
@@ -0,0 +1,3 @@
1
+ export function sleep(ms: number) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
@@ -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
+ };