agent-yes 1.70.1 → 1.71.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,4 +1,5 @@
1
1
  import { t as logger } from "./logger-CX77vJDA.js";
2
+ import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-BBI_URhR.js";
2
3
  import { arch, platform } from "process";
3
4
  import { execSync } from "child_process";
4
5
  import { execaCommandSync, parseCommandString } from "execa";
@@ -178,251 +179,6 @@ function removeControlCharacters(str) {
178
179
  return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
179
180
  }
180
181
 
181
- //#endregion
182
- //#region ts/runningLock.ts
183
- const getLockDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
184
- const getLockFile = () => path.join(getLockDir(), "running.lock.json");
185
- const MAX_RETRIES = 5;
186
- const RETRY_DELAYS = [
187
- 50,
188
- 100,
189
- 200,
190
- 400,
191
- 800
192
- ];
193
- const POLL_INTERVAL = 2e3;
194
- /**
195
- * Check if a process is running
196
- */
197
- function isProcessRunning(pid) {
198
- try {
199
- process.kill(pid, 0);
200
- return true;
201
- } catch {
202
- return false;
203
- }
204
- }
205
- /**
206
- * Get git repository root for a directory
207
- */
208
- function getGitRoot(cwd) {
209
- try {
210
- return execSync("git rev-parse --show-toplevel", {
211
- cwd,
212
- encoding: "utf8",
213
- stdio: [
214
- "pipe",
215
- "pipe",
216
- "ignore"
217
- ]
218
- }).trim();
219
- } catch {
220
- return null;
221
- }
222
- }
223
- /**
224
- * Check if directory is in a git repository
225
- */
226
- function isGitRepo(cwd) {
227
- try {
228
- return getGitRoot(cwd) !== null;
229
- } catch {
230
- return false;
231
- }
232
- }
233
- /**
234
- * Resolve path to real path (handling symlinks)
235
- */
236
- function resolveRealPath(p) {
237
- try {
238
- return path.resolve(p);
239
- } catch {
240
- return p;
241
- }
242
- }
243
- /**
244
- * Sleep for a given number of milliseconds
245
- */
246
- function sleep$1(ms) {
247
- return new Promise((resolve) => setTimeout(resolve, ms));
248
- }
249
- /**
250
- * Read lock file with retry logic and stale lock cleanup
251
- */
252
- async function readLockFile() {
253
- try {
254
- const lockDir = getLockDir();
255
- const lockFilePath = getLockFile();
256
- await mkdir(lockDir, { recursive: true });
257
- if (!existsSync(lockFilePath)) return { tasks: [] };
258
- const content = await readFile(lockFilePath, "utf8");
259
- const lockFile = JSON.parse(content);
260
- lockFile.tasks = lockFile.tasks.filter((task) => {
261
- if (isProcessRunning(task.pid)) return true;
262
- return false;
263
- });
264
- return lockFile;
265
- } catch {
266
- return { tasks: [] };
267
- }
268
- }
269
- /**
270
- * Write lock file atomically with retry logic
271
- */
272
- async function writeLockFile(lockFile, retryCount = 0) {
273
- try {
274
- const lockDir = getLockDir();
275
- const lockFilePath = getLockFile();
276
- await mkdir(lockDir, { recursive: true });
277
- const tempFile = `${lockFilePath}.tmp.${process.pid}`;
278
- await writeFile(tempFile, JSON.stringify(lockFile, null, 2), "utf8");
279
- await rename(tempFile, lockFilePath);
280
- } catch (error) {
281
- if (retryCount < MAX_RETRIES) {
282
- await sleep$1(RETRY_DELAYS[retryCount] || 800);
283
- return writeLockFile(lockFile, retryCount + 1);
284
- }
285
- throw error;
286
- }
287
- }
288
- /**
289
- * Check if lock exists for the current working directory
290
- */
291
- async function checkLock(cwd, _prompt) {
292
- const resolvedCwd = resolveRealPath(cwd);
293
- const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
294
- const lockKey = gitRoot || resolvedCwd;
295
- const blockingTasks = (await readLockFile()).tasks.filter((task) => {
296
- if (!isProcessRunning(task.pid)) return false;
297
- if (task.status !== "running") return false;
298
- if (gitRoot && task.gitRoot) return task.gitRoot === gitRoot;
299
- else return task.cwd === lockKey;
300
- });
301
- return {
302
- isLocked: blockingTasks.length > 0,
303
- blockingTasks,
304
- lockKey
305
- };
306
- }
307
- /**
308
- * Add a task to the lock file
309
- */
310
- async function addTask(task) {
311
- const lockFile = await readLockFile();
312
- lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== task.pid);
313
- lockFile.tasks.push(task);
314
- await writeLockFile(lockFile);
315
- }
316
- /**
317
- * Update task status
318
- */
319
- async function updateTaskStatus(pid, status) {
320
- const lockFile = await readLockFile();
321
- const task = lockFile.tasks.find((t) => t.pid === pid);
322
- if (task) {
323
- task.status = status;
324
- await writeLockFile(lockFile);
325
- }
326
- }
327
- /**
328
- * Remove a task from the lock file
329
- */
330
- async function removeTask(pid) {
331
- const lockFile = await readLockFile();
332
- lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== pid);
333
- await writeLockFile(lockFile);
334
- }
335
- /**
336
- * Wait for lock to be released
337
- */
338
- async function waitForUnlock(blockingTasks, currentTask) {
339
- const blockingTask = blockingTasks[0];
340
- if (!blockingTask) return;
341
- console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
342
- console.log(` Press 'b' to bypass queue, 'k' to kill previous instance`);
343
- await addTask({
344
- ...currentTask,
345
- status: "queued"
346
- });
347
- const stdin = process.stdin;
348
- const wasRaw = stdin.isRaw;
349
- stdin.setRawMode?.(true);
350
- stdin.resume();
351
- let bypassed = false;
352
- let killed = false;
353
- const keyHandler = (key) => {
354
- const char = key.toString();
355
- if (char === "b" || char === "B") {
356
- console.log("\n⚡ Bypassing queue...");
357
- bypassed = true;
358
- } else if (char === "k" || char === "K") {
359
- console.log("\n🔪 Killing previous instance...");
360
- killed = true;
361
- }
362
- };
363
- stdin.on("data", keyHandler);
364
- let dots = 0;
365
- while (true) {
366
- if (bypassed) {
367
- await updateTaskStatus(currentTask.pid, "running");
368
- console.log("✓ Queue bypassed, starting task...");
369
- break;
370
- }
371
- if (killed && blockingTask) {
372
- try {
373
- process.kill(blockingTask.pid, "SIGTERM");
374
- console.log(`✓ Killed process ${blockingTask.pid}`);
375
- await sleep$1(1e3);
376
- } catch (err) {
377
- console.log(`⚠️ Could not kill process ${blockingTask.pid}: ${err}`);
378
- }
379
- killed = false;
380
- }
381
- await sleep$1(POLL_INTERVAL);
382
- if (!(await checkLock(currentTask.cwd, currentTask.task)).isLocked) {
383
- await updateTaskStatus(currentTask.pid, "running");
384
- console.log(`\n✓ Lock released, starting task...`);
385
- break;
386
- }
387
- dots = (dots + 1) % 4;
388
- process.stdout.write(`\r⏳ Queueing${".".repeat(dots)}${" ".repeat(3 - dots)}`);
389
- }
390
- stdin.off("data", keyHandler);
391
- stdin.setRawMode?.(wasRaw);
392
- if (!wasRaw) stdin.pause();
393
- }
394
- /**
395
- * Acquire lock or wait if locked
396
- */
397
- async function acquireLock(cwd, prompt = "no prompt provided") {
398
- const resolvedCwd = resolveRealPath(cwd);
399
- const task = {
400
- cwd: resolvedCwd,
401
- gitRoot: (isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null) || void 0,
402
- task: prompt.substring(0, 100),
403
- pid: process.pid,
404
- status: "running",
405
- startedAt: Date.now(),
406
- lockedAt: Date.now()
407
- };
408
- const lockCheck = await checkLock(resolvedCwd, prompt);
409
- if (lockCheck.isLocked) await waitForUnlock(lockCheck.blockingTasks, task);
410
- else await addTask(task);
411
- }
412
- /**
413
- * Release lock for current process
414
- */
415
- async function releaseLock(pid = process.pid) {
416
- await removeTask(pid);
417
- }
418
- /**
419
- * Check if we should use locking for this directory
420
- * Only use locking if we're in a git repository
421
- */
422
- function shouldUseLock(_cwd) {
423
- return true;
424
- }
425
-
426
182
  //#endregion
