claude-yes 1.71.0 → 1.72.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.
@@ -815,7 +815,7 @@ function tryCatch(catchFn, fn) {
815
815
  //#endregion
816
816
  //#region package.json
817
817
  var name = "agent-yes";
818
- var version = "1.71.0";
818
+ var version = "1.72.0";
819
819
 
820
820
  //#endregion
821
821
  //#region ts/pty-fix.ts
@@ -1895,4 +1895,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
1895
1895
 
1896
1896
  //#endregion
1897
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 };
1898
- //# sourceMappingURL=SUPPORTED_CLIS-DtYo1wxO.js.map
1898
+ //# sourceMappingURL=SUPPORTED_CLIS-jR_I2op4.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-DtYo1wxO.js";
2
+ import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-jR_I2op4.js";
3
3
  import { t as logger } from "./logger-CX77vJDA.js";
4
4
  import { argv } from "process";
5
5
  import { spawn } from "child_process";
@@ -481,7 +481,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
481
481
  const updateCheckPromise = checkAndAutoUpdate();
482
482
  const config = parseCliArgs(process.argv);
483
483
  if (config.tray) {
484
- const { startTray } = await import("./tray-BzSS0v-i.js");
484
+ const { startTray } = await import("./tray-Dyiihcrq.js");
485
485
  await startTray();
486
486
  await new Promise(() => {});
487
487
  }
@@ -572,6 +572,10 @@ if (config.verbose) {
572
572
  console.log(config);
573
573
  console.log(argv);
574
574
  }
575
+ {
576
+ const { ensureTray } = await import("./tray-Dyiihcrq.js");
577
+ ensureTray();
578
+ }
575
579
  const { default: cliYes } = await import("./index.js");
