@anusornneal/chat-relay 0.7.2 → 0.8.1

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,243 @@
1
+ ObjC.import("AppKit");
2
+ ObjC.import("CoreGraphics");
3
+
4
+ function ok(extra) {
5
+ return Object.assign({ ok: true }, extra || {});
6
+ }
7
+
8
+ function fail(error) {
9
+ return { ok: false, error: String(error || "input_failed") };
10
+ }
11
+
12
+ function screenInfo() {
13
+ $.NSApplication.sharedApplication;
14
+ const screens = $.NSScreen.screens;
15
+ const count = Number(screens.count);
16
+ if (!count) return fail("session_unavailable");
17
+ const primary = screens.objectAtIndex(0);
18
+ const primaryHeight = Number(primary.frame.size.height);
19
+ const output = [];
20
+
21
+ for (let i = 0; i < count; i += 1) {
22
+ const screen = screens.objectAtIndex(i);
23
+ const frame = screen.frame;
24
+ const x = Number(frame.origin.x);
25
+ const nsY = Number(frame.origin.y);
26
+ const width = Number(frame.size.width);
27
+ const height = Number(frame.size.height);
28
+ output.push({
29
+ index: i,
30
+ x,
31
+ y: primaryHeight - (nsY + height),
32
+ width,
33
+ height,
34
+ scaleFactor: Number(screen.backingScaleFactor || 1),
35
+ isPrimary: i === 0,
36
+ deviceName: "Display " + (i + 1),
37
+ });
38
+ }
39
+
40
+ const minX = Math.min.apply(null, output.map((item) => item.x));
41
+ const minY = Math.min.apply(null, output.map((item) => item.y));
42
+ const maxX = Math.max.apply(null, output.map((item) => item.x + item.width));
43
+ const maxY = Math.max.apply(null, output.map((item) => item.y + item.height));
44
+
45
+ return ok({
46
+ screens: output,
47
+ virtualDesktopOriginX: minX,
48
+ virtualDesktopOriginY: minY,
49
+ virtualDesktopWidth: maxX - minX,
50
+ virtualDesktopHeight: maxY - minY,
51
+ });
52
+ }
53
+
54
+ function modifierList(input) {
55
+ const result = [];
56
+ if (input.ctrl) result.push("control down");
57
+ if (input.alt) result.push("option down");
58
+ if (input.shift) result.push("shift down");
59
+ if (input.win) result.push("command down");
60
+ return result;
61
+ }
62
+
63
+ function keyboardInput(input) {
64
+ const system = Application("System Events");
65
+ const modifiers = modifierList(input);
66
+ const options = modifiers.length === 0
67
+ ? {}
68
+ : { using: modifiers.length === 1 ? modifiers[0] : modifiers };
69
+
70
+ if (typeof input.text === "string") {
71
+ system.keystroke(input.text);
72
+ return ok();
73
+ }
74
+
75
+ const key = String(input.key || "").toLowerCase();
76
+ const special = {
77
+ enter: 36,
78
+ tab: 48,
79
+ escape: 53,
80
+ backspace: 51,
81
+ delete: 117,
82
+ arrowleft: 123,
83
+ arrowup: 126,
84
+ arrowright: 124,
85
+ arrowdown: 125,
86
+ home: 115,
87
+ end: 119,
88
+ pageup: 116,
89
+ pagedown: 121,
90
+ space: 49,
91
+ f1: 122,
92
+ f2: 120,
93
+ f3: 99,
94
+ f4: 118,
95
+ f5: 96,
96
+ f6: 97,
97
+ f7: 98,
98
+ f8: 100,
99
+ f9: 101,
100
+ f10: 109,
101
+ f11: 103,
102
+ f12: 111,
103
+ };
104
+
105
+ if (Object.prototype.hasOwnProperty.call(special, key)) {
106
+ system.keyCode(special[key], options);
107
+ return ok();
108
+ }
109
+
110
+ if (/^[a-z0-9]$/.test(key)) {
111
+ system.keystroke(key, options);
112
+ return ok();
113
+ }
114
+
115
+ return fail("unsupported_key");
116
+ }
117
+
118
+ function mouseClick(input) {
119
+ const info = screenInfo();
120
+ if (!info.ok) return info;
121
+ const x = Number(input.x);
122
+ const y = Number(input.y);
123
+ const contains = info.screens.some((screen) =>
124
+ x >= screen.x && x < screen.x + screen.width &&
125
+ y >= screen.y && y < screen.y + screen.height
126
+ );
127
+ if (!contains) return fail("invalid_coordinates");
128
+
129
+ const button = String(input.button || "left");
130
+ let mouseButton = $.kCGMouseButtonLeft;
131
+ let downType = $.kCGEventLeftMouseDown;
132
+ let upType = $.kCGEventLeftMouseUp;
133
+ if (button === "right") {
134
+ mouseButton = $.kCGMouseButtonRight;
135
+ downType = $.kCGEventRightMouseDown;
136
+ upType = $.kCGEventRightMouseUp;
137
+ } else if (button === "middle") {
138
+ mouseButton = $.kCGMouseButtonCenter;
139
+ downType = $.kCGEventOtherMouseDown;
140
+ upType = $.kCGEventOtherMouseUp;
141
+ } else if (button !== "left") {
142
+ return fail("invalid_button");
143
+ }
144
+
145
+ const point = $.CGPointMake(x, y);
146
+ const count = Number(input.clicks || 1);
147
+ for (let i = 0; i < count; i += 1) {
148
+ const down = $.CGEventCreateMouseEvent($(), downType, point, mouseButton);
149
+ const up = $.CGEventCreateMouseEvent($(), upType, point, mouseButton);
150
+ if (!down || !up) return fail("input_failed");
151
+ if (count > 1) {
152
+ const clickState = i + 1;
153
+ $.CGEventSetIntegerValueField(down, $.kCGMouseEventClickState, clickState);
154
+ $.CGEventSetIntegerValueField(up, $.kCGMouseEventClickState, clickState);
155
+ }
156
+ $.CGEventPost($.kCGHIDEventTap, down);
157
+ $.CGEventPost($.kCGHIDEventTap, up);
158
+ $.CFRelease(down);
159
+ $.CFRelease(up);
160
+ if (i + 1 < count) $.NSThread.sleepForTimeInterval(0.08);
161
+ }
162
+ return ok();
163
+ }
164
+
165
+ function windowList(input) {
166
+ const limit = Math.max(1, Math.min(100, Number(input.limit || 50)));
167
+ const system = Application("System Events");
168
+ const processes = system.applicationProcesses();
169
+ const windows = [];
170
+
171
+ for (let p = 0; p < processes.length && windows.length < limit; p += 1) {
172
+ const processRef = processes[p];
173
+ let pid;
174
+ let items;
175
+ try {
176
+ pid = Number(processRef.unixId());
177
+ items = processRef.windows();
178
+ } catch (_) {
179
+ continue;
180
+ }
181
+
182
+ for (let i = 0; i < items.length && windows.length < limit; i += 1) {
183
+ const windowRef = items[i];
184
+ try {
185
+ const position = windowRef.position();
186
+ const size = windowRef.size();
187
+ const title = String(windowRef.name() || "");
188
+ const width = Number(size[0]);
189
+ const height = Number(size[1]);
190
+ if (!(width > 0 && height > 0)) continue;
191
+ windows.push({
192
+ windowId: String(pid * 10000 + i + 1),
193
+ processId: pid,
194
+ title,
195
+ x: Number(position[0]),
196
+ y: Number(position[1]),
197
+ width,
198
+ height,
199
+ isForeground: processRef.frontmost() === true,
200
+ });
201
+ } catch (_) {}
202
+ }
203
+ }
204
+
205
+ return ok({ windows, count: windows.length });
206
+ }
207
+
208
+ function windowFocus(input) {
209
+ const encoded = Number(input.windowId);
210
+ if (!Number.isFinite(encoded) || encoded <= 0) return fail("invalid_window_id");
211
+ const pid = Math.floor(encoded / 10000);
212
+ const index = (encoded % 10000) - 1;
213
+ if (!(pid > 0 && index >= 0)) return fail("invalid_window_id");
214
+
215
+ const system = Application("System Events");
216
+ const matches = system.applicationProcesses.whose({ unixId: pid });
217
+ if (!matches.length) return fail("window_not_found");
218
+ const processRef = matches[0];
219
+ const items = processRef.windows();
220
+ if (index >= items.length) return fail("window_not_found");
221
+
222
+ processRef.frontmost = true;
223
+ try {
224
+ const action = items[index].actions.byName("AXRaise");
225
+ action.perform();
226
+ } catch (_) {}
227
+ return ok();
228
+ }
229
+
230
+ function run(argv) {
231
+ try {
232
+ const operation = String(argv[0] || "");
233
+ const input = argv[1] ? JSON.parse(argv[1]) : {};
234
+ if (operation === "screen_info") return JSON.stringify(screenInfo());
235
+ if (operation === "keyboard_input") return JSON.stringify(keyboardInput(input));
236
+ if (operation === "mouse_click") return JSON.stringify(mouseClick(input));
237
+ if (operation === "window_list") return JSON.stringify(windowList(input));
238
+ if (operation === "window_focus") return JSON.stringify(windowFocus(input));
239
+ return JSON.stringify(fail("unsupported_operation"));
240
+ } catch (error) {
241
+ return JSON.stringify(fail(error && error.message ? error.message : "input_failed"));
242
+ }
243
+ }
@@ -0,0 +1,292 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFile, spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ const MAX_SCREENSHOT_BINARY_BYTES = 32 * 1024;
9
+ const DEFAULT_TIMEOUT_MS = 15_000;
10
+ const helperPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "macos-desktop-helper.js");
11
+
12
+ function execNative(file, args, options = {}) {
13
+ return new Promise((resolve, reject) => {
14
+ execFile(file, args, {
15
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
16
+ maxBuffer: options.maxBuffer ?? 512 * 1024,
17
+ encoding: "utf8",
18
+ }, (error, stdout, stderr) => {
19
+ if (error) {
20
+ reject(new Error(String(stderr || error.message).trim()));
21
+ return;
22
+ }
23
+ resolve(String(stdout || ""));
24
+ });
25
+ });
26
+ }
27
+
28
+ function writeNative(file, args, input, options = {}) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(file, args, { stdio: ["pipe", "pipe", "pipe"] });
31
+ let stdout = "";
32
+ let stderr = "";
33
+ const timer = setTimeout(() => {
34
+ try { child.kill("SIGKILL"); } catch {}
35
+ reject(new Error("native_command_timeout"));
36
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
37
+ child.stdout.on("data", (chunk) => { stdout += String(chunk); });
38
+ child.stderr.on("data", (chunk) => { stderr += String(chunk); });
39
+ child.on("error", (error) => {
40
+ clearTimeout(timer);
41
+ reject(error);
42
+ });
43
+ child.on("exit", (code) => {
44
+ clearTimeout(timer);
45
+ if (code === 0) resolve(stdout);
46
+ else reject(new Error(stderr.trim() || "native_command_failed"));
47
+ });
48
+ child.stdin.end(input);
49
+ });
50
+ }
51
+
52
+ function parseSipsSize(output) {
53
+ const width = Number(/pixelWidth:\s*(\d+)/.exec(output)?.[1] || 0);
54
+ const height = Number(/pixelHeight:\s*(\d+)/.exec(output)?.[1] || 0);
55
+ return { width, height };
56
+ }
57
+
58
+ export class MacOSDesktopRunner {
59
+ constructor(options = {}) {
60
+ this.platform = options.platform ?? process.platform;
61
+ this.timeoutMs = Number(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
62
+ }
63
+
64
+ async #jxa(operation, args = {}) {
65
+ try {
66
+ const stdout = await execNative("/usr/bin/osascript", [
67
+ "-l", "JavaScript", helperPath, operation, JSON.stringify(args),
68
+ ], { timeoutMs: this.timeoutMs });
69
+ const line = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
70
+ const result = line ? JSON.parse(line) : { ok: false, error: "desktop_helper_error" };
71
+ if (result?.ok === false && /not authorized|assistive|accessibility|(-1743)|(-25211)/i.test(String(result.error || ""))) {
72
+ return { ok: false, error: "accessibility_permission_required" };
73
+ }
74
+ return result;
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : "";
77
+ if (/not authorized|assistive|accessibility|(-1743)/i.test(message)) {
78
+ return { ok: false, error: "accessibility_permission_required" };
79
+ }
80
+ return { ok: false, error: "desktop_helper_error" };
81
+ }
82
+ }
83
+
84
+ async #screenInfo() {
85
+ return this.#jxa("screen_info", {});
86
+ }
87
+
88
+ async #screenshot(args = {}) {
89
+ const screenshotStartedAt = Date.now();
90
+ const info = await this.#screenInfo();
91
+ if (!info?.ok || !Array.isArray(info.screens) || info.screens.length === 0) {
92
+ return info?.ok === false ? info : { ok: false, error: "capture_failed" };
93
+ }
94
+
95
+ const requested = args.monitor ?? "primary";
96
+ let index;
97
+ if (requested === "primary") index = 0;
98
+ else if (requested === "secondary") index = 1;
99
+ else index = Number(requested);
100
+ if (!Number.isInteger(index) || index < 0 || index >= info.screens.length) {
101
+ return { ok: false, error: "monitor_not_found" };
102
+ }
103
+
104
+ const screen = info.screens[index];
105
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "chat-relay-shot-"));
106
+ const source = path.join(dir, "source.jpg");
107
+ const output = path.join(dir, "output.jpg");
108
+ try {
109
+ const captureStartedAt = Date.now();
110
+ await execNative("/usr/sbin/screencapture", ["-x", "-D", String(index + 1), "-t", "jpg", source], {
111
+ timeoutMs: this.timeoutMs,
112
+ }).catch((error) => {
113
+ const message = error instanceof Error ? error.message : "";
114
+ if (/screen recording|permission|not authorized/i.test(message)) throw new Error("screen_recording_permission_required");
115
+ throw error;
116
+ });
117
+ const captureMs = Math.max(0, Date.now() - captureStartedAt);
118
+ const encodeStartedAt = Date.now();
119
+
120
+ const originalInfo = parseSipsSize(await execNative("/usr/bin/sips", [
121
+ "-g", "pixelWidth", "-g", "pixelHeight", source,
122
+ ]));
123
+ if (!(originalInfo.width > 0 && originalInfo.height > 0)) {
124
+ return { ok: false, error: "capture_failed" };
125
+ }
126
+
127
+ let targetWidth = Math.min(Number(args.maxWidth || 960), originalInfo.width);
128
+ const requestedQuality = Math.max(20, Math.min(85, Number(args.quality || 58)));
129
+ const qualities = [...new Set([requestedQuality, 55, 45, 35, 25, 20].filter((q) => q <= requestedQuality))];
130
+ let data = null;
131
+ let finalWidth = 0;
132
+ let finalHeight = 0;
133
+ let finalQuality = requestedQuality;
134
+
135
+ for (let attempt = 0; attempt < 5 && !data; attempt += 1) {
136
+ for (const quality of qualities) {
137
+ await fs.rm(output, { force: true });
138
+ await execNative("/usr/bin/sips", [
139
+ "-Z", String(Math.max(320, Math.round(targetWidth))),
140
+ "-s", "format", "jpeg",
141
+ "-s", "formatOptions", String(quality),
142
+ source,
143
+ "--out", output,
144
+ ]);
145
+ const bytes = await fs.readFile(output);
146
+ if (bytes.byteLength > 0 && bytes.byteLength <= MAX_SCREENSHOT_BINARY_BYTES) {
147
+ const resized = parseSipsSize(await execNative("/usr/bin/sips", [
148
+ "-g", "pixelWidth", "-g", "pixelHeight", output,
149
+ ]));
150
+ data = bytes.toString("base64");
151
+ finalWidth = resized.width;
152
+ finalHeight = resized.height;
153
+ finalQuality = quality;
154
+ break;
155
+ }
156
+ }
157
+ targetWidth = Math.max(320, Math.floor(targetWidth * 0.78));
158
+ }
159
+
160
+ if (!data) return { ok: false, error: "screenshot_too_large" };
161
+ const scaleX = Number(screen.width) / finalWidth;
162
+ const scaleY = Number(screen.height) / finalHeight;
163
+ return {
164
+ ok: true,
165
+ mimeType: "image/jpeg",
166
+ data,
167
+ width: finalWidth,
168
+ height: finalHeight,
169
+ desktopOriginX: Number(screen.x),
170
+ desktopOriginY: Number(screen.y),
171
+ desktopWidth: Number(screen.width),
172
+ desktopHeight: Number(screen.height),
173
+ scaleX,
174
+ scaleY,
175
+ monitorIndex: index,
176
+ isPrimary: index === 0,
177
+ deviceName: String(screen.deviceName || ("Display " + (index + 1))),
178
+ virtualDesktopOriginX: Number(info.virtualDesktopOriginX),
179
+ virtualDesktopOriginY: Number(info.virtualDesktopOriginY),
180
+ virtualDesktopWidth: Number(info.virtualDesktopWidth),
181
+ virtualDesktopHeight: Number(info.virtualDesktopHeight),
182
+ monitorCount: info.screens.length,
183
+ jpegQuality: finalQuality,
184
+ timing: {
185
+ captureMs,
186
+ encodeMs: Math.max(0, Date.now() - encodeStartedAt),
187
+ screenshotTotalMs: Math.max(0, Date.now() - screenshotStartedAt),
188
+ },
189
+ };
190
+ } catch (error) {
191
+ if (error instanceof Error && error.message === "screen_recording_permission_required") {
192
+ return { ok: false, error: "screen_recording_permission_required" };
193
+ }
194
+ return { ok: false, error: "capture_failed" };
195
+ } finally {
196
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
197
+ }
198
+ }
199
+
200
+ async #clipboardRead() {
201
+ try {
202
+ const text = await execNative("/usr/bin/pbpaste", [], { timeoutMs: this.timeoutMs, maxBuffer: 64 * 1024 });
203
+ return { ok: true, text, truncated: false };
204
+ } catch {
205
+ return { ok: false, error: "clipboard_failed" };
206
+ }
207
+ }
208
+
209
+ async #clipboardWrite(args = {}) {
210
+ try {
211
+ await writeNative("/usr/bin/pbcopy", [], String(args.text ?? ""), { timeoutMs: this.timeoutMs });
212
+ return { ok: true };
213
+ } catch {
214
+ return { ok: false, error: "clipboard_failed" };
215
+ }
216
+ }
217
+
218
+ async #step(args = {}) {
219
+ const startedAt = Date.now();
220
+ let inputMs = 0;
221
+ let explicitWaitMs = 0;
222
+ let settleMs = 0;
223
+
224
+ for (const action of args.actions || []) {
225
+ if (action.type === "wait") {
226
+ const waitStartedAt = Date.now();
227
+ await new Promise((resolve) => setTimeout(resolve, action.ms));
228
+ explicitWaitMs += Math.max(0, Date.now() - waitStartedAt);
229
+ continue;
230
+ }
231
+
232
+ const inputStartedAt = Date.now();
233
+ const result = action.type === "click"
234
+ ? await this.#jxa("mouse_click", action)
235
+ : await this.#jxa("keyboard_input", action);
236
+ inputMs += Math.max(0, Date.now() - inputStartedAt);
237
+ if (!result?.ok) {
238
+ return {
239
+ ...(result || { ok: false, error: "input_failed" }),
240
+ timing: { inputMs, explicitWaitMs, settleMs, captureMs: 0, encodeMs: 0, workerMs: Math.max(0, Date.now() - startedAt) },
241
+ };
242
+ }
243
+
244
+ if (Number(action.settleAfterMs) > 0) {
245
+ const settleStartedAt = Date.now();
246
+ await new Promise((resolve) => setTimeout(resolve, Number(action.settleAfterMs)));
247
+ settleMs += Math.max(0, Date.now() - settleStartedAt);
248
+ }
249
+ }
250
+
251
+ if (Number(args.settleMs) > 0) {
252
+ const settleStartedAt = Date.now();
253
+ await new Promise((resolve) => setTimeout(resolve, Number(args.settleMs)));
254
+ settleMs += Math.max(0, Date.now() - settleStartedAt);
255
+ }
256
+
257
+ if (args.captureAfter !== true) {
258
+ return {
259
+ ok: true,
260
+ timing: { inputMs, explicitWaitMs, settleMs, captureMs: 0, encodeMs: 0, workerMs: Math.max(0, Date.now() - startedAt) },
261
+ };
262
+ }
263
+
264
+ const screenshot = await this.#screenshot(args);
265
+ return {
266
+ ...screenshot,
267
+ timing: {
268
+ inputMs,
269
+ explicitWaitMs,
270
+ settleMs,
271
+ captureMs: Number(screenshot?.timing?.captureMs) || 0,
272
+ encodeMs: Number(screenshot?.timing?.encodeMs) || 0,
273
+ workerMs: Math.max(0, Date.now() - startedAt),
274
+ },
275
+ };
276
+ }
277
+
278
+ async run(operation, args = {}) {
279
+ if (this.platform !== "darwin") return { ok: false, error: "unsupported_platform" };
280
+ if (operation === "screenshot") return this.#screenshot(args);
281
+ if (operation === "clipboard_read") return this.#clipboardRead();
282
+ if (operation === "clipboard_write") return this.#clipboardWrite(args);
283
+ if (operation === "window_list") return this.#jxa("window_list", args);
284
+ if (operation === "window_focus") return this.#jxa("window_focus", args);
285
+ if (operation === "mouse_click") return this.#jxa("mouse_click", args);
286
+ if (operation === "keyboard_input") return this.#jxa("keyboard_input", args);
287
+ if (operation === "step") return this.#step(args);
288
+ return { ok: false, error: "unsupported_operation" };
289
+ }
290
+
291
+ close() {}
292
+ }
@@ -1,37 +1,88 @@
1
1
  import { execFile } from "node:child_process";
