@kortix/agent-tunnel 0.1.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.
- package/package.json +37 -0
- package/src/agent/agent.ts +331 -0
- package/src/agent/capabilities/desktop/atspi-helper.ts +345 -0
- package/src/agent/capabilities/desktop/csharp-helper.ts +914 -0
- package/src/agent/capabilities/desktop/linux-driver.ts +368 -0
- package/src/agent/capabilities/desktop/macos-driver.ts +601 -0
- package/src/agent/capabilities/desktop/swift-helper.ts +736 -0
- package/src/agent/capabilities/desktop/types.ts +201 -0
- package/src/agent/capabilities/desktop/windows-driver.ts +220 -0
- package/src/agent/capabilities/desktop.ts +196 -0
- package/src/agent/capabilities/filesystem.ts +133 -0
- package/src/agent/capabilities/index.ts +42 -0
- package/src/agent/capabilities/shell.ts +96 -0
- package/src/agent/cli.ts +222 -0
- package/src/agent/config.ts +54 -0
- package/src/agent/index.ts +11 -0
- package/src/agent/security/command-validator.ts +61 -0
- package/src/agent/security/path-validator.ts +55 -0
- package/src/agent/security/permission-guard.ts +66 -0
- package/src/client/index.ts +4 -0
- package/src/client/tools.ts +603 -0
- package/src/client/tunnel-client.ts +270 -0
- package/src/index.ts +62 -0
- package/src/server/heartbeat.ts +84 -0
- package/src/server/index.ts +7 -0
- package/src/server/relay.ts +266 -0
- package/src/server/routes.ts +61 -0
- package/src/server/server.ts +114 -0
- package/src/server/ws-handler.ts +54 -0
- package/src/shared/crypto.ts +58 -0
- package/src/shared/index.ts +33 -0
- package/src/shared/types.ts +164 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { readFile, unlink } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
import { execHelper } from './swift-helper';
|
|
7
|
+
import type {
|
|
8
|
+
DesktopDriver,
|
|
9
|
+
ScreenshotOptions,
|
|
10
|
+
ScreenshotResult,
|
|
11
|
+
MouseClickOptions,
|
|
12
|
+
MouseMoveOptions,
|
|
13
|
+
MouseDragOptions,
|
|
14
|
+
MouseScrollOptions,
|
|
15
|
+
MousePosition,
|
|
16
|
+
KeyboardTypeOptions,
|
|
17
|
+
KeyboardKeyOptions,
|
|
18
|
+
WindowInfo,
|
|
19
|
+
WindowBounds,
|
|
20
|
+
AppInfo,
|
|
21
|
+
ScreenInfo,
|
|
22
|
+
AXTreeOptions,
|
|
23
|
+
AXTreeResult,
|
|
24
|
+
AXActionOptions,
|
|
25
|
+
AXActionResult,
|
|
26
|
+
AXSetValueOptions,
|
|
27
|
+
AXSetValueResult,
|
|
28
|
+
AXFocusOptions,
|
|
29
|
+
AXFocusResult,
|
|
30
|
+
AXSearchOptions,
|
|
31
|
+
AXSearchResult,
|
|
32
|
+
} from './types';
|
|
33
|
+
|
|
34
|
+
function tmpPath(ext: string = '.png'): string {
|
|
35
|
+
return join(tmpdir(), `kortix-ss-${randomBytes(6).toString('hex')}${ext}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function exec(cmd: string, args: string[], timeoutMs = 15000): Promise<string> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const proc = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
41
|
+
let stdout = '';
|
|
42
|
+
let stderr = '';
|
|
43
|
+
let killed = false;
|
|
44
|
+
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
killed = true;
|
|
47
|
+
proc.kill('SIGKILL');
|
|
48
|
+
reject(new Error(`${cmd} timed out after ${timeoutMs}ms`));
|
|
49
|
+
}, timeoutMs);
|
|
50
|
+
|
|
51
|
+
proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
|
|
52
|
+
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
53
|
+
proc.on('close', (code) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (killed) return;
|
|
56
|
+
if (code !== 0) reject(new Error(`${cmd} failed (${code}): ${stderr}`));
|
|
57
|
+
else resolve(stdout);
|
|
58
|
+
});
|
|
59
|
+
proc.on('error', (err) => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
if (!killed) reject(err);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function osascript(script: string, timeoutMs = 15000): Promise<string> {
|
|
67
|
+
return exec('osascript', ['-l', 'JavaScript', '-e', script], timeoutMs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── JXA-based Accessibility (System Events) ──────────────────────────────────
|
|
71
|
+
// Uses Apple's System Events bridge — no compilation, no binary caching,
|
|
72
|
+
// instant code updates. Every property access goes through System Events IPC.
|
|
73
|
+
|
|
74
|
+
const JXA_AX_SCRIPT = `function run(argv) {
|
|
75
|
+
try {
|
|
76
|
+
var p = JSON.parse(argv[0]);
|
|
77
|
+
var se = Application("System Events");
|
|
78
|
+
|
|
79
|
+
// Resolve target process: by PID or frontmost
|
|
80
|
+
var proc;
|
|
81
|
+
if (p.pid && p.pid > 0) {
|
|
82
|
+
var m = se.processes.whose({unixId: p.pid})();
|
|
83
|
+
if (!m.length) return JSON.stringify({ok:false, error:"Process with PID "+p.pid+" not found"});
|
|
84
|
+
proc = m[0];
|
|
85
|
+
} else {
|
|
86
|
+
var m = se.processes.whose({frontmost: true})();
|
|
87
|
+
if (!m.length) return JSON.stringify({ok:false, error:"No frontmost application found"});
|
|
88
|
+
proc = m[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Navigate to element by dot-path (e.g. "0.3.1")
|
|
92
|
+
function nav(path) {
|
|
93
|
+
var el = proc, parts = path.split(".");
|
|
94
|
+
for (var i = 0; i < parts.length; i++) {
|
|
95
|
+
try { el = el.uiElements()[parseInt(parts[i])]; }
|
|
96
|
+
catch(e) { return null; }
|
|
97
|
+
}
|
|
98
|
+
return el;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read all useful properties, safely
|
|
102
|
+
function props(el) {
|
|
103
|
+
var r = {role:"",title:"",value:"",desc:"",pos:[0,0],sz:[0,0],en:true,foc:false,acts:[]};
|
|
104
|
+
try { r.role = el.role() || ""; } catch(e) {}
|
|
105
|
+
try { r.title = el.title() || ""; } catch(e) {}
|
|
106
|
+
try { var v = el.value(); r.value = (v == null) ? "" : String(v); } catch(e) {}
|
|
107
|
+
try { r.desc = el.description() || ""; } catch(e) {}
|
|
108
|
+
try { r.pos = el.position() || [0,0]; } catch(e) {}
|
|
109
|
+
try { r.sz = el.size() || [0,0]; } catch(e) {}
|
|
110
|
+
try { r.en = el.enabled(); } catch(e) {}
|
|
111
|
+
try { r.foc = el.focused(); } catch(e) {}
|
|
112
|
+
try { r.acts = el.actions().map(function(a) { return a.name(); }); } catch(e) {}
|
|
113
|
+
return r;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Convert to output node
|
|
117
|
+
function toNode(id, pr) {
|
|
118
|
+
var label = pr.title || pr.value || pr.desc || "(unnamed)";
|
|
119
|
+
if (label.length > 120) label = label.substring(0, 120) + "...";
|
|
120
|
+
return {
|
|
121
|
+
id:id, role:pr.role, title:label, value:pr.value, description:pr.desc,
|
|
122
|
+
bounds:{x:pr.pos[0]||0, y:pr.pos[1]||0, width:pr.sz[0]||0, height:pr.sz[1]||0},
|
|
123
|
+
enabled:pr.en, focused:pr.foc, actions:pr.acts, children:[]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── TREE ──
|
|
128
|
+
if (p.op === "tree") {
|
|
129
|
+
var cnt = 0, maxD = p.maxDepth || 8;
|
|
130
|
+
function walk(el, id, depth) {
|
|
131
|
+
if (depth > maxD) return null;
|
|
132
|
+
cnt++;
|
|
133
|
+
var pr = props(el);
|
|
134
|
+
var node = toNode(id, pr);
|
|
135
|
+
if (depth < maxD) {
|
|
136
|
+
try {
|
|
137
|
+
var kids = el.uiElements();
|
|
138
|
+
for (var i = 0; i < kids.length; i++) {
|
|
139
|
+
var c = walk(kids[i], id + "." + i, depth + 1);
|
|
140
|
+
if (c) node.children.push(c);
|
|
141
|
+
}
|
|
142
|
+
} catch(e) {}
|
|
143
|
+
}
|
|
144
|
+
return node;
|
|
145
|
+
}
|
|
146
|
+
var root = walk(proc, "0", 0);
|
|
147
|
+
return JSON.stringify({ok:true, root:root, elementCount:cnt});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── ACTION (with before/after verification) ──
|
|
151
|
+
if (p.op === "action") {
|
|
152
|
+
var el = nav(p.elementId || "0");
|
|
153
|
+
if (!el) return JSON.stringify({ok:false, error:"Element not found: " + p.elementId});
|
|
154
|
+
var bPr = props(el);
|
|
155
|
+
try { el.actions.byName(p.actionName).perform(); }
|
|
156
|
+
catch(e) { return JSON.stringify({ok:false, error:"Action failed: " + String(e)}); }
|
|
157
|
+
delay(0.05);
|
|
158
|
+
var aPr = props(el);
|
|
159
|
+
var changed = (bPr.foc !== aPr.foc) || (bPr.value !== aPr.value);
|
|
160
|
+
return JSON.stringify({
|
|
161
|
+
ok:true, action:p.actionName, elementId:p.elementId,
|
|
162
|
+
before:{focused:bPr.foc, value:bPr.value},
|
|
163
|
+
after:{focused:aPr.foc, value:aPr.value},
|
|
164
|
+
stateChanged:changed, role:aPr.role, title:aPr.title||aPr.value||""
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── SET VALUE (direct + verify) ──
|
|
169
|
+
if (p.op === "set_value") {
|
|
170
|
+
var el = nav(p.elementId || "0");
|
|
171
|
+
if (!el) return JSON.stringify({ok:false, error:"Element not found: " + p.elementId});
|
|
172
|
+
try { el.focused = true; } catch(e) {}
|
|
173
|
+
delay(0.03);
|
|
174
|
+
try { el.value = p.value; }
|
|
175
|
+
catch(e) {
|
|
176
|
+
return JSON.stringify({ok:false, elementId:p.elementId, requestedValue:p.value,
|
|
177
|
+
actualValue:"", error:"Cannot set value: " + String(e)});
|
|
178
|
+
}
|
|
179
|
+
delay(0.05);
|
|
180
|
+
var actual = "";
|
|
181
|
+
try { var v = el.value(); actual = (v == null) ? "" : String(v); } catch(e) {}
|
|
182
|
+
var ok = (actual === p.value) || actual.indexOf(p.value) >= 0;
|
|
183
|
+
return JSON.stringify({ok:ok, elementId:p.elementId, requestedValue:p.value,
|
|
184
|
+
actualValue:actual, error:ok ? undefined : "Verification failed: value is " + JSON.stringify(actual)});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── FOCUS (direct + verify) ──
|
|
188
|
+
if (p.op === "focus") {
|
|
189
|
+
var el = nav(p.elementId || "0");
|
|
190
|
+
if (!el) return JSON.stringify({ok:false, error:"Element not found: " + p.elementId});
|
|
191
|
+
var bFoc = false;
|
|
192
|
+
try { bFoc = el.focused(); } catch(e) {}
|
|
193
|
+
try { el.focused = true; }
|
|
194
|
+
catch(e) {
|
|
195
|
+
return JSON.stringify({ok:false, elementId:p.elementId, role:"", title:"",
|
|
196
|
+
before:{focused:bFoc}, after:{focused:false}, error:"Cannot set focus: " + String(e)});
|
|
197
|
+
}
|
|
198
|
+
delay(0.05);
|
|
199
|
+
var pr = props(el);
|
|
200
|
+
return JSON.stringify({ok:pr.foc, elementId:p.elementId, role:pr.role,
|
|
201
|
+
title:pr.title||pr.value||"",
|
|
202
|
+
before:{focused:bFoc}, after:{focused:pr.foc},
|
|
203
|
+
error:pr.foc ? undefined : "Element does not report as focused after setting"});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── SEARCH ──
|
|
207
|
+
if (p.op === "search") {
|
|
208
|
+
var results = [], q = (p.query || "").toLowerCase(), maxR = p.maxResults || 20;
|
|
209
|
+
function srch(el, id, depth) {
|
|
210
|
+
if (results.length >= maxR || depth > 20) return;
|
|
211
|
+
var pr = props(el);
|
|
212
|
+
var txt = (pr.title + " " + pr.value + " " + pr.desc).toLowerCase();
|
|
213
|
+
if (txt.indexOf(q) >= 0) {
|
|
214
|
+
if (!p.role || pr.role.toLowerCase() === p.role.toLowerCase()) {
|
|
215
|
+
results.push(toNode(id, pr));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
var kids = el.uiElements();
|
|
220
|
+
for (var i = 0; i < kids.length; i++) { srch(kids[i], id+"."+i, depth+1); }
|
|
221
|
+
} catch(e) {}
|
|
222
|
+
}
|
|
223
|
+
srch(proc, "0", 0);
|
|
224
|
+
return JSON.stringify({ok:true, elements:results});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return JSON.stringify({ok:false, error:"Unknown op: " + p.op});
|
|
228
|
+
} catch(e) {
|
|
229
|
+
return JSON.stringify({ok:false, error:String(e)});
|
|
230
|
+
}
|
|
231
|
+
}`;
|
|
232
|
+
|
|
233
|
+
async function runAx(params: Record<string, unknown>): Promise<any> {
|
|
234
|
+
const paramsJson = JSON.stringify(params);
|
|
235
|
+
const result = await exec('osascript', ['-l', 'JavaScript', '-e', JXA_AX_SCRIPT, '--', paramsJson], 30000);
|
|
236
|
+
const parsed = JSON.parse(result.trim());
|
|
237
|
+
if (!parsed.ok && parsed.error) throw new Error(parsed.error);
|
|
238
|
+
return parsed;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function captureToBase64(args: string[]): Promise<ScreenshotResult> {
|
|
242
|
+
const capturePath = tmpPath('.png');
|
|
243
|
+
const jpegPath = tmpPath('.jpg');
|
|
244
|
+
await exec('screencapture', ['-x', '-t', 'png', ...args, capturePath]);
|
|
245
|
+
await exec('sips', [
|
|
246
|
+
'-s', 'format', 'jpeg',
|
|
247
|
+
'-s', 'formatOptions', '60',
|
|
248
|
+
'--resampleHeightWidthMax', '1920',
|
|
249
|
+
capturePath,
|
|
250
|
+
'--out', jpegPath,
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
let width = 0, height = 0;
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const info = await exec('sips', ['-g', 'pixelWidth', '-g', 'pixelHeight', jpegPath]);
|
|
257
|
+
const wm = info.match(/pixelWidth:\s*(\d+)/);
|
|
258
|
+
const hm = info.match(/pixelHeight:\s*(\d+)/);
|
|
259
|
+
if (wm) width = parseInt(wm[1], 10);
|
|
260
|
+
if (hm) height = parseInt(hm[1], 10);
|
|
261
|
+
} catch {}
|
|
262
|
+
|
|
263
|
+
const buf = await readFile(jpegPath);
|
|
264
|
+
await unlink(capturePath).catch(() => {});
|
|
265
|
+
await unlink(jpegPath).catch(() => {});
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
image: buf.toString('base64'),
|
|
269
|
+
width,
|
|
270
|
+
height,
|
|
271
|
+
format: 'jpeg',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export class MacOSDriver implements DesktopDriver {
|
|
276
|
+
async screenshot(options: ScreenshotOptions): Promise<ScreenshotResult> {
|
|
277
|
+
const args: string[] = [];
|
|
278
|
+
|
|
279
|
+
if (options.region) {
|
|
280
|
+
const { x, y, width, height } = options.region;
|
|
281
|
+
args.push('-R', `${x},${y},${width},${height}`);
|
|
282
|
+
} else if (options.windowId) {
|
|
283
|
+
args.push('-l', String(options.windowId));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return captureToBase64(args);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async mouseClick(options: MouseClickOptions): Promise<void> {
|
|
290
|
+
await execHelper({
|
|
291
|
+
action: 'click',
|
|
292
|
+
x: options.x,
|
|
293
|
+
y: options.y,
|
|
294
|
+
button: options.button || 'left',
|
|
295
|
+
clicks: options.clicks || 1,
|
|
296
|
+
modifiers: options.modifiers,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async mouseMove(options: MouseMoveOptions): Promise<void> {
|
|
301
|
+
await execHelper({ action: 'move', x: options.x, y: options.y });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async mouseDrag(options: MouseDragOptions): Promise<void> {
|
|
305
|
+
await execHelper({
|
|
306
|
+
action: 'drag',
|
|
307
|
+
x: options.fromX,
|
|
308
|
+
y: options.fromY,
|
|
309
|
+
toX: options.toX,
|
|
310
|
+
toY: options.toY,
|
|
311
|
+
button: options.button || 'left',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async mouseScroll(options: MouseScrollOptions): Promise<void> {
|
|
316
|
+
await execHelper({
|
|
317
|
+
action: 'scroll',
|
|
318
|
+
x: options.x,
|
|
319
|
+
y: options.y,
|
|
320
|
+
deltaX: options.deltaX || 0,
|
|
321
|
+
deltaY: options.deltaY || 0,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async mousePosition(): Promise<MousePosition> {
|
|
326
|
+
const res = await execHelper({ action: 'position' });
|
|
327
|
+
return { x: res.x!, y: res.y! };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async keyboardType(options: KeyboardTypeOptions): Promise<void> {
|
|
331
|
+
const escaped = options.text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
332
|
+
const script = `
|
|
333
|
+
const se = Application("System Events");
|
|
334
|
+
se.keystroke("${escaped}");
|
|
335
|
+
`;
|
|
336
|
+
await osascript(script);
|
|
337
|
+
|
|
338
|
+
if (options.delay) {
|
|
339
|
+
await new Promise(r => setTimeout(r, options.delay));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async keyboardKey(options: KeyboardKeyOptions): Promise<void> {
|
|
344
|
+
await execHelper({ action: 'key', keys: options.keys });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async windowList(): Promise<WindowInfo[]> {
|
|
348
|
+
const script = `
|
|
349
|
+
ObjC.import("CoreGraphics");
|
|
350
|
+
ObjC.import("Foundation");
|
|
351
|
+
const kOnScreen = (1 << 0);
|
|
352
|
+
const kExclDesk = (1 << 4);
|
|
353
|
+
const raw = $.CGWindowListCopyWindowInfo(kOnScreen | kExclDesk, 0);
|
|
354
|
+
const list = ObjC.unwrap(raw);
|
|
355
|
+
const result = [];
|
|
356
|
+
for (let i = 0; i < list.length; i++) {
|
|
357
|
+
const w = list[i];
|
|
358
|
+
const layer = w["kCGWindowLayer"];
|
|
359
|
+
if (layer !== 0) continue;
|
|
360
|
+
const owner = w["kCGWindowOwnerName"] || "";
|
|
361
|
+
const name = w["kCGWindowName"];
|
|
362
|
+
if (name === undefined || name === null) continue;
|
|
363
|
+
const num = w["kCGWindowNumber"];
|
|
364
|
+
const b = w["kCGWindowBounds"];
|
|
365
|
+
result.push({
|
|
366
|
+
id: num,
|
|
367
|
+
app: owner,
|
|
368
|
+
title: name || "",
|
|
369
|
+
bounds: { x: b.X, y: b.Y, width: b.Width, height: b.Height },
|
|
370
|
+
minimized: false,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
JSON.stringify(result);
|
|
374
|
+
`;
|
|
375
|
+
const out = await osascript(script);
|
|
376
|
+
return JSON.parse(out.trim()) as WindowInfo[];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async windowFocus(windowId: number): Promise<void> {
|
|
380
|
+
const windows = await this.windowList();
|
|
381
|
+
const win = windows.find(w => w.id === windowId);
|
|
382
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
383
|
+
|
|
384
|
+
const script = `
|
|
385
|
+
const app = Application("${win.app}");
|
|
386
|
+
app.activate();
|
|
387
|
+
`;
|
|
388
|
+
await osascript(script);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async windowResize(windowId: number, bounds: Partial<WindowBounds>): Promise<void> {
|
|
392
|
+
const windows = await this.windowList();
|
|
393
|
+
const win = windows.find(w => w.id === windowId);
|
|
394
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
395
|
+
|
|
396
|
+
const parts: string[] = [];
|
|
397
|
+
if (bounds.x !== undefined || bounds.y !== undefined) {
|
|
398
|
+
const x = bounds.x ?? win.bounds.x;
|
|
399
|
+
const y = bounds.y ?? win.bounds.y;
|
|
400
|
+
parts.push(`w.position = [${x}, ${y}];`);
|
|
401
|
+
}
|
|
402
|
+
if (bounds.width !== undefined || bounds.height !== undefined) {
|
|
403
|
+
const w = bounds.width ?? win.bounds.width;
|
|
404
|
+
const h = bounds.height ?? win.bounds.height;
|
|
405
|
+
parts.push(`w.size = [${w}, ${h}];`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (parts.length === 0) return;
|
|
409
|
+
|
|
410
|
+
const title = win.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
411
|
+
const script = `
|
|
412
|
+
const se = Application("System Events");
|
|
413
|
+
const proc = se.processes.byName("${win.app}");
|
|
414
|
+
const wins = proc.windows();
|
|
415
|
+
for (const w of wins) {
|
|
416
|
+
try {
|
|
417
|
+
const pos = w.position();
|
|
418
|
+
if (w.title() === "${title}" && pos[0] === ${win.bounds.x} && pos[1] === ${win.bounds.y}) {
|
|
419
|
+
${parts.join('\n ')}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
} catch(e) {}
|
|
423
|
+
}
|
|
424
|
+
`;
|
|
425
|
+
await osascript(script);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async windowClose(windowId: number): Promise<void> {
|
|
429
|
+
const windows = await this.windowList();
|
|
430
|
+
const win = windows.find(w => w.id === windowId);
|
|
431
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
432
|
+
|
|
433
|
+
const title = win.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
434
|
+
const script = `
|
|
435
|
+
const se = Application("System Events");
|
|
436
|
+
const proc = se.processes.byName("${win.app}");
|
|
437
|
+
const wins = proc.windows();
|
|
438
|
+
for (const w of wins) {
|
|
439
|
+
try {
|
|
440
|
+
const pos = w.position();
|
|
441
|
+
if (w.title() === "${title}" && pos[0] === ${win.bounds.x} && pos[1] === ${win.bounds.y}) {
|
|
442
|
+
w.buttons.whose({subrole: "AXCloseButton"})()[0].click();
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
} catch(e) {}
|
|
446
|
+
}
|
|
447
|
+
`;
|
|
448
|
+
await osascript(script);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async windowMinimize(windowId: number): Promise<void> {
|
|
452
|
+
const windows = await this.windowList();
|
|
453
|
+
const win = windows.find(w => w.id === windowId);
|
|
454
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
455
|
+
|
|
456
|
+
const title = win.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
457
|
+
const script = `
|
|
458
|
+
const se = Application("System Events");
|
|
459
|
+
const proc = se.processes.byName("${win.app}");
|
|
460
|
+
const wins = proc.windows();
|
|
461
|
+
for (const w of wins) {
|
|
462
|
+
try {
|
|
463
|
+
const pos = w.position();
|
|
464
|
+
if (w.title() === "${title}" && pos[0] === ${win.bounds.x} && pos[1] === ${win.bounds.y}) {
|
|
465
|
+
w.minimized = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
} catch(e) {}
|
|
469
|
+
}
|
|
470
|
+
`;
|
|
471
|
+
await osascript(script);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async appLaunch(name: string): Promise<void> {
|
|
475
|
+
await exec('open', ['-a', name]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async appQuit(name: string): Promise<void> {
|
|
479
|
+
const script = `
|
|
480
|
+
try {
|
|
481
|
+
const app = Application("${name}");
|
|
482
|
+
app.quit();
|
|
483
|
+
"ok";
|
|
484
|
+
} catch(e) {
|
|
485
|
+
"error: " + e.message;
|
|
486
|
+
}
|
|
487
|
+
`;
|
|
488
|
+
const result = await osascript(script);
|
|
489
|
+
if (result.trim().startsWith('error:')) {
|
|
490
|
+
throw new Error(result.trim());
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async appList(): Promise<AppInfo[]> {
|
|
495
|
+
const script = `
|
|
496
|
+
const se = Application("System Events");
|
|
497
|
+
const procs = se.processes.whose({backgroundOnly: false})();
|
|
498
|
+
const result = [];
|
|
499
|
+
for (const proc of procs) {
|
|
500
|
+
try {
|
|
501
|
+
result.push({
|
|
502
|
+
name: proc.name(),
|
|
503
|
+
pid: proc.unixId(),
|
|
504
|
+
bundleId: proc.bundleIdentifier() || undefined,
|
|
505
|
+
});
|
|
506
|
+
} catch(e) {}
|
|
507
|
+
}
|
|
508
|
+
JSON.stringify(result);
|
|
509
|
+
`;
|
|
510
|
+
const out = await osascript(script);
|
|
511
|
+
return JSON.parse(out.trim()) as AppInfo[];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async clipboardRead(): Promise<string> {
|
|
515
|
+
return exec('pbpaste', []);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async clipboardWrite(text: string): Promise<void> {
|
|
519
|
+
await new Promise<void>((resolve, reject) => {
|
|
520
|
+
const proc = spawn('pbcopy', [], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
521
|
+
proc.on('close', (code) => {
|
|
522
|
+
if (code !== 0) reject(new Error(`pbcopy failed (${code})`));
|
|
523
|
+
else resolve();
|
|
524
|
+
});
|
|
525
|
+
proc.on('error', reject);
|
|
526
|
+
proc.stdin.write(text);
|
|
527
|
+
proc.stdin.end();
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async screenInfo(): Promise<ScreenInfo> {
|
|
532
|
+
const script = `
|
|
533
|
+
ObjC.import("AppKit");
|
|
534
|
+
const screen = $.NSScreen.mainScreen;
|
|
535
|
+
const frame = screen.frame;
|
|
536
|
+
const scale = screen.backingScaleFactor;
|
|
537
|
+
JSON.stringify({
|
|
538
|
+
width: frame.size.width,
|
|
539
|
+
height: frame.size.height,
|
|
540
|
+
scaleFactor: scale,
|
|
541
|
+
});
|
|
542
|
+
`;
|
|
543
|
+
const out = await osascript(script);
|
|
544
|
+
return JSON.parse(out.trim()) as ScreenInfo;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async cursorImage(radius: number = 50): Promise<ScreenshotResult> {
|
|
548
|
+
const pos = await this.mousePosition();
|
|
549
|
+
const x = Math.max(0, Math.round(pos.x - radius));
|
|
550
|
+
const y = Math.max(0, Math.round(pos.y - radius));
|
|
551
|
+
const size = radius * 2;
|
|
552
|
+
|
|
553
|
+
return captureToBase64(['-R', `${x},${y},${size},${size}`]);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async axTree(options: AXTreeOptions): Promise<AXTreeResult> {
|
|
557
|
+
const res = await runAx({
|
|
558
|
+
op: 'tree',
|
|
559
|
+
pid: options.pid || 0,
|
|
560
|
+
maxDepth: options.maxDepth ?? 8,
|
|
561
|
+
});
|
|
562
|
+
return { root: res.root, elementCount: res.elementCount };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async axAction(options: AXActionOptions): Promise<AXActionResult> {
|
|
566
|
+
return await runAx({
|
|
567
|
+
op: 'action',
|
|
568
|
+
elementId: options.elementId,
|
|
569
|
+
actionName: options.action,
|
|
570
|
+
pid: options.pid || 0,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async axSetValue(options: AXSetValueOptions): Promise<AXSetValueResult> {
|
|
575
|
+
return await runAx({
|
|
576
|
+
op: 'set_value',
|
|
577
|
+
elementId: options.elementId,
|
|
578
|
+
value: options.value,
|
|
579
|
+
pid: options.pid || 0,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async axFocus(options: AXFocusOptions): Promise<AXFocusResult> {
|
|
584
|
+
return await runAx({
|
|
585
|
+
op: 'focus',
|
|
586
|
+
elementId: options.elementId,
|
|
587
|
+
pid: options.pid || 0,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async axSearch(options: AXSearchOptions): Promise<AXSearchResult> {
|
|
592
|
+
const res = await runAx({
|
|
593
|
+
op: 'search',
|
|
594
|
+
query: options.query,
|
|
595
|
+
role: options.role,
|
|
596
|
+
pid: options.pid || 0,
|
|
597
|
+
maxResults: options.maxResults ?? 20,
|
|
598
|
+
});
|
|
599
|
+
return { elements: res.elements || [] };
|
|
600
|
+
}
|
|
601
|
+
}
|