576
580
  const { exitCode } = await cliYes({
577
581
  ...config,
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-DtYo1wxO.js";
1
+ import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-jR_I2op4.js";
2
2
  import "./logger-CX77vJDA.js";
3
3
 
4
4
  export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
@@ -1,7 +1,13 @@
1
1
  import { n as getRunningAgentCount } from "./runningLock-BBI_URhR.js";
2
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3
+ import path from "path";
4
+ import { homedir } from "os";
5
+ import { existsSync } from "fs";
2
6
 
3
7
  //#region ts/tray.ts
4
8
  const POLL_INTERVAL = 2e3;
9
+ const getTrayDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
10
+ const getTrayPidFile = () => path.join(getTrayDir(), "tray.pid");
5
11
  const ICON_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAjklEQVQ4T2NkoBAwUqifgWoGMDIyNjAyMv5nYGBYQMgVjMgCQM0LGBkZHYDYAY8BDUBxByB2wGcAyAUOQOwAxPYMDAyOeCzAbwBIMyMjowNQsz0ely8ACjng8wJeA0CaGRgY7IHYAZ8hQHEHfF7AawBYMwODPZABRHsBpwEgzUDN9kDsgM8lQHEHfC4gJhwAAM3hMBGq3cNNAAAAAElFTkSuQmCC";
6
12
  function buildMenuItems(tasks) {
7
13
  const items = [];
@@ -43,15 +49,83 @@ function buildMenuItems(tasks) {
43
49
  });
44
50
  return items;
45
51
  }
52
+ function isDesktopOS() {
53
+ return process.platform === "darwin" || process.platform === "win32";
54
+ }
55
+ function isTrayProcessRunning(pid) {
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ /**
64
+ * Write the current process PID to the tray PID file
65
+ */
66
+ async function writeTrayPid() {
67
+ await mkdir(getTrayDir(), { recursive: true });
68
+ await writeFile(getTrayPidFile(), String(process.pid), "utf8");
69
+ }
70
+ /**
71
+ * Remove the tray PID file
72
+ */
73
+ async function removeTrayPid() {
74
+ try {
75
+ await unlink(getTrayPidFile());
76
+ } catch {}
77
+ }
78
+ /**
79
+ * Check if a tray process is already running
80
+ */
81
+ async function isTrayRunning() {
82
+ try {
83
+ const pidFile = getTrayPidFile();
84
+ if (!existsSync(pidFile)) return false;
85
+ const pid = parseInt(await readFile(pidFile, "utf8"), 10);
86
+ if (isNaN(pid)) return false;
87
+ return isTrayProcessRunning(pid);
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+ /**
93
+ * Auto-spawn a tray process in the background if not already running.
94
+ * Only spawns on desktop OS (macOS/Windows).
95
+ * Silently does nothing if systray2 is not installed or on non-desktop OS.
96
+ */
97
+ async function ensureTray() {
98
+ if (!isDesktopOS()) return;
99
+ if (await isTrayRunning()) return;
100
+ try {
101
+ const cliPath = new URL("./cli.ts", import.meta.url).pathname;
102
+ const { spawn } = await import("child_process");
103
+ spawn(process.execPath, [
104
+ cliPath,
105
+ "--tray",
106
+ "--no-rust"
107
+ ], {
108
+ detached: true,
109
+ stdio: "ignore",
110
+ env: { ...process.env }
111
+ }).unref();
112
+ } catch {}
113
+ }
46
114
  async function startTray() {
47
- if (process.platform !== "darwin" && process.platform !== "win32") {
115
+ if (!isDesktopOS()) {
48
116
  console.error("Tray icon is only supported on macOS and Windows.");
49
117
  return;
50
118
  }
119
+ if (await isTrayRunning()) {
120
+ console.error("Tray is already running.");
121
+ return;
122
+ }
123
+ await writeTrayPid();
51
124
  let SysTray;
52
125
  try {
53
126
  SysTray = (await import("systray2")).default;
54
127
  } catch {
128
+ await removeTrayPid();
55
129
  console.error("systray2 is not installed. Install it with: npm install systray2");
56
130
  return;
57
131
  }
@@ -71,7 +145,7 @@ async function startTray() {
71
145
  systray.onClick((action) => {
72
146
  if (action.item.title === "Quit Tray") {
73
147
  systray.kill(false);
74
- process.exit(0);
148
+ removeTrayPid().finally(() => process.exit(0));
75
149
  }
76
150
  });
77
151
  let lastCount = count;
@@ -92,18 +166,15 @@ async function startTray() {
92
166
  }
93
167
  } catch {}
94
168
  }, POLL_INTERVAL);
95
- process.on("SIGINT", () => {
169
+ const cleanup = () => {
96
170
  clearInterval(interval);
97
171
  systray.kill(false);
98
- process.exit(0);
99
- });
100
- process.on("SIGTERM", () => {
101
- clearInterval(interval);
102
- systray.kill(false);
103
- process.exit(0);
104
- });
172
+ removeTrayPid().finally(() => process.exit(0));
173
+ };
174
+ process.on("SIGINT", cleanup);
175
+ process.on("SIGTERM", cleanup);
105
176
  }
106
177
 
107
178
  //#endregion
108
- export { startTray };
109
- //# sourceMappingURL=tray-BzSS0v-i.js.map
179
+ export { ensureTray, startTray };
180
+ //# sourceMappingURL=tray-Dyiihcrq.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.71.0",
3
+ "version": "1.72.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",
package/ts/cli.ts CHANGED
@@ -142,6 +142,12 @@ if (config.verbose) {
142
142
  console.log(argv);
143
143
  }
144
144
 
145
+ // Auto-spawn tray icon in background on desktop OS (best-effort, silent failure)
146
+ {
147
+ const { ensureTray } = await import("./tray.ts");
148
+ ensureTray(); // fire-and-forget, don't await
149
+ }
150
+
145
151
  const { default: cliYes } = await import("./index.ts");
146
152
  const { exitCode } = await cliYes({ ...config, autoYes: config.autoYes });
147
153
 
package/ts/tray.spec.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
 
3
3
  // Mock systray2 before imports
4
4
  const mockSysTray = vi.hoisted(() => {
@@ -8,12 +8,10 @@ const mockSysTray = vi.hoisted(() => {
8
8
  sendAction: vi.fn(),
9
9
  kill: vi.fn(),
10
10
  };
11
- const MockClass = vi.fn().mockImplementation(function (this: any, opts: any) {
11
+ const MockClass = vi.fn().mockImplementation(function (this: any) {
12
12
  Object.assign(this, instance);
13
- (MockClass as any).__lastOpts = opts;
14
13
  return this;
15
14
  }) as any;
16
- MockClass.__lastOpts = null;
17
15
  return {
18
16
  instance,
19
17
  MockClass,
@@ -33,10 +31,33 @@ vi.mock("./runningLock.ts", () => ({
33
31
  getRunningAgentCount: mockGetRunningAgentCount,
34
32
  }));
35
33
 
34
+ // Mock fs for PID file operations
35
+ const mockFs = vi.hoisted(() => ({
36
+ existsSync: vi.fn().mockReturnValue(false),
37
+ }));
38
+ const mockFsPromises = vi.hoisted(() => ({
39
+ mkdir: vi.fn().mockResolvedValue(undefined),
40
+ readFile: vi.fn().mockResolvedValue(""),
41
+ writeFile: vi.fn().mockResolvedValue(undefined),
42
+ unlink: vi.fn().mockResolvedValue(undefined),
43
+ }));
44
+
45
+ vi.mock("fs", () => ({ existsSync: mockFs.existsSync }));
46
+ vi.mock("fs/promises", () => mockFsPromises);
47
+
48
+ // Mock child_process for ensureTray
49
+ const mockSpawn = vi.hoisted(() => {
50
+ const child = { unref: vi.fn() };
51
+ return { spawn: vi.fn().mockReturnValue(child), child };
52
+ });
53
+
54
+ vi.mock("child_process", () => ({ spawn: mockSpawn.spawn }));
55
+
36
56
  describe("tray", () => {
37
57
  beforeEach(() => {
38
58
  vi.clearAllMocks();
39
59
  mockGetRunningAgentCount.mockResolvedValue({ count: 0, tasks: [] });
60
+ mockFs.existsSync.mockReturnValue(false);
40
61
  });
41
62
 
42
63
  describe("startTray", () => {
@@ -57,6 +78,8 @@ describe("tray", () => {
57
78
  );
58
79
  expect(mockSysTray.instance.ready).toHaveBeenCalled();
59
80
  expect(mockSysTray.instance.onClick).toHaveBeenCalled();
81
+ // Should write PID file
82
+ expect(mockFsPromises.writeFile).toHaveBeenCalled();
60
83
 
61
84
  Object.defineProperty(process, "platform", { value: originalPlatform });
62
85
  });
@@ -94,15 +117,12 @@ describe("tray", () => {
94
117
  expect(menuArg.menu.title).toBe("AY: 2");
95
118
  expect(menuArg.menu.tooltip).toBe("agent-yes: 2 running");
96
119
 
97
- // Should have: header, separator, 2 agent items, separator, quit
98
120
  const items = menuArg.menu.items;
99
121
  expect(items[0].title).toBe("Running agents: 2");
100
122
  expect(items[2].title).toContain("[1234]");
101
123
  expect(items[2].title).toContain("project-a");
102
124
  expect(items[3].title).toContain("[5678]");
103
125
  expect(items[3].title).toContain("project-b");
104
-
105
- // Last item should be "Quit Tray"
106
126
  expect(items[items.length - 1].title).toBe("Quit Tray");
107
127
 
108
128
  Object.defineProperty(process, "platform", { value: originalPlatform });
@@ -111,20 +131,17 @@ describe("tray", () => {
111
131
  it("should handle quit menu click", async () => {
112
132
  const originalPlatform = process.platform;
113
133
  Object.defineProperty(process, "platform", { value: "darwin" });
114
-
115
134
  const mockExit = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
116
135
 
117
136
  const { startTray } = await import("./tray.ts");
118
137
  await startTray();
119
138
 
120
- // Get the onClick callback
121
139
  const onClickCb = mockSysTray.instance.onClick.mock.calls[0][0];
122
-
123
- // Simulate "Quit Tray" click
124
140
  onClickCb({ item: { title: "Quit Tray" } });
125
141
 
126
142
  expect(mockSysTray.instance.kill).toHaveBeenCalledWith(false);
127
- expect(mockExit).toHaveBeenCalledWith(0);
143
+ // removeTrayPid is called (unlink)
144
+ await vi.waitFor(() => expect(mockExit).toHaveBeenCalledWith(0));
128
145
 
129
146
  mockExit.mockRestore();
130
147
  Object.defineProperty(process, "platform", { value: originalPlatform });
@@ -133,13 +150,11 @@ describe("tray", () => {
133
150
  it("should update tray when agent count changes", async () => {
134
151
  const originalPlatform = process.platform;
135
152
  Object.defineProperty(process, "platform", { value: "darwin" });
136
-
137
153
  vi.useFakeTimers();
138
154
 
139
155
  const { startTray } = await import("./tray.ts");
140
156
  await startTray();
141
157
 
142
- // Now simulate agent count change on next poll
143
158
  mockGetRunningAgentCount.mockResolvedValue({
144
159
  count: 3,
145
160
  tasks: [
@@ -170,15 +185,12 @@ describe("tray", () => {
170
185
  ],
171
186
  });
172
187
 
173
- // Advance timer past poll interval
174
188
  await vi.advanceTimersByTimeAsync(2100);
175
189
 
176
190
  expect(mockSysTray.instance.sendAction).toHaveBeenCalledWith(
177
191
  expect.objectContaining({
178
192
  type: "update-menu",
179
- menu: expect.objectContaining({
180
- title: "AY: 3",
181
- }),
193
+ menu: expect.objectContaining({ title: "AY: 3" }),
182
194
  }),
183
195
  );
184
196
 
@@ -189,15 +201,12 @@ describe("tray", () => {
189
201
  it("should not update tray when agent count stays the same", async () => {
190
202
  const originalPlatform = process.platform;
191
203
  Object.defineProperty(process, "platform", { value: "darwin" });
192
-
193
204
  vi.useFakeTimers();
194
205
 
195
206
  const { startTray } = await import("./tray.ts");
196
207
  await startTray();
197
208
 
198
- // Same count on next poll
199
209
  mockGetRunningAgentCount.mockResolvedValue({ count: 0, tasks: [] });
200
-
201
210
  await vi.advanceTimersByTimeAsync(2100);
202
211
 
203
212
  expect(mockSysTray.instance.sendAction).not.toHaveBeenCalled();
@@ -229,5 +238,103 @@ describe("tray", () => {
229
238
 
230
239
  Object.defineProperty(process, "platform", { value: originalPlatform });
231
240
  });
241
+
242
+ it("should skip if tray already running", async () => {
243
+ const originalPlatform = process.platform;
244
+ Object.defineProperty(process, "platform", { value: "darwin" });
245
+
246
+ // Simulate existing tray PID file with a live process (our own PID)
247
+ mockFs.existsSync.mockReturnValue(true);
248
+ mockFsPromises.readFile.mockResolvedValue(String(process.pid));
249
+
250
+ const { startTray } = await import("./tray.ts");
251
+ await startTray();
252
+
253
+ // Should NOT create systray because one is already running
254
+ expect(mockSysTray.MockClass).not.toHaveBeenCalled();
255
+
256
+ Object.defineProperty(process, "platform", { value: originalPlatform });
257
+ });
258
+ });
259
+
260
+ describe("isTrayRunning", () => {
261
+ it("should return false when no PID file exists", async () => {
262
+ mockFs.existsSync.mockReturnValue(false);
263
+
264
+ const { isTrayRunning } = await import("./tray.ts");
265
+ expect(await isTrayRunning()).toBe(false);
266
+ });
267
+
268
+ it("should return false when PID file has invalid content", async () => {
269
+ mockFs.existsSync.mockReturnValue(true);
270
+ mockFsPromises.readFile.mockResolvedValue("not-a-number");
271
+
272
+ const { isTrayRunning } = await import("./tray.ts");
273
+ expect(await isTrayRunning()).toBe(false);
274
+ });
275
+
276
+ it("should return true when PID file points to a running process", async () => {
277
+ mockFs.existsSync.mockReturnValue(true);
278
+ mockFsPromises.readFile.mockResolvedValue(String(process.pid));
279
+
280
+ const { isTrayRunning } = await import("./tray.ts");
281
+ expect(await isTrayRunning()).toBe(true);
282
+ });
283
+
284
+ it("should return false when PID file points to a dead process", async () => {
285
+ mockFs.existsSync.mockReturnValue(true);
286
+ mockFsPromises.readFile.mockResolvedValue("999999999");
287
+
288
+ const { isTrayRunning } = await import("./tray.ts");
289
+ expect(await isTrayRunning()).toBe(false);
290
+ });
291
+ });
292
+
293
+ describe("ensureTray", () => {
294
+ it("should spawn tray on macOS when not running", async () => {
295
+ const originalPlatform = process.platform;
296
+ Object.defineProperty(process, "platform", { value: "darwin" });
297
+ mockFs.existsSync.mockReturnValue(false);
298
+
299
+ const { ensureTray } = await import("./tray.ts");
300
+ await ensureTray();
301
+
302
+ expect(mockSpawn.spawn).toHaveBeenCalledWith(
303
+ process.execPath,
304
+ expect.arrayContaining(["--tray", "--no-rust"]),
305
+ expect.objectContaining({ detached: true, stdio: "ignore" }),
306
+ );
307
+ expect(mockSpawn.child.unref).toHaveBeenCalled();
308
+
309
+ Object.defineProperty(process, "platform", { value: originalPlatform });
310
+ });
311
+
312
+ it("should not spawn on Linux", async () => {
313
+ const originalPlatform = process.platform;
314
+ Object.defineProperty(process, "platform", { value: "linux" });
315
+
316
+ const { ensureTray } = await import("./tray.ts");
317
+ await ensureTray();
318
+
319
+ expect(mockSpawn.spawn).not.toHaveBeenCalled();
320
+
321
+ Object.defineProperty(process, "platform", { value: originalPlatform });
322
+ });
323
+
324
+ it("should not spawn if tray already running", async () => {
325
+ const originalPlatform = process.platform;
326
+ Object.defineProperty(process, "platform", { value: "darwin" });
327
+
328
+ // Simulate existing tray
329
+ mockFs.existsSync.mockReturnValue(true);
330
+ mockFsPromises.readFile.mockResolvedValue(String(process.pid));
331
+
332
+ const { ensureTray } = await import("./tray.ts");
333
+ await ensureTray();
334
+
335
+ expect(mockSpawn.spawn).not.toHaveBeenCalled();
336
+
337
+ Object.defineProperty(process, "platform", { value: originalPlatform });
338
+ });
232
339
  });
233
340
  });
package/ts/tray.ts CHANGED
@@ -1,7 +1,14 @@
1
+ import { existsSync } from "fs";
2
+ import { mkdir, readFile, writeFile, unlink } from "fs/promises";
3
+ import { homedir } from "os";
4
+ import path from "path";
1
5
  import { getRunningAgentCount, type Task } from "./runningLock.ts";
2
6
 
3
7
  const POLL_INTERVAL = 2000;
4
8
 
9
+ const getTrayDir = () => path.join(process.env.CLAUDE_YES_HOME || homedir(), ".claude-yes");
10
+ const getTrayPidFile = () => path.join(getTrayDir(), "tray.pid");
11
+
5
12
  // Minimal 16x16 white circle PNG as base64 (used as tray icon)
6
13
  const ICON_BASE64 =
7
14
  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA" +
@@ -40,17 +47,100 @@ function buildMenuItems(tasks: Task[]) {
40
47
  return items;
41
48
  }
42
49
 
50
+ function isDesktopOS(): boolean {
51
+ return process.platform === "darwin" || process.platform === "win32";
52
+ }
53
+
54
+ function isTrayProcessRunning(pid: number): boolean {
55
+ try {
56
+ process.kill(pid, 0);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Write the current process PID to the tray PID file
65
+ */
66
+ async function writeTrayPid(): Promise<void> {
67
+ const dir = getTrayDir();
68
+ await mkdir(dir, { recursive: true });
69
+ await writeFile(getTrayPidFile(), String(process.pid), "utf8");
70
+ }
71
+
72
+ /**
73
+ * Remove the tray PID file
74
+ */
75
+ async function removeTrayPid(): Promise<void> {
76
+ try {
77
+ await unlink(getTrayPidFile());
78
+ } catch {
79
+ // ignore
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Check if a tray process is already running
85
+ */
86
+ export async function isTrayRunning(): Promise<boolean> {
87
+ try {
88
+ const pidFile = getTrayPidFile();
89
+ if (!existsSync(pidFile)) return false;
90
+ const pid = parseInt(await readFile(pidFile, "utf8"), 10);
91
+ if (isNaN(pid)) return false;
92
+ return isTrayProcessRunning(pid);
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Auto-spawn a tray process in the background if not already running.
100
+ * Only spawns on desktop OS (macOS/Windows).
101
+ * Silently does nothing if systray2 is not installed or on non-desktop OS.
102
+ */
103
+ export async function ensureTray(): Promise<void> {
104
+ if (!isDesktopOS()) return;
105
+ if (await isTrayRunning()) return;
106
+
107
+ try {
108
+ // Resolve the CLI entry point (dist/cli.js or ts/cli.ts)
109
+ const cliPath = new URL("./cli.ts", import.meta.url).pathname;
110
+ const { spawn } = await import("child_process");
111
+
112
+ // Spawn detached tray process
113
+ const child = spawn(process.execPath, [cliPath, "--tray", "--no-rust"], {
114
+ detached: true,
115
+ stdio: "ignore",
116
+ env: { ...process.env },
117
+ });
118
+ child.unref();
119
+ } catch {
120
+ // Silently fail — tray is best-effort
121
+ }
122
+ }
123
+
43
124
  export async function startTray(): Promise<void> {
44
- // Only macOS and Windows have proper tray support
45
- if (process.platform !== "darwin" && process.platform !== "win32") {
125
+ if (!isDesktopOS()) {
46
126
  console.error("Tray icon is only supported on macOS and Windows.");
47
127
  return;
48
128
  }
49
129
 
130
+ // Check if another tray is already running
131
+ if (await isTrayRunning()) {
132
+ console.error("Tray is already running.");
133
+ return;
134
+ }
135
+
136
+ // Register our PID
137
+ await writeTrayPid();
138
+
50
139
  let SysTray: typeof import("systray2").default;
51
140
  try {
52
141
  SysTray = (await import("systray2")).default;
53
142
  } catch {
143
+ await removeTrayPid();
54
144
  console.error("systray2 is not installed. Install it with: npm install systray2");
55
145
  return;
56
146
  }
@@ -75,7 +165,7 @@ export async function startTray(): Promise<void> {
75
165
  systray.onClick((action) => {
76
166
  if (action.item.title === "Quit Tray") {
77
167
  systray.kill(false);
78
- process.exit(0);
168
+ removeTrayPid().finally(() => process.exit(0));
79
169
  }
80
170
  });
81
171
 
@@ -105,14 +195,11 @@ export async function startTray(): Promise<void> {
105
195
  }, POLL_INTERVAL);
106
196
 
107
197
  // Cleanup on exit
108
- process.on("SIGINT", () => {
198
+ const cleanup = () => {
109
199
  clearInterval(interval);
110
200
  systray.kill(false);
111
- process.exit(0);
112
- });
113
- process.on("SIGTERM", () => {
114
- clearInterval(interval);
115
- systray.kill(false);
116
- process.exit(0);
117
- });
201
+ removeTrayPid().finally(() => process.exit(0));
202
+ };
203
+ process.on("SIGINT", cleanup);
204
+ process.on("SIGTERM", cleanup);
118
205
  }