2
+ import path from "node:path";
2
3
  import os from "node:os";
3
4
 
4
- function execPowerShell(script, timeout = 15000) {
5
+ function execCommand(file, args, options = {}) {
5
6
  return new Promise((resolve, reject) => {
6
7
  execFile(
7
- "powershell.exe",
8
- ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
9
- { timeout, windowsHide: true, maxBuffer: 256 * 1024 },
8
+ file,
9
+ args,
10
+ {
11
+ timeout: options.timeout ?? 15000,
12
+ windowsHide: true,
13
+ maxBuffer: options.maxBuffer ?? 256 * 1024,
14
+ encoding: "utf8",
15
+ },
10
16
  (error, stdout, stderr) => {
11
17
  if (error) {
12
- reject(new Error(stderr?.trim() || error.message));
18
+ reject(new Error(String(stderr || error.message).trim()));
13
19
  return;
14
20
  }
15
- resolve(stdout);
21
+ resolve(String(stdout || ""));
16
22
  },
17
23
  );
18
24
  });
19
25
  }
20
26
 
27
+ function execPowerShell(script, timeout = 15000) {
28
+ return execCommand(
29
+ "powershell.exe",
30
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
31
+ { timeout },
32
+ );
33
+ }
34
+
35
+ function parsePosixProcesses(stdout, filter = "") {
36
+ const query = String(filter || "").toLowerCase();
37
+ const items = [];
38
+ for (const line of stdout.split(/\r?\n/)) {
39
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
40
+ if (!match) continue;
41
+ const processId = Number(match[1]);
42
+ const parentProcessId = Number(match[2]);
43
+ const commandLine = match[3].trim();
44
+ if (!commandLine) continue;
45
+ const executablePath = commandLine.split(/\s+/)[0] || "";
46
+ const name = path.basename(executablePath) || executablePath;
47
+ if (query && !name.toLowerCase().includes(query) && !commandLine.toLowerCase().includes(query)) continue;
48
+ items.push({
49
+ ProcessId: processId,
50
+ ParentProcessId: parentProcessId,
51
+ Name: name,
52
+ CommandLine: commandLine,
53
+ ExecutablePath: executablePath,
54
+ });
55
+ }
56
+ return items;
57
+ }
58
+
21
59
  export class ProcessManager {
60
+ constructor(options = {}) {
61
+ this.platform = options.platform ?? process.platform;
62
+ }
63
+
22
64
  async list(filter = "") {
23
- const escaped = String(filter).replace(/'/g, "''");
24
- const where = escaped
25
- ? ` | Where-Object { $_.Name -like '*${escaped}*' -or $_.CommandLine -like '*${escaped}*' }`
26
- : "";
27
- const script =
28
- "$items = Get-CimInstance Win32_Process" + where +
29
- " | Select-Object ProcessId,Name,CommandLine,ExecutablePath;" +
30
- "$items | ConvertTo-Json -Compress -Depth 3";
31
- const stdout = await execPowerShell(script);
32
- if (!stdout.trim()) return [];
33
- const parsed = JSON.parse(stdout);
34
- return Array.isArray(parsed) ? parsed : [parsed];
65
+ if (this.platform === "win32") {
66
+ const escaped = String(filter).replace(/'/g, "''");
67
+ const where = escaped
68
+ ? ` | Where-Object { $_.Name -like '*${escaped}*' -or $_.CommandLine -like '*${escaped}*' }`
69
+ : "";
70
+ const script =
71
+ "$items = Get-CimInstance Win32_Process" + where +
72
+ " | Select-Object ProcessId,Name,CommandLine,ExecutablePath;" +
73
+ "$items | ConvertTo-Json -Compress -Depth 3";
74
+ const stdout = await execPowerShell(script);
75
+ if (!stdout.trim()) return [];
76
+ const parsed = JSON.parse(stdout);
77
+ return Array.isArray(parsed) ? parsed : [parsed];
78
+ }
79
+
80
+ if (this.platform === "darwin" || this.platform === "linux") {
81
+ const stdout = await execCommand("/bin/ps", ["-axo", "pid=,ppid=,command="]);
82
+ return parsePosixProcesses(stdout, filter);
83
+ }
84
+
85
+ throw new Error("unsupported_platform");
35
86
  }
36
87
 
37
88
  async kill(pid) {
@@ -39,15 +90,46 @@ export class ProcessManager {
39
90
  if (!Number.isInteger(value) || value <= 100 || value === process.pid) {
40
91
  throw new Error("invalid_or_protected_pid");
41
92
  }
42
- return new Promise((resolve, reject) => {
43
- execFile("taskkill.exe", ["/PID", String(value), "/T", "/F"], { windowsHide: true }, (error, stdout, stderr) => {
44
- if (error) {
45
- reject(new Error(stderr?.trim() || error.message));
46
- return;
47
- }
48
- resolve({ ok: true, pid: value, output: stdout.trim() });
93
+
94
+ if (this.platform === "win32") {
95
+ return new Promise((resolve, reject) => {
96
+ execFile("taskkill.exe", ["/PID", String(value), "/T", "/F"], { windowsHide: true }, (error, stdout, stderr) => {
97
+ if (error) {
98
+ reject(new Error(stderr?.trim() || error.message));
99
+ return;
100
+ }
101
+ resolve({ ok: true, pid: value, output: stdout.trim() });
102
+ });
49
103
  });
50
- });
104
+ }
105
+
106
+ if (this.platform === "darwin" || this.platform === "linux") {
107
+ const processes = await this.list();
108
+ const children = new Map();
109
+ for (const item of processes) {
110
+ const parent = Number(item.ParentProcessId);
111
+ const child = Number(item.ProcessId);
112
+ if (!children.has(parent)) children.set(parent, []);
113
+ children.get(parent).push(child);
114
+ }
115
+ const ordered = [];
116
+ const visit = (parent) => {
117
+ for (const child of children.get(parent) || []) {
118
+ visit(child);
119
+ ordered.push(child);
120
+ }
121
+ };
122
+ visit(value);
123
+ ordered.push(value);
124
+ for (const target of ordered) {
125
+ try { process.kill(target, "SIGKILL"); } catch (error) {
126
+ if (target === value && error?.code !== "ESRCH") throw error;
127
+ }
128
+ }
129
+ return { ok: true, pid: value, output: "" };
130
+ }
131
+
132
+ throw new Error("unsupported_platform");
51
133
  }
52
134
 
53
135
  info(extra = {}) {
@@ -67,3 +149,5 @@ export class ProcessManager {
67
149
  };
68
150
  }
69
151
  }
152
+
153
+ export { parsePosixProcesses };