427
183
  //#region ts/beta/fifo.ts
428
184
  /**
@@ -1059,7 +815,7 @@ function tryCatch(catchFn, fn) {
1059
815
  //#endregion
1060
816
  //#region package.json
1061
817
  var name = "agent-yes";
1062
- var version = "1.70.1";
818
+ var version = "1.71.0";
1063
819
 
1064
820
  //#endregion
1065
821
  //#region ts/pty-fix.ts
@@ -1486,7 +1242,7 @@ async function notifyWebhook(status, details, cwd = process.cwd()) {
1486
1242
 
1487
1243
  //#endregion
1488
1244
  //#region ts/index.ts
1489
- const config = await import("./agent-yes.config-DcxG25Gv.js").then((mod) => mod.default || mod);
1245
+ const config = await import("./agent-yes.config-CtQprJrA.js").then((mod) => mod.default || mod);
1490
1246
  const CLIS_CONFIG = config.clis;
1491
1247
  /**
1492
1248
  * Main function to run agent-cli with automatic yes/no responses
@@ -2139,4 +1895,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
2139
1895
 
2140
1896
  //#endregion
2141
1897
  export { AgentContext as a, PidStore as c, config as i, removeControlCharacters as l, CLIS_CONFIG as n, name as o, agentYes as r, version as s, SUPPORTED_CLIS as t };
2142
- //# sourceMappingURL=SUPPORTED_CLIS-Bq5hoKRN.js.map
1898
+ //# sourceMappingURL=SUPPORTED_CLIS-DtYo1wxO.js.map
@@ -332,4 +332,4 @@ function getDefaultConfig() {
332
332
 
333
333
  //#endregion
334
334
  export { agent_yes_config_default as default };
335
- //# sourceMappingURL=agent-yes.config-DcxG25Gv.js.map
335
+ //# sourceMappingURL=agent-yes.config-CtQprJrA.js.map
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Bq5hoKRN.js";
2
+ import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DtYo1wxO.js";
3
3
  import { t as logger } from "./logger-CX77vJDA.js";
4
4
  import { argv } from "process";
5
5
  import { spawn } from "child_process";
@@ -91,6 +91,10 @@ function parseCliArgs(argv) {
91
91
  description: "Pass --dangerously-skip-permissions to the CLI (claude shortcut)",
92
92
  default: false,
93
93
  alias: "y"
94
+ }).option("tray", {
95
+ type: "boolean",
96
+ description: "Show a system tray icon with running agent count (macOS/Windows only)",
97
+ default: false
94
98
  }).option("rust", {
95
99
  type: "boolean",
96
100
  description: "Use the Rust implementation (enabled by default, use --no-rust for TypeScript)",
@@ -193,6 +197,7 @@ function parseCliArgs(argv) {
193
197
  showVersion: parsedArgv.version,
194
198
  autoYes: parsedArgv.auto !== "no",
195
199
  idleAction: parsedArgv.idleAction,
200
+ tray: parsedArgv.tray,
196
201
  useRust: parsedArgv.rust,
197
202
  swarm: parsedArgv.swarm ?? (parsedArgv.experimentalSwarm ? parsedArgv.swarmTopic : void 0),
198
203
  experimentalSwarm: parsedArgv.experimentalSwarm,
@@ -475,6 +480,11 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
475
480
  //#region ts/cli.ts
476
481
  const updateCheckPromise = checkAndAutoUpdate();
477
482
  const config = parseCliArgs(process.argv);
483
+ if (config.tray) {
484
+ const { startTray } = await import("./tray-BzSS0v-i.js");
485
+ await startTray();
486
+ await new Promise(() => {});
487
+ }
478
488
  if (config.useRust) {
479
489
  let rustBinary;
480
490
  try {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-Bq5hoKRN.js";
1
+ import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-DtYo1wxO.js";
2
2
  import "./logger-CX77vJDA.js";
3
3
 
4
4
  export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
@@ -0,0 +1,263 @@
1
+ import { execSync } from "child_process";
2
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
3
+ import path from "path";
4
+ import { homedir } from "os";
5
+ import { existsSync } from "fs";
6
+
7
+ //#region ts/runningLock.ts
8
+ const getLockDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
9
+ const getLockFile = () => path.join(getLockDir(), "running.lock.json");
10
+ const MAX_RETRIES = 5;
11
+ const RETRY_DELAYS = [
12
+ 50,
13
+ 100,
14
+ 200,
15
+ 400,
16
+ 800
17
+ ];
18
+ const POLL_INTERVAL = 2e3;
19
+ /**
20
+ * Check if a process is running
21
+ */
22
+ function isProcessRunning(pid) {
23
+ try {
24
+ process.kill(pid, 0);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Get git repository root for a directory
32
+ */
33
+ function getGitRoot(cwd) {
34
+ try {
35
+ return execSync("git rev-parse --show-toplevel", {
36
+ cwd,
37
+ encoding: "utf8",
38
+ stdio: [
39
+ "pipe",
40
+ "pipe",
41
+ "ignore"
42
+ ]
43
+ }).trim();
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Check if directory is in a git repository
50
+ */
51
+ function isGitRepo(cwd) {
52
+ try {
53
+ return getGitRoot(cwd) !== null;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * Resolve path to real path (handling symlinks)
60
+ */
61
+ function resolveRealPath(p) {
62
+ try {
63
+ return path.resolve(p);
64
+ } catch {
65
+ return p;
66
+ }
67
+ }
68
+ /**
69
+ * Sleep for a given number of milliseconds
70
+ */
71
+ function sleep(ms) {
72
+ return new Promise((resolve) => setTimeout(resolve, ms));
73
+ }
74
+ /**
75
+ * Read lock file with retry logic and stale lock cleanup
76
+ */
77
+ async function readLockFile() {
78
+ try {
79
+ const lockDir = getLockDir();
80
+ const lockFilePath = getLockFile();
81
+ await mkdir(lockDir, { recursive: true });
82
+ if (!existsSync(lockFilePath)) return { tasks: [] };
83
+ const content = await readFile(lockFilePath, "utf8");
84
+ const lockFile = JSON.parse(content);
85
+ lockFile.tasks = lockFile.tasks.filter((task) => {
86
+ if (isProcessRunning(task.pid)) return true;
87
+ return false;
88
+ });
89
+ return lockFile;
90
+ } catch {
91
+ return { tasks: [] };
92
+ }
93
+ }
94
+ /**
95
+ * Write lock file atomically with retry logic
96
+ */
97
+ async function writeLockFile(lockFile, retryCount = 0) {
98
+ try {
99
+ const lockDir = getLockDir();
100
+ const lockFilePath = getLockFile();
101
+ await mkdir(lockDir, { recursive: true });
102
+ const tempFile = `${lockFilePath}.tmp.${process.pid}`;
103
+ await writeFile(tempFile, JSON.stringify(lockFile, null, 2), "utf8");
104
+ await rename(tempFile, lockFilePath);
105
+ } catch (error) {
106
+ if (retryCount < MAX_RETRIES) {
107
+ await sleep(RETRY_DELAYS[retryCount] || 800);
108
+ return writeLockFile(lockFile, retryCount + 1);
109
+ }
110
+ throw error;
111
+ }
112
+ }
113
+ /**
114
+ * Check if lock exists for the current working directory
115
+ */
116
+ async function checkLock(cwd, _prompt) {
117
+ const resolvedCwd = resolveRealPath(cwd);
118
+ const gitRoot = isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null;
119
+ const lockKey = gitRoot || resolvedCwd;
120
+ const blockingTasks = (await readLockFile()).tasks.filter((task) => {
121
+ if (!isProcessRunning(task.pid)) return false;
122
+ if (task.status !== "running") return false;
123
+ if (gitRoot && task.gitRoot) return task.gitRoot === gitRoot;
124
+ else return task.cwd === lockKey;
125
+ });
126
+ return {
127
+ isLocked: blockingTasks.length > 0,
128
+ blockingTasks,
129
+ lockKey
130
+ };
131
+ }
132
+ /**
133
+ * Add a task to the lock file
134
+ */
135
+ async function addTask(task) {
136
+ const lockFile = await readLockFile();
137
+ lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== task.pid);
138
+ lockFile.tasks.push(task);
139
+ await writeLockFile(lockFile);
140
+ }
141
+ /**
142
+ * Update task status
143
+ */
144
+ async function updateTaskStatus(pid, status) {
145
+ const lockFile = await readLockFile();
146
+ const task = lockFile.tasks.find((t) => t.pid === pid);
147
+ if (task) {
148
+ task.status = status;
149
+ await writeLockFile(lockFile);
150
+ }
151
+ }
152
+ /**
153
+ * Remove a task from the lock file
154
+ */
155
+ async function removeTask(pid) {
156
+ const lockFile = await readLockFile();
157
+ lockFile.tasks = lockFile.tasks.filter((t) => t.pid !== pid);
158
+ await writeLockFile(lockFile);
159
+ }
160
+ /**
161
+ * Wait for lock to be released
162
+ */
163
+ async function waitForUnlock(blockingTasks, currentTask) {
164
+ const blockingTask = blockingTasks[0];
165
+ if (!blockingTask) return;
166
+ console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
167
+ console.log(` Press 'b' to bypass queue, 'k' to kill previous instance`);
168
+ await addTask({
169
+ ...currentTask,
170
+ status: "queued"
171
+ });
172
+ const stdin = process.stdin;
173
+ const wasRaw = stdin.isRaw;
174
+ stdin.setRawMode?.(true);
175
+ stdin.resume();
176
+ let bypassed = false;
177
+ let killed = false;
178
+ const keyHandler = (key) => {
179
+ const char = key.toString();
180
+ if (char === "b" || char === "B") {
181
+ console.log("\n⚡ Bypassing queue...");
182
+ bypassed = true;
183
+ } else if (char === "k" || char === "K") {
184
+ console.log("\n🔪 Killing previous instance...");
185
+ killed = true;
186
+ }
187
+ };
188
+ stdin.on("data", keyHandler);
189
+ let dots = 0;
190
+ while (true) {
191
+ if (bypassed) {
192
+ await updateTaskStatus(currentTask.pid, "running");
193
+ console.log("✓ Queue bypassed, starting task...");
194
+ break;
195
+ }
196
+ if (killed && blockingTask) {
197
+ try {
198
+ process.kill(blockingTask.pid, "SIGTERM");
199
+ console.log(`✓ Killed process ${blockingTask.pid}`);
200
+ await sleep(1e3);
201
+ } catch (err) {
202
+ console.log(`⚠️ Could not kill process ${blockingTask.pid}: ${err}`);
203
+ }
204
+ killed = false;
205
+ }
206
+ await sleep(POLL_INTERVAL);
207
+ if (!(await checkLock(currentTask.cwd, currentTask.task)).isLocked) {
208
+ await updateTaskStatus(currentTask.pid, "running");
209
+ console.log(`\n✓ Lock released, starting task...`);
210
+ break;
211
+ }
212
+ dots = (dots + 1) % 4;
213
+ process.stdout.write(`\r⏳ Queueing${".".repeat(dots)}${" ".repeat(3 - dots)}`);
214
+ }
215
+ stdin.off("data", keyHandler);
216
+ stdin.setRawMode?.(wasRaw);
217
+ if (!wasRaw) stdin.pause();
218
+ }
219
+ /**
220
+ * Get the count of currently running agents
221
+ */
222
+ async function getRunningAgentCount() {
223
+ const running = (await readLockFile()).tasks.filter((t) => t.status === "running");
224
+ return {
225
+ count: running.length,
226
+ tasks: running
227
+ };
228
+ }
229
+ /**
230
+ * Acquire lock or wait if locked
231
+ */
232
+ async function acquireLock(cwd, prompt = "no prompt provided") {
233
+ const resolvedCwd = resolveRealPath(cwd);
234
+ const task = {
235
+ cwd: resolvedCwd,
236
+ gitRoot: (isGitRepo(resolvedCwd) ? getGitRoot(resolvedCwd) : null) || void 0,
237
+ task: prompt.substring(0, 100),
238
+ pid: process.pid,
239
+ status: "running",
240
+ startedAt: Date.now(),
241
+ lockedAt: Date.now()
242
+ };
243
+ const lockCheck = await checkLock(resolvedCwd, prompt);
244
+ if (lockCheck.isLocked) await waitForUnlock(lockCheck.blockingTasks, task);
245
+ else await addTask(task);
246
+ }
247
+ /**
248
+ * Release lock for current process
249
+ */
250
+ async function releaseLock(pid = process.pid) {
251
+ await removeTask(pid);
252
+ }
253
+ /**
254
+ * Check if we should use locking for this directory
255
+ * Only use locking if we're in a git repository
256
+ */
257
+ function shouldUseLock(_cwd) {
258
+ return true;
259
+ }
260
+
261
+ //#endregion
262
+ export { shouldUseLock as i, getRunningAgentCount as n, releaseLock as r, acquireLock as t };
263
+ //# sourceMappingURL=runningLock-BBI_URhR.js.map
@@ -0,0 +1,109 @@
1
+ import { n as getRunningAgentCount } from "./runningLock-BBI_URhR.js";
2
+
3
+ //#region ts/tray.ts
4
+ const POLL_INTERVAL = 2e3;
5
+ const ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAjklEQVQ4T2NkoBAwUqifgWoGMDIyNjAyMv5nYGBYQMgVjMgCQM0LGBkZHYDYAY8BDUBxByB2wGcAyAUOQOwAxPYMDAyOeCzAbwBIMyMjowNQsz0ely8ACjng8wJeA0CaGRgY7IHYAZ8hQHEHfF7AawBYMwODPZABRHsBpwEgzUDN9kDsgM8lQHEHfC4gJhwAAM3hMBGq3cNNAAAAAElFTkSuQmCC";
6
+ function buildMenuItems(tasks) {
7
+ const items = [];
8
+ if (tasks.length === 0) items.push({
9
+ title: "No running agents",
10
+ tooltip: "",
11
+ enabled: false
12
+ });
13
+ else {
14
+ items.push({
15
+ title: `Running agents: ${tasks.length}`,
16
+ tooltip: "",
17
+ enabled: false
18
+ });
19
+ items.push({
20
+ title: "---",
21
+ tooltip: "",
22
+ enabled: false
23
+ });
24
+ for (const task of tasks) {
25
+ const dir = task.cwd.replace(/^.*[/\\]/, "");
26
+ const desc = task.task ? ` - ${task.task.slice(0, 40)}` : "";
27
+ items.push({
28
+ title: `[${task.pid}] ${dir}${desc}`,
29
+ tooltip: task.cwd,
30
+ enabled: false
31
+ });
32
+ }
33
+ }
34
+ items.push({
35
+ title: "---",
36
+ tooltip: "",
37
+ enabled: false
38
+ });
39
+ items.push({
40
+ title: "Quit Tray",
41
+ tooltip: "Exit tray icon",
42
+ enabled: true
43
+ });
44
+ return items;
45
+ }
46
+ async function startTray() {
47
+ if (process.platform !== "darwin" && process.platform !== "win32") {
48
+ console.error("Tray icon is only supported on macOS and Windows.");
49
+ return;
50
+ }
51
+ let SysTray;
52
+ try {
53
+ SysTray = (await import("systray2")).default;
54
+ } catch {
55
+ console.error("systray2 is not installed. Install it with: npm install systray2");
56
+ return;
57
+ }
58
+ const { count, tasks } = await getRunningAgentCount();
59
+ const systray = new SysTray({
60
+ menu: {
61
+ icon: ICON_BASE64,
62
+ title: `AY: ${count}`,
63
+ tooltip: `agent-yes: ${count} running`,
64
+ items: buildMenuItems(tasks)
65
+ },
66
+ debug: false,
67
+ copyDir: false
68
+ });
69
+ await systray.ready();
70
+ console.log(`Tray started. Watching ${count} running agent(s).`);
71
+ systray.onClick((action) => {
72
+ if (action.item.title === "Quit Tray") {
73
+ systray.kill(false);
74
+ process.exit(0);
75
+ }
76
+ });
77
+ let lastCount = count;
78
+ const interval = setInterval(async () => {
79
+ try {
80
+ const { count: newCount, tasks: newTasks } = await getRunningAgentCount();
81
+ if (newCount !== lastCount) {
82
+ lastCount = newCount;
83
+ systray.sendAction({
84
+ type: "update-menu",
85
+ menu: {
86
+ icon: ICON_BASE64,
87
+ title: `AY: ${newCount}`,
88
+ tooltip: `agent-yes: ${newCount} running`,
89
+ items: buildMenuItems(newTasks)
90
+ }
91
+ });
92
+ }
93
+ } catch {}
94
+ }, POLL_INTERVAL);
95
+ process.on("SIGINT", () => {
96
+ clearInterval(interval);
97
+ systray.kill(false);
98
+ process.exit(0);
99
+ });
100
+ process.on("SIGTERM", () => {
101
+ clearInterval(interval);
102
+ systray.kill(false);
103
+ process.exit(0);
104
+ });
105
+ }
106
+
107
+ //#endregion
108
+ export { startTray };
109
+ //# sourceMappingURL=tray-BzSS0v-i.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.70.1",
3
+ "version": "1.71.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
  "ai",
@@ -130,6 +130,9 @@
130
130
  "optional": true
131
131
  }
132
132
  },
133
+ "optionalDependencies": {
134
+ "systray2": "^2.1.4"
135
+ },
133
136
  "engines": {
134
137
  "node": ">=22.0.0"
135
138
  },
package/ts/cli.ts CHANGED
@@ -15,6 +15,13 @@ const updateCheckPromise = checkAndAutoUpdate();
15
15
  // Parse CLI arguments
16
16
  const config = parseCliArgs(process.argv);
17
17
 
18
+ // Handle --tray: show system tray icon and block
19
+ if (config.tray) {
20
+ const { startTray } = await import("./tray.ts");
21
+ await startTray();
22
+ await new Promise(() => {}); // Block forever, exit via tray quit or signal
23
+ }
24
+
18
25
  // Handle --rust: spawn the Rust binary instead, fall back to TypeScript if unavailable
19
26
  if (config.useRust) {
20
27
  let rustBinary: string | undefined;
@@ -126,6 +126,11 @@ export function parseCliArgs(argv: string[]) {
126
126
  default: false,
127
127
  alias: "y",
128
128
  })
129
+ .option("tray", {
130
+ type: "boolean",
131
+ description: "Show a system tray icon with running agent count (macOS/Windows only)",
132
+ default: false,
133
+ })
129
134
  .option("rust", {
130
135
  type: "boolean",
131
136
  description: "Use the Rust implementation (enabled by default, use --no-rust for TypeScript)",
@@ -275,6 +280,7 @@ export function parseCliArgs(argv: string[]) {
275
280
  showVersion: parsedArgv.version,
276
281
  autoYes: parsedArgv.auto !== "no", // auto-yes enabled by default, disabled with --auto=no
277
282
  idleAction: parsedArgv.idleAction as string | undefined,
283
+ tray: parsedArgv.tray,
278
284
  useRust: parsedArgv.rust,
279
285
  // New unified --swarm flag (takes precedence over deprecated flags)
280
286
  swarm: parsedArgv.swarm ?? (parsedArgv.experimentalSwarm ? parsedArgv.swarmTopic : undefined),
package/ts/runningLock.ts CHANGED
@@ -286,6 +286,20 @@ async function waitForUnlock(blockingTasks: Task[], currentTask: Task): Promise<
286
286
  if (!wasRaw) stdin.pause();
287
287
  }
288
288
 
289
+ /**
290
+ * Read the current lock file (exported for tray and other consumers)
291
+ */
292
+ export { readLockFile };
293
+
294
+ /**
295
+ * Get the count of currently running agents
296
+ */
297
+ export async function getRunningAgentCount(): Promise<{ count: number; tasks: Task[] }> {
298
+ const lockFile = await readLockFile();
299
+ const running = lockFile.tasks.filter((t) => t.status === "running");
300
+ return { count: running.length, tasks: running };
301
+ }
302
+
289
303
  /**
290
304
  * Clean stale locks from the lock file
291
305
  */
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ // Mock systray2 before imports
4
+ const mockSysTray = vi.hoisted(() => {
5
+ const instance = {
6
+ ready: vi.fn().mockResolvedValue(undefined),
7
+ onClick: vi.fn(),
8
+ sendAction: vi.fn(),
9
+ kill: vi.fn(),
10
+ };
11
+ const MockClass = vi.fn().mockImplementation(function (this: any, opts: any) {
12
+ Object.assign(this, instance);
13
+ (MockClass as any).__lastOpts = opts;
14
+ return this;
15
+ }) as any;
16
+ MockClass.__lastOpts = null;
17
+ return {
18
+ instance,
19
+ MockClass,
20
+ };
21
+ });
22
+
23
+ vi.mock("systray2", () => ({
24
+ default: mockSysTray.MockClass,
25
+ }));
26
+
27
+ // Mock runningLock
28
+ const mockGetRunningAgentCount = vi.hoisted(() =>
29
+ vi.fn().mockResolvedValue({ count: 0, tasks: [] }),
30
+ );
31
+
32
+ vi.mock("./runningLock.ts", () => ({
33
+ getRunningAgentCount: mockGetRunningAgentCount,
34
+ }));
35
+
36
+ describe("tray", () => {
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ mockGetRunningAgentCount.mockResolvedValue({ count: 0, tasks: [] });
40
+ });
41
+
42
+ describe("startTray", () => {
43
+ it("should create a systray instance on macOS", async () => {
44
+ const originalPlatform = process.platform;
45
+ Object.defineProperty(process, "platform", { value: "darwin" });
46
+
47
+ const { startTray } = await import("./tray.ts");
48
+ await startTray();
49
+
50
+ expect(mockSysTray.MockClass).toHaveBeenCalledWith(
51
+ expect.objectContaining({
52
+ menu: expect.objectContaining({
53
+ title: "AY: 0",
54
+ tooltip: "agent-yes: 0 running",
55
+ }),
56
+ }),
57
+ );
58
+ expect(mockSysTray.instance.ready).toHaveBeenCalled();
59
+ expect(mockSysTray.instance.onClick).toHaveBeenCalled();
60
+
61
+ Object.defineProperty(process, "platform", { value: originalPlatform });
62
+ });
63
+
64
+ it("should show running agent count and task details", async () => {
65
+ const originalPlatform = process.platform;
66
+ Object.defineProperty(process, "platform", { value: "darwin" });
67
+
68
+ mockGetRunningAgentCount.mockResolvedValue({
69
+ count: 2,
70
+ tasks: [
71
+ {
72
+ pid: 1234,
73
+ cwd: "/home/user/project-a",
74
+ task: "fix bugs",
75
+ status: "running",
76
+ startedAt: Date.now(),
77
+ lockedAt: Date.now(),
78
+ },
79
+ {
80
+ pid: 5678,
81
+ cwd: "/home/user/project-b",
82
+ task: "add tests",
83
+ status: "running",
84
+ startedAt: Date.now(),
85
+ lockedAt: Date.now(),
86
+ },
87
+ ],
88
+ });
89
+
90
+ const { startTray } = await import("./tray.ts");
91
+ await startTray();
92
+
93
+ const menuArg = mockSysTray.MockClass.mock.calls[0][0];
94
+ expect(menuArg.menu.title).toBe("AY: 2");
95
+ expect(menuArg.menu.tooltip).toBe("agent-yes: 2 running");
96
+
97
+ // Should have: header, separator, 2 agent items, separator, quit
98
+ const items = menuArg.menu.items;
99
+ expect(items[0].title).toBe("Running agents: 2");
100
+ expect(items[2].title).toContain("[1234]");
101
+ expect(items[2].title).toContain("project-a");
102
+ expect(items[3].title).toContain("[5678]");
103
+ expect(items[3].title).toContain("project-b");
104
+
105
+ // Last item should be "Quit Tray"
106
+ expect(items[items.length - 1].title).toBe("Quit Tray");
107
+
108
+ Object.defineProperty(process, "platform", { value: originalPlatform });
109
+ });
110
+
111
+ it("should handle quit menu click", async () => {
112
+ const originalPlatform = process.platform;
113
+ Object.defineProperty(process, "platform", { value: "darwin" });
114
+
115
+ const mockExit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
116
+
117
+ const { startTray } = await import("./tray.ts");
118
+ await startTray();
119
+
120
+ // Get the onClick callback
121
+ const onClickCb = mockSysTray.instance.onClick.mock.calls[0][0];
122
+
123
+ // Simulate "Quit Tray" click
124
+ onClickCb({ item: { title: "Quit Tray" } });
125
+
126
+ expect(mockSysTray.instance.kill).toHaveBeenCalledWith(false);
127
+ expect(mockExit).toHaveBeenCalledWith(0);
128
+
129
+ mockExit.mockRestore();
130
+ Object.defineProperty(process, "platform", { value: originalPlatform });
131
+ });
132
+
133
+ it("should update tray when agent count changes", async () => {
134
+ const originalPlatform = process.platform;
135
+ Object.defineProperty(process, "platform", { value: "darwin" });
136
+
137
+ vi.useFakeTimers();
138
+
139
+ const { startTray } = await import("./tray.ts");
140
+ await startTray();
141
+
142
+ // Now simulate agent count change on next poll
143
+ mockGetRunningAgentCount.mockResolvedValue({
144
+ count: 3,
145
+ tasks: [
146
+ {
147
+ pid: 111,
148
+ cwd: "/a",
149
+ task: "t1",
150
+ status: "running" as const,
151
+ startedAt: 0,
152
+ lockedAt: 0,
153
+ },
154
+ {
155
+ pid: 222,
156
+ cwd: "/b",
157
+ task: "t2",
158
+ status: "running" as const,
159
+ startedAt: 0,
160
+ lockedAt: 0,
161
+ },
162
+ {
163
+ pid: 333,
164
+ cwd: "/c",
165
+ task: "t3",
166
+ status: "running" as const,
167
+ startedAt: 0,
168
+ lockedAt: 0,
169
+ },
170
+ ],
171
+ });
172
+
173
+ // Advance timer past poll interval
174
+ await vi.advanceTimersByTimeAsync(2100);
175
+
176
+ expect(mockSysTray.instance.sendAction).toHaveBeenCalledWith(
177
+ expect.objectContaining({
178
+ type: "update-menu",
179
+ menu: expect.objectContaining({
180
+ title: "AY: 3",
181
+ }),
182
+ }),
183
+ );
184
+
185
+ vi.useRealTimers();
186
+ Object.defineProperty(process, "platform", { value: originalPlatform });
187
+ });
188
+
189
+ it("should not update tray when agent count stays the same", async () => {
190
+ const originalPlatform = process.platform;
191
+ Object.defineProperty(process, "platform", { value: "darwin" });
192
+
193
+ vi.useFakeTimers();
194
+
195
+ const { startTray } = await import("./tray.ts");
196
+ await startTray();
197
+
198
+ // Same count on next poll
199
+ mockGetRunningAgentCount.mockResolvedValue({ count: 0, tasks: [] });
200
+
201
+ await vi.advanceTimersByTimeAsync(2100);
202
+
203
+ expect(mockSysTray.instance.sendAction).not.toHaveBeenCalled();
204
+
205
+ vi.useRealTimers();
206
+ Object.defineProperty(process, "platform", { value: originalPlatform });
207
+ });
208
+
209
+ it("should work on Windows", async () => {
210
+ const originalPlatform = process.platform;
211
+ Object.defineProperty(process, "platform", { value: "win32" });
212
+
213
+ const { startTray } = await import("./tray.ts");
214
+ await startTray();
215
+
216
+ expect(mockSysTray.MockClass).toHaveBeenCalled();
217
+
218
+ Object.defineProperty(process, "platform", { value: originalPlatform });
219
+ });
220
+
221
+ it("should skip on Linux", async () => {
222
+ const originalPlatform = process.platform;
223
+ Object.defineProperty(process, "platform", { value: "linux" });
224
+
225
+ const { startTray } = await import("./tray.ts");
226
+ await startTray();
227
+
228
+ expect(mockSysTray.MockClass).not.toHaveBeenCalled();
229
+
230
+ Object.defineProperty(process, "platform", { value: originalPlatform });
231
+ });
232
+ });
233
+ });
package/ts/tray.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { getRunningAgentCount, type Task } from "./runningLock.ts";
2
+
3
+ const POLL_INTERVAL = 2000;
4
+
5
+ // Minimal 16x16 white circle PNG as base64 (used as tray icon)
6
+ const ICON_BASE64 =
7
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA" +
8
+ "jklEQVQ4T2NkoBAwUqifgWoGMDIyNjAyMv5nYGBYQMgVjMgC" +
9
+ "QM0LGBkZHYDYAY8BDUBxByB2wGcAyAUOQOwAxPYMDAyOeCzA" +
10
+ "bwBIMyMjowNQsz0ely8ACjng8wJeA0CaGRgY7IHYAZ8hQHEH" +
11
+ "fF7AawBYMwODPZABRHsBpwEgzUDN9kDsgM8lQHEHfC4gJhwA" +
12
+ "AM3hMBGq3cNNAAAAAElFTkSuQmCC";
13
+
14
+ function buildMenuItems(tasks: Task[]) {
15
+ const items = [];
16
+
17
+ if (tasks.length === 0) {
18
+ items.push({ title: "No running agents", tooltip: "", enabled: false });
19
+ } else {
20
+ items.push({
21
+ title: `Running agents: ${tasks.length}`,
22
+ tooltip: "",
23
+ enabled: false,
24
+ });
25
+ items.push({ title: "---", tooltip: "", enabled: false });
26
+ for (const task of tasks) {
27
+ const dir = task.cwd.replace(/^.*[/\\]/, "");
28
+ const desc = task.task ? ` - ${task.task.slice(0, 40)}` : "";
29
+ items.push({
30
+ title: `[${task.pid}] ${dir}${desc}`,
31
+ tooltip: task.cwd,
32
+ enabled: false,
33
+ });
34
+ }
35
+ }
36
+
37
+ items.push({ title: "---", tooltip: "", enabled: false });
38
+ items.push({ title: "Quit Tray", tooltip: "Exit tray icon", enabled: true });
39
+
40
+ return items;
41
+ }
42
+
43
+ export async function startTray(): Promise<void> {
44
+ // Only macOS and Windows have proper tray support
45
+ if (process.platform !== "darwin" && process.platform !== "win32") {
46
+ console.error("Tray icon is only supported on macOS and Windows.");
47
+ return;
48
+ }
49
+
50
+ let SysTray: typeof import("systray2").default;
51
+ try {
52
+ SysTray = (await import("systray2")).default;
53
+ } catch {
54
+ console.error("systray2 is not installed. Install it with: npm install systray2");
55
+ return;
56
+ }
57
+
58
+ const { count, tasks } = await getRunningAgentCount();
59
+
60
+ const systray = new SysTray({
61
+ menu: {
62
+ icon: ICON_BASE64,
63
+ title: `AY: ${count}`,
64
+ tooltip: `agent-yes: ${count} running`,
65
+ items: buildMenuItems(tasks),
66
+ },
67
+ debug: false,
68
+ copyDir: false,
69
+ });
70
+
71
+ await systray.ready();
72
+ console.log(`Tray started. Watching ${count} running agent(s).`);
73
+
74
+ // Handle quit
75
+ systray.onClick((action) => {
76
+ if (action.item.title === "Quit Tray") {
77
+ systray.kill(false);
78
+ process.exit(0);
79
+ }
80
+ });
81
+
82
+ // Poll and update
83
+ let lastCount = count;
84
+ const interval = setInterval(async () => {
85
+ try {
86
+ const { count: newCount, tasks: newTasks } = await getRunningAgentCount();
87
+
88
+ if (newCount !== lastCount) {
89
+ lastCount = newCount;
90
+
91
+ // Update title and tooltip
92
+ systray.sendAction({
93
+ type: "update-menu",
94
+ menu: {
95
+ icon: ICON_BASE64,
96
+ title: `AY: ${newCount}`,
97
+ tooltip: `agent-yes: ${newCount} running`,
98
+ items: buildMenuItems(newTasks),
99
+ },
100
+ });
101
+ }
102
+ } catch {
103
+ // Ignore polling errors
104
+ }
105
+ }, POLL_INTERVAL);
106
+
107
+ // Cleanup on exit
108
+ process.on("SIGINT", () => {
109
+ clearInterval(interval);
110
+ systray.kill(false);
111
+ process.exit(0);
112
+ });
113
+ process.on("SIGTERM", () => {
114
+ clearInterval(interval);
115
+ systray.kill(false);
116
+ process.exit(0);
117
+ });
118
+ }