@phuetz/code-buddy 0.1.3 → 0.1.5

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.
Files changed (44) hide show
  1. package/README.md +49 -33
  2. package/dist/agent/tool-handler.js +21 -1
  3. package/dist/agent/tool-handler.js.map +1 -1
  4. package/dist/browser-automation/browser-manager.js +0 -2
  5. package/dist/browser-automation/browser-manager.js.map +1 -1
  6. package/dist/codebuddy/client.js +24 -0
  7. package/dist/codebuddy/client.js.map +1 -1
  8. package/dist/codebuddy/tool-definitions/computer-control-tools.js +2 -0
  9. package/dist/codebuddy/tool-definitions/computer-control-tools.js.map +1 -1
  10. package/dist/desktop-automation/automation-manager.d.ts +8 -0
  11. package/dist/desktop-automation/automation-manager.js +52 -0
  12. package/dist/desktop-automation/automation-manager.js.map +1 -1
  13. package/dist/desktop-automation/base-native-provider.d.ts +72 -0
  14. package/dist/desktop-automation/base-native-provider.js +75 -0
  15. package/dist/desktop-automation/base-native-provider.js.map +1 -0
  16. package/dist/desktop-automation/index.d.ts +4 -0
  17. package/dist/desktop-automation/index.js +5 -0
  18. package/dist/desktop-automation/index.js.map +1 -1
  19. package/dist/desktop-automation/linux-native-provider.d.ts +48 -0
  20. package/dist/desktop-automation/linux-native-provider.js +515 -0
  21. package/dist/desktop-automation/linux-native-provider.js.map +1 -0
  22. package/dist/desktop-automation/macos-native-provider.d.ts +61 -0
  23. package/dist/desktop-automation/macos-native-provider.js +768 -0
  24. package/dist/desktop-automation/macos-native-provider.js.map +1 -0
  25. package/dist/desktop-automation/smart-snapshot.d.ts +21 -2
  26. package/dist/desktop-automation/smart-snapshot.js +237 -12
  27. package/dist/desktop-automation/smart-snapshot.js.map +1 -1
  28. package/dist/desktop-automation/types.d.ts +1 -1
  29. package/dist/desktop-automation/types.js +2 -2
  30. package/dist/desktop-automation/types.js.map +1 -1
  31. package/dist/desktop-automation/windows-native-provider.d.ts +71 -0
  32. package/dist/desktop-automation/windows-native-provider.js +653 -0
  33. package/dist/desktop-automation/windows-native-provider.js.map +1 -0
  34. package/dist/index.js +0 -0
  35. package/dist/tools/computer-control-tool.d.ts +6 -1
  36. package/dist/tools/computer-control-tool.js +97 -1
  37. package/dist/tools/computer-control-tool.js.map +1 -1
  38. package/dist/tools/screenshot-tool.d.ts +23 -0
  39. package/dist/tools/screenshot-tool.js +224 -1
  40. package/dist/tools/screenshot-tool.js.map +1 -1
  41. package/dist/tools/web-search.d.ts +2 -0
  42. package/dist/tools/web-search.js +12 -2
  43. package/dist/tools/web-search.js.map +1 -1
  44. package/package.json +14 -5
