@phuetz/code-buddy 0.1.4 → 0.1.6
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.
- package/.codebuddy/README.md +52 -0
- package/README.md +17 -17
- package/dist/browser-automation/browser-manager.js +0 -2
- package/dist/browser-automation/browser-manager.js.map +1 -1
- package/dist/codebuddy/tool-definitions/computer-control-tools.js +2 -0
- package/dist/codebuddy/tool-definitions/computer-control-tools.js.map +1 -1
- package/dist/commands/cli/daemon-commands.js +25 -0
- package/dist/commands/cli/daemon-commands.js.map +1 -1
- package/dist/daemon/daemon-manager.js +3 -1
- package/dist/daemon/daemon-manager.js.map +1 -1
- package/dist/desktop-automation/automation-manager.d.ts +8 -0
- package/dist/desktop-automation/automation-manager.js +52 -0
- package/dist/desktop-automation/automation-manager.js.map +1 -1
- package/dist/desktop-automation/base-native-provider.d.ts +72 -0
- package/dist/desktop-automation/base-native-provider.js +75 -0
- package/dist/desktop-automation/base-native-provider.js.map +1 -0
- package/dist/desktop-automation/index.d.ts +4 -0
- package/dist/desktop-automation/index.js +5 -0
- package/dist/desktop-automation/index.js.map +1 -1
- package/dist/desktop-automation/linux-native-provider.d.ts +48 -0
- package/dist/desktop-automation/linux-native-provider.js +515 -0
- package/dist/desktop-automation/linux-native-provider.js.map +1 -0
- package/dist/desktop-automation/macos-native-provider.d.ts +61 -0
- package/dist/desktop-automation/macos-native-provider.js +768 -0
- package/dist/desktop-automation/macos-native-provider.js.map +1 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +21 -2
- package/dist/desktop-automation/smart-snapshot.js +237 -12
- package/dist/desktop-automation/smart-snapshot.js.map +1 -1
- package/dist/desktop-automation/types.d.ts +1 -1
- package/dist/desktop-automation/types.js +2 -2
- package/dist/desktop-automation/types.js.map +1 -1
- package/dist/desktop-automation/windows-native-provider.d.ts +71 -0
- package/dist/desktop-automation/windows-native-provider.js +653 -0
- package/dist/desktop-automation/windows-native-provider.js.map +1 -0
- package/dist/index.js +31 -38
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +26 -10
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/chat.js +12 -1
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/websocket/handler.js +10 -1
- package/dist/server/websocket/handler.js.map +1 -1
- package/dist/tools/computer-control-tool.d.ts +6 -1
- package/dist/tools/computer-control-tool.js +97 -1
- package/dist/tools/computer-control-tool.js.map +1 -1
- package/dist/tools/screenshot-tool.d.ts +23 -0
- package/dist/tools/screenshot-tool.js +224 -1
- package/dist/tools/screenshot-tool.js.map +1 -1
- 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
|