@@ -0,0 +1,768 @@
1
+ /**
2
+ * macOS Native Desktop Automation Provider
3
+ *
4
+ * Uses osascript (AppleScript) and cliclick for desktop automation on macOS.
5
+ * cliclick is optional but recommended for reliable mouse control.
6
+ */
7
+ import { spawn, execSync as nodeExecSync } from 'child_process';
8
+ import { BaseNativeProvider } from './base-native-provider.js';
9
+ // osascript key code map for special keys
10
+ const keyCodeMap = {
11
+ return: 36,
12
+ enter: 36,
13
+ escape: 53,
14
+ tab: 48,
15
+ delete: 51,
16
+ backspace: 51,
17
+ forwarddelete: 117,
18
+ space: 49,
19
+ up: 126,
20
+ down: 125,
21
+ left: 123,
22
+ right: 124,
23
+ home: 115,
24
+ end: 119,
25
+ pageup: 116,
26
+ pagedown: 121,
27
+ f1: 122,
28
+ f2: 120,
29
+ f3: 99,
30
+ f4: 118,
31
+ f5: 96,
32
+ f6: 97,
33
+ f7: 98,
34
+ f8: 100,
35
+ f9: 101,
36
+ f10: 109,
37
+ f11: 103,
38
+ f12: 111,
39
+ };
40
+ /**
41
+ * Escape a string for safe use inside single-quoted osascript shell arguments.
42
+ */
43
+ function escapeForOsascript(str) {
44
+ return str.replace(/'/g, "'\\''");
45
+ }
46
+ /**
47
+ * Escape a string for use inside AppleScript double-quoted strings.
48
+ */
49
+ function escapeAppleString(str) {
50
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
51
+ }
52
+ /**
53
+ * Parse a macOS window handle in processName:windowIndex format.
54
+ */
55
+ function parseHandle(handle) {
56
+ const colonIdx = handle.lastIndexOf(':');
57
+ if (colonIdx === -1) {
58
+ return { processName: handle, windowIndex: 1 };
59
+ }
60
+ const processName = handle.substring(0, colonIdx);
61
+ const windowIndex = parseInt(handle.substring(colonIdx + 1), 10) || 1;
62
+ return { processName, windowIndex };
63
+ }
64
+ export class MacOSNativeProvider extends BaseNativeProvider {
65
+ platformName = 'macOS';
66
+ capabilities = {
67
+ mouse: true,
68
+ keyboard: true,
69
+ windows: true,
70
+ apps: true,
71
+ screenshots: true,
72
+ colorPicker: false,
73
+ clipboard: true,
74
+ ocr: false,
75
+ };
76
+ hasCliclick = false;
77
+ // --------------------------------------------------------------------------
78
+ // Private Helpers
79
+ // --------------------------------------------------------------------------
80
+ /**
81
+ * Execute an osascript command.
82
+ */
83
+ async osascript(script) {
84
+ const escaped = escapeForOsascript(script);
85
+ return this.exec(`osascript -e '${escaped}'`);
86
+ }
87
+ /**
88
+ * Execute an AppleScript via System Events.
89
+ */
90
+ async sysEvents(script) {
91
+ return this.osascript(`tell application "System Events" to ${script}`);
92
+ }
93
+ /**
94
+ * Map a modifier key to AppleScript modifier syntax.
95
+ */
96
+ mapModifier(mod) {
97
+ switch (mod.toLowerCase()) {
98
+ case 'ctrl':
99
+ case 'control':
100
+ return 'control down';
101
+ case 'alt':
102
+ case 'option':
103
+ return 'option down';
104
+ case 'shift':
105
+ return 'shift down';
106
+ case 'meta':
107
+ case 'command':
108
+ case 'cmd':
109
+ case 'win':
110
+ return 'command down';
111
+ default:
112
+ return `${mod} down`;
113
+ }
114
+ }
115
+ /**
116
+ * Build the AppleScript 'using' clause for modifiers.
117
+ */
118
+ buildModifierClause(modifiers) {
119
+ if (!modifiers || modifiers.length === 0)
120
+ return '';
121
+ const mapped = modifiers.map(m => this.mapModifier(m));
122
+ return ` using {${mapped.join(', ')}}`;
123
+ }
124
+ // --------------------------------------------------------------------------
125
+ // Lifecycle
126
+ // --------------------------------------------------------------------------
127
+ async initialize() {
128
+ // cliclick is optional but recommended
129
+ this.hasCliclick = this.checkTool('cliclick');
130
+ this.initialized = true;
131
+ }
132
+ async isAvailable() {
133
+ return process.platform === 'darwin';
134
+ }
135
+ async shutdown() {
136
+ this.initialized = false;
137
+ }
138
+ // --------------------------------------------------------------------------
139
+ // Mouse
140
+ // --------------------------------------------------------------------------
141
+ async getMousePosition() {
142
+ this.ensureInitialized();
143
+ if (!this.hasCliclick) {
144
+ throw new Error('cliclick is required for mouse position on macOS. Install with: brew install cliclick');
145
+ }
146
+ try {
147
+ const output = await this.exec('cliclick p:');
148
+ const parts = output.trim().split(',');
149
+ return {
150
+ x: parseInt(parts[0], 10) || 0,
151
+ y: parseInt(parts[1], 10) || 0,
152
+ };
153
+ }
154
+ catch {
155
+ return { x: 0, y: 0 };
156
+ }
157
+ }
158
+ async moveMouse(x, y, _options) {
159
+ this.ensureInitialized();
160
+ if (this.hasCliclick) {
161
+ await this.exec(`cliclick m:${x},${y}`);
162
+ }
163
+ else {
164
+ await this.osascript(`do shell script "cliclick m:${x},${y}"`);
165
+ }
166
+ }
167
+ async click(options) {
168
+ this.ensureInitialized();
169
+ const button = options?.button ?? 'left';
170
+ const clicks = options?.clicks ?? 1;
171
+ if (this.hasCliclick) {
172
+ const pos = await this.getMousePosition();
173
+ const x = pos.x;
174
+ const y = pos.y;
175
+ let clickCmd;
176
+ switch (button) {
177
+ case 'right':
178
+ clickCmd = `rc:${x},${y}`;
179
+ break;
180
+ case 'middle':
181
+ clickCmd = `mc:${x},${y}`;
182
+ break;
183
+ default:
184
+ clickCmd = clicks === 2 ? `dc:${x},${y}` : `c:${x},${y}`;
185
+ break;
186
+ }
187
+ if (button === 'left' && clicks > 2) {
188
+ const cmds = [];
189
+ for (let i = 0; i < clicks; i++) {
190
+ cmds.push(`c:${x},${y}`);
191
+ }
192
+ await this.exec(`cliclick ${cmds.join(' ')}`);
193
+ }
194
+ else {
195
+ await this.exec(`cliclick ${clickCmd}`);
196
+ }
197
+ }
198
+ else {
199
+ await this.sysEvents('click at {0, 0}');
200
+ }
201
+ }
202
+ async doubleClick(button) {
203
+ this.ensureInitialized();
204
+ if (this.hasCliclick) {
205
+ const pos = await this.getMousePosition();
206
+ if (button === 'right') {
207
+ await this.exec(`cliclick rc:${pos.x},${pos.y} rc:${pos.x},${pos.y}`);
208
+ }
209
+ else {
210
+ await this.exec(`cliclick dc:${pos.x},${pos.y}`);
211
+ }
212
+ }
213
+ else {
214
+ await this.sysEvents('click at {0, 0}');
215
+ }
216
+ }
217
+ async rightClick() {
218
+ this.ensureInitialized();
219
+ if (this.hasCliclick) {
220
+ const pos = await this.getMousePosition();
221
+ await this.exec(`cliclick rc:${pos.x},${pos.y}`);
222
+ }
223
+ else {
224
+ await this.sysEvents('click at {0, 0}');
225
+ }
226
+ }
227
+ async drag(fromX, fromY, toX, toY, _options) {
228
+ this.ensureInitialized();
229
+ if (this.hasCliclick) {
230
+ await this.exec(`cliclick dd:${fromX},${fromY} du:${toX},${toY}`);
231
+ }
232
+ else {
233
+ throw new Error('Drag requires cliclick on macOS. Install with: brew install cliclick');
234
+ }
235
+ }
236
+ async scroll(options) {
237
+ this.ensureInitialized();
238
+ const { deltaX = 0, deltaY = 0 } = options;
239
+ if (deltaY !== 0) {
240
+ if (this.hasCliclick) {
241
+ // cliclick scroll: positive = up, negative = down
242
+ const amount = -deltaY;
243
+ await this.exec(`cliclick "scroll:0,${amount}"`);
244
+ }
245
+ else {
246
+ // Fallback: use key codes for up/down arrows
247
+ const keyCode = deltaY < 0 ? 126 : 125; // up : down
248
+ const count = Math.abs(Math.round(deltaY));
249
+ for (let i = 0; i < count; i++) {
250
+ await this.sysEvents(`key code ${keyCode}`);
251
+ }
252
+ }
253
+ }
254
+ if (deltaX !== 0) {
255
+ if (this.hasCliclick) {
256
+ const amount = -deltaX;
257
+ await this.exec(`cliclick "scroll:${amount},0"`);
258
+ }
259
+ else {
260
+ const keyCode = deltaX < 0 ? 123 : 124; // left : right
261
+ const count = Math.abs(Math.round(deltaX));
262
+ for (let i = 0; i < count; i++) {
263
+ await this.sysEvents(`key code ${keyCode}`);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ // --------------------------------------------------------------------------
269
+ // Keyboard
270
+ // --------------------------------------------------------------------------
271
+ async keyPress(key, options) {
272
+ this.ensureInitialized();
273
+ const modClause = this.buildModifierClause(options?.modifiers);
274
+ const lowerKey = key.toLowerCase();
275
+ const code = keyCodeMap[lowerKey];
276
+ if (code !== undefined) {
277
+ await this.sysEvents(`key code ${code}${modClause}`);
278
+ }
279
+ else if (key.length === 1) {
280
+ const escaped = escapeAppleString(key);
281
+ await this.sysEvents(`keystroke "${escaped}"${modClause}`);
282
+ }
283
+ else {
284
+ // Try as key code name
285
+ const fallbackCode = keyCodeMap[lowerKey];
286
+ if (fallbackCode !== undefined) {
287
+ await this.sysEvents(`key code ${fallbackCode}${modClause}`);
288
+ }
289
+ else {
290
+ await this.sysEvents(`keystroke "${escapeAppleString(key)}"${modClause}`);
291
+ }
292
+ }
293
+ }
294
+ async keyDown(key) {
295
+ this.ensureInitialized();
296
+ const lowerKey = key.toLowerCase();
297
+ const code = keyCodeMap[lowerKey];
298
+ if (code !== undefined) {
299
+ await this.sysEvents(`key down ${code}`);
300
+ }
301
+ else {
302
+ await this.sysEvents(`key down "${escapeAppleString(key)}"`);
303
+ }
304
+ }
305
+ async keyUp(key) {
306
+ this.ensureInitialized();
307
+ const lowerKey = key.toLowerCase();
308
+ const code = keyCodeMap[lowerKey];
309
+ if (code !== undefined) {
310
+ await this.sysEvents(`key up ${code}`);
311
+ }
312
+ else {
313
+ await this.sysEvents(`key up "${escapeAppleString(key)}"`);
314
+ }
315
+ }
316
+ async type(text, options) {
317
+ this.ensureInitialized();
318
+ const delay = options?.delay ?? 0;
319
+ // Split long text into chunks to avoid osascript limits
320
+ const chunkSize = 200;
321
+ for (let i = 0; i < text.length; i += chunkSize) {
322
+ const chunk = text.substring(i, i + chunkSize);
323
+ const escaped = escapeAppleString(chunk);
324
+ await this.sysEvents(`keystroke "${escaped}"`);
325
+ if (delay > 0 && i + chunkSize < text.length) {
326
+ await this.delay(delay);
327
+ }
328
+ }
329
+ }
330
+ async hotkey(sequence) {
331
+ this.ensureInitialized();
332
+ const modClause = this.buildModifierClause(sequence.modifiers);
333
+ for (const key of sequence.keys) {
334
+ const lowerKey = key.toLowerCase();
335
+ const code = keyCodeMap[lowerKey];
336
+ if (code !== undefined) {
337
+ await this.sysEvents(`key code ${code}${modClause}`);
338
+ }
339
+ else if (key.length === 1) {
340
+ await this.sysEvents(`keystroke "${escapeAppleString(key)}"${modClause}`);
341
+ }
342
+ else {
343
+ await this.sysEvents(`keystroke "${escapeAppleString(key)}"${modClause}`);
344
+ }
345
+ }
346
+ }
347
+ // --------------------------------------------------------------------------
348
+ // Window Management
349
+ // --------------------------------------------------------------------------
350
+ async getActiveWindow() {
351
+ this.ensureInitialized();
352
+ try {
353
+ const appName = await this.osascript('tell application "System Events" to get name of first application process whose frontmost is true');
354
+ if (!appName)
355
+ return null;
356
+ // Get window properties
357
+ const script = `tell application "System Events"
358
+ set proc to first application process whose name is "${escapeAppleString(appName)}"
359
+ set wins to windows of proc
360
+ if (count of wins) > 0 then
361
+ set w to window 1 of proc
362
+ set wName to name of w
363
+ set wPos to position of w
364
+ set wSize to size of w
365
+ set wPid to unix id of proc
366
+ return wName & "|" & (item 1 of wPos) & "," & (item 2 of wPos) & "|" & (item 1 of wSize) & "," & (item 2 of wSize) & "|" & wPid
367
+ end if
368
+ end tell`;
369
+ const result = await this.osascript(script);
370
+ if (!result)
371
+ return null;
372
+ const parts = result.split('|');
373
+ const title = parts[0] ?? '';
374
+ const posStr = (parts[1] ?? '0,0').split(',');
375
+ const sizeStr = (parts[2] ?? '0,0').split(',');
376
+ const pid = parseInt(parts[3] ?? '0', 10) || 0;
377
+ return {
378
+ handle: `${appName}:1`,
379
+ title,
380
+ pid,
381
+ processName: appName,
382
+ bounds: {
383
+ x: parseInt(posStr[0], 10) || 0,
384
+ y: parseInt(posStr[1], 10) || 0,
385
+ width: parseInt(sizeStr[0], 10) || 0,
386
+ height: parseInt(sizeStr[1], 10) || 0,
387
+ },
388
+ focused: true,
389
+ visible: true,
390
+ minimized: false,
391
+ maximized: false,
392
+ fullscreen: false,
393
+ };
394
+ }
395
+ catch {
396
+ return null;
397
+ }
398
+ }
399
+ async getWindows(options) {
400
+ this.ensureInitialized();
401
+ try {
402
+ const script = `tell application "System Events"
403
+ set output to ""
404
+ repeat with proc in (every application process whose visible is true)
405
+ set procName to name of proc
406
+ set procPid to unix id of proc
407
+ set idx to 1
408
+ repeat with w in (every window of proc)
409
+ set wName to name of w
410
+ set wPos to position of w
411
+ set wSize to size of w
412
+ set output to output & procName & "|||" & wName & "|||" & (item 1 of wPos) & "," & (item 2 of wPos) & "|||" & (item 1 of wSize) & "," & (item 2 of wSize) & "|||" & procPid & "|||" & idx & "###"
413
+ set idx to idx + 1
414
+ end repeat
415
+ end repeat
416
+ return output
417
+ end tell`;
418
+ const result = await this.osascript(script);
419
+ if (!result)
420
+ return [];
421
+ const entries = result.split('###').filter(e => e.trim().length > 0);
422
+ const windows = [];
423
+ for (const entry of entries) {
424
+ const parts = entry.split('|||');
425
+ if (parts.length < 6)
426
+ continue;
427
+ const processName = parts[0].trim();
428
+ const title = parts[1].trim();
429
+ const posStr = parts[2].split(',');
430
+ const sizeStr = parts[3].split(',');
431
+ const pid = parseInt(parts[4], 10) || 0;
432
+ const idx = parseInt(parts[5], 10) || 1;
433
+ const win = {
434
+ handle: `${processName}:${idx}`,
435
+ title,
436
+ pid,
437
+ processName,
438
+ bounds: {
439
+ x: parseInt(posStr[0], 10) || 0,
440
+ y: parseInt(posStr[1], 10) || 0,
441
+ width: parseInt(sizeStr[0], 10) || 0,
442
+ height: parseInt(sizeStr[1], 10) || 0,
443
+ },
444
+ focused: false,
445
+ visible: true,
446
+ minimized: false,
447
+ maximized: false,
448
+ fullscreen: false,
449
+ };
450
+ // Apply filters
451
+ if (options?.title) {
452
+ const titleMatch = options.title instanceof RegExp
453
+ ? options.title.test(win.title)
454
+ : win.title.includes(options.title);
455
+ if (!titleMatch)
456
+ continue;
457
+ }
458
+ if (options?.processName && win.processName !== options.processName)
459
+ continue;
460
+ if (options?.pid && win.pid !== options.pid)
461
+ continue;
462
+ windows.push(win);
463
+ }
464
+ return windows;
465
+ }
466
+ catch {
467
+ return [];
468
+ }
469
+ }
470
+ async getWindow(handle) {
471
+ this.ensureInitialized();
472
+ try {
473
+ const { processName, windowIndex } = parseHandle(handle);
474
+ const script = `tell application "System Events"
475
+ set proc to first application process whose name is "${escapeAppleString(processName)}"
476
+ set wins to windows of proc
477
+ if (count of wins) >= ${windowIndex} then
478
+ set w to window ${windowIndex} of proc
479
+ set wName to name of w
480
+ set wPos to position of w
481
+ set wSize to size of w
482
+ set wPid to unix id of proc
483
+ set isFront to frontmost of proc
484
+ return wName & "|" & (item 1 of wPos) & "," & (item 2 of wPos) & "|" & (item 1 of wSize) & "," & (item 2 of wSize) & "|" & wPid & "|" & isFront
485
+ end if
486
+ end tell`;
487
+ const result = await this.osascript(script);
488
+ if (!result)
489
+ return null;
490
+ const parts = result.split('|');
491
+ const title = parts[0] ?? '';
492
+ const posStr = (parts[1] ?? '0,0').split(',');
493
+ const sizeStr = (parts[2] ?? '0,0').split(',');
494
+ const pid = parseInt(parts[3] ?? '0', 10) || 0;
495
+ const focused = parts[4]?.trim() === 'true';
496
+ return {
497
+ handle,
498
+ title,
499
+ pid,
500
+ processName,
501
+ bounds: {
502
+ x: parseInt(posStr[0], 10) || 0,
503
+ y: parseInt(posStr[1], 10) || 0,
504
+ width: parseInt(sizeStr[0], 10) || 0,
505
+ height: parseInt(sizeStr[1], 10) || 0,
506
+ },
507
+ focused,
508
+ visible: true,
509
+ minimized: false,
510
+ maximized: false,
511
+ fullscreen: false,
512
+ };
513
+ }
514
+ catch {
515
+ return null;
516
+ }
517
+ }
518
+ async focusWindow(handle) {
519
+ this.ensureInitialized();
520
+ const { processName, windowIndex } = parseHandle(handle);
521
+ const escaped = escapeAppleString(processName);
522
+ await this.osascript(`tell application "${escaped}" to activate`);
523
+ if (windowIndex > 1) {
524
+ await this.sysEvents(`perform action "AXRaise" of window ${windowIndex} of process "${escaped}"`);
525
+ }
526
+ }
527
+ async minimizeWindow(handle) {
528
+ this.ensureInitialized();
529
+ const { processName, windowIndex } = parseHandle(handle);
530
+ const escaped = escapeAppleString(processName);
531
+ await this.sysEvents(`set miniaturized of window ${windowIndex} of process "${escaped}" to true`);
532
+ }
533
+ async maximizeWindow(handle) {
534
+ this.ensureInitialized();
535
+ const { processName, windowIndex } = parseHandle(handle);
536
+ const escaped = escapeAppleString(processName);
537
+ // macOS has no true maximize; move to origin and resize to screen size
538
+ try {
539
+ const screenScript = `tell application "Finder" to get bounds of window of desktop`;
540
+ const boundsStr = await this.osascript(screenScript);
541
+ const bounds = boundsStr.split(',').map(s => parseInt(s.trim(), 10));
542
+ if (bounds.length >= 4) {
543
+ const width = bounds[2] - bounds[0];
544
+ const height = bounds[3] - bounds[1];
545
+ await this.sysEvents(`set position of window ${windowIndex} of process "${escaped}" to {0, 0}`);
546
+ await this.sysEvents(`set size of window ${windowIndex} of process "${escaped}" to {${width}, ${height}}`);
547
+ }
548
+ }
549
+ catch {
550
+ // Fallback: use AXZoomButton (green button) behavior
551
+ await this.sysEvents(`click button 2 of window ${windowIndex} of process "${escaped}"`);
552
+ }
553
+ }
554
+ async restoreWindow(handle) {
555
+ this.ensureInitialized();
556
+ const { processName, windowIndex } = parseHandle(handle);
557
+ const escaped = escapeAppleString(processName);
558
+ await this.sysEvents(`set miniaturized of window ${windowIndex} of process "${escaped}" to false`);
559
+ }
560
+ async closeWindow(handle) {
561
+ this.ensureInitialized();
562
+ const { processName, windowIndex } = parseHandle(handle);
563
+ const escaped = escapeAppleString(processName);
564
+ try {
565
+ await this.osascript(`tell application "${escaped}" to close window ${windowIndex}`);
566
+ }
567
+ catch {
568
+ // Fallback: click close button via System Events
569
+ await this.sysEvents(`click button 1 of window ${windowIndex} of process "${escaped}"`);
570
+ }
571
+ }
572
+ async setWindow(handle, options) {
573
+ this.ensureInitialized();
574
+ const { processName, windowIndex } = parseHandle(handle);
575
+ const escaped = escapeAppleString(processName);
576
+ if (options.position) {
577
+ await this.sysEvents(`set position of window ${windowIndex} of process "${escaped}" to {${options.position.x}, ${options.position.y}}`);
578
+ }
579
+ if (options.size) {
580
+ await this.sysEvents(`set size of window ${windowIndex} of process "${escaped}" to {${options.size.width}, ${options.size.height}}`);
581
+ }
582
+ if (options.focus) {
583
+ await this.focusWindow(handle);
584
+ }
585
+ }
586
+ // --------------------------------------------------------------------------
587
+ // Application Management
588
+ // --------------------------------------------------------------------------
589
+ async getRunningApps() {
590
+ this.ensureInitialized();
591
+ try {
592
+ const script = `tell application "System Events"
593
+ set output to ""
594
+ repeat with proc in (every application process whose background only is false)
595
+ set procName to name of proc
596
+ set procPid to unix id of proc
597
+ set procFile to ""
598
+ try
599
+ set procFile to POSIX path of (file of proc as alias)
600
+ end try
601
+ set output to output & procName & "|||" & procPid & "|||" & procFile & "###"
602
+ end repeat
603
+ return output
604
+ end tell`;
605
+ const result = await this.osascript(script);
606
+ if (!result)
607
+ return [];
608
+ const entries = result.split('###').filter(e => e.trim().length > 0);
609
+ const apps = [];
610
+ for (const entry of entries) {
611
+ const parts = entry.split('|||');
612
+ if (parts.length < 3)
613
+ continue;
614
+ apps.push({
615
+ name: parts[0].trim(),
616
+ path: parts[2]?.trim() ?? '',
617
+ pid: parseInt(parts[1], 10) || undefined,
618
+ running: true,
619
+ });
620
+ }
621
+ return apps;
622
+ }
623
+ catch {
624
+ return [];
625
+ }
626
+ }
627
+ async launchApp(appPath, options) {
628
+ this.ensureInitialized();
629
+ const args = options?.args ?? [];
630
+ const appName = appPath.replace(/\.app$/, '').split('/').pop() ?? appPath;
631
+ // Use 'open' command for .app bundles or direct paths
632
+ const openArgs = ['-a', appPath, ...args];
633
+ if (options?.hidden) {
634
+ openArgs.unshift('-g'); // open in background
635
+ }
636
+ const child = spawn('open', openArgs, {
637
+ cwd: options?.cwd,
638
+ env: options?.env ? { ...process.env, ...options.env } : undefined,
639
+ detached: true,
640
+ stdio: 'ignore',
641
+ });
642
+ child.unref();
643
+ // Wait briefly then try to get PID
644
+ await this.delay(500);
645
+ let pid;
646
+ try {
647
+ const pidStr = await this.exec(`pgrep -n "${escapeAppleString(appName)}"`);
648
+ pid = parseInt(pidStr.trim(), 10) || undefined;
649
+ }
650
+ catch {
651
+ // Process might not be findable by name
652
+ }
653
+ return {
654
+ name: appName,
655
+ path: appPath,
656
+ pid,
657
+ running: true,
658
+ };
659
+ }
660
+ async closeApp(pid) {
661
+ this.ensureInitialized();
662
+ try {
663
+ // Try graceful quit via AppleScript first
664
+ const nameResult = await this.exec(`ps -p ${pid} -o comm=`);
665
+ const appName = nameResult.trim();
666
+ if (appName) {
667
+ await this.osascript(`tell application "${escapeAppleString(appName)}" to quit`);
668
+ return;
669
+ }
670
+ }
671
+ catch {
672
+ // Fallback to SIGTERM
673
+ }
674
+ try {
675
+ process.kill(pid, 'SIGTERM');
676
+ }
677
+ catch {
678
+ // Process may already be gone
679
+ }
680
+ }
681
+ // --------------------------------------------------------------------------
682
+ // Screen
683
+ // --------------------------------------------------------------------------
684
+ async getScreens() {
685
+ this.ensureInitialized();
686
+ try {
687
+ const output = await this.exec('system_profiler SPDisplaysDataType -json');
688
+ const data = JSON.parse(output);
689
+ const screens = [];
690
+ let screenId = 0;
691
+ const gpus = data?.SPDisplaysDataType ?? [];
692
+ for (const gpu of gpus) {
693
+ const displays = gpu?.spdisplays_ndrvs ?? [];
694
+ for (const display of displays) {
695
+ const resolution = display?._spdisplays_resolution ?? '';
696
+ // Parse resolution like "1920 x 1080 (QHD/QWXGA - Wide)"
697
+ const resMatch = resolution.match(/(\d+)\s*x\s*(\d+)/);
698
+ const width = resMatch ? parseInt(resMatch[1], 10) : 0;
699
+ const height = resMatch ? parseInt(resMatch[2], 10) : 0;
700
+ const name = display?._name ?? `Display ${screenId}`;
701
+ const isPrimary = display?.spdisplays_main === 'spdisplays_yes';
702
+ const scaleStr = display?._spdisplays_retina ?? '';
703
+ const scaleFactor = scaleStr.toLowerCase().includes('retina') ? 2 : 1;
704
+ screens.push({
705
+ id: screenId++,
706
+ name,
707
+ bounds: { x: 0, y: 0, width, height },
708
+ workArea: { x: 0, y: 0, width, height },
709
+ scaleFactor,
710
+ primary: isPrimary,
711
+ });
712
+ }
713
+ }
714
+ return screens;
715
+ }
716
+ catch {
717
+ return [];
718
+ }
719
+ }
720
+ async getPixelColor(_x, _y) {
721
+ throw new Error('Color picker requires screenshot analysis on macOS');
722
+ }
723
+ // --------------------------------------------------------------------------
724
+ // Clipboard
725
+ // --------------------------------------------------------------------------
726
+ async getClipboard() {
727
+ this.ensureInitialized();
728
+ let text = '';
729
+ try {
730
+ text = await this.exec('pbpaste');
731
+ }
732
+ catch {
733
+ // Clipboard may be empty
734
+ }
735
+ return {
736
+ text: text || undefined,
737
+ formats: text ? ['text'] : [],
738
+ };
739
+ }
740
+ async setClipboard(content) {
741
+ this.ensureInitialized();
742
+ const text = content.text ?? '';
743
+ try {
744
+ nodeExecSync('pbcopy', {
745
+ input: text,
746
+ encoding: 'utf-8',
747
+ timeout: 5000,
748
+ });
749
+ }
750
+ catch {
751
+ // Clipboard operation failed
752
+ }
753
+ }
754
+ async clearClipboard() {
755
+ this.ensureInitialized();
756
+ try {
757
+ nodeExecSync('pbcopy', {
758
+ input: '',
759
+ encoding: 'utf-8',
760
+ timeout: 5000,
761
+ });
762
+ }
763
+ catch {
764
+ // Clipboard operation failed
765
+ }
766
+ }
767
+ }
768
+ //# sourceMappingURL=macos-native-provider.js.map