@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,368 @@
|
|
|
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 { execAtspiHelper } from './atspi-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(): string {
|
|
35
|
+
return join(tmpdir(), `kortix-ss-${randomBytes(6).toString('hex')}.png`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function exec(cmd: string, args: string[]): 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
|
+
proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
|
|
44
|
+
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
45
|
+
proc.on('close', (code) => {
|
|
46
|
+
if (code !== 0) reject(new Error(`${cmd} failed (${code}): ${stderr}`));
|
|
47
|
+
else resolve(stdout);
|
|
48
|
+
});
|
|
49
|
+
proc.on('error', (err) => {
|
|
50
|
+
reject(new Error(`${cmd} not found. Install it: sudo apt install ${cmd}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parsePngDimensions(buf: Buffer): { width: number; height: number } {
|
|
56
|
+
if (buf.length >= 24 && buf[0] === 0x89 && buf[1] === 0x50) {
|
|
57
|
+
return {
|
|
58
|
+
width: buf.readUInt32BE(16),
|
|
59
|
+
height: buf.readUInt32BE(20),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { width: 0, height: 0 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const BUTTON_MAP: Record<string, string> = {
|
|
66
|
+
left: '1',
|
|
67
|
+
middle: '2',
|
|
68
|
+
right: '3',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const SCROLL_MAP: Record<string, string> = {
|
|
72
|
+
up: '4',
|
|
73
|
+
down: '5',
|
|
74
|
+
left: '6',
|
|
75
|
+
right: '7',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export class LinuxDriver implements DesktopDriver {
|
|
79
|
+
async screenshot(options: ScreenshotOptions): Promise<ScreenshotResult> {
|
|
80
|
+
const path = tmpPath();
|
|
81
|
+
|
|
82
|
+
if (options.region) {
|
|
83
|
+
const { x, y, width, height } = options.region;
|
|
84
|
+
await exec('scrot', ['-a', `${x},${y},${width},${height}`, path]);
|
|
85
|
+
} else if (options.windowId) {
|
|
86
|
+
await exec('scrot', ['-u', '-w', path]);
|
|
87
|
+
} else {
|
|
88
|
+
await exec('scrot', [path]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const buf = await readFile(path);
|
|
92
|
+
await unlink(path).catch(() => {});
|
|
93
|
+
const { width, height } = parsePngDimensions(buf);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
image: buf.toString('base64'),
|
|
97
|
+
width,
|
|
98
|
+
height,
|
|
99
|
+
format: 'png',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async mouseClick(options: MouseClickOptions): Promise<void> {
|
|
104
|
+
const button = BUTTON_MAP[options.button || 'left'] || '1';
|
|
105
|
+
const clicks = options.clicks || 1;
|
|
106
|
+
|
|
107
|
+
await exec('xdotool', ['mousemove', '--sync', String(options.x), String(options.y)]);
|
|
108
|
+
|
|
109
|
+
const clickArgs = ['click', '--repeat', String(clicks), button];
|
|
110
|
+
await exec('xdotool', clickArgs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async mouseMove(options: MouseMoveOptions): Promise<void> {
|
|
114
|
+
await exec('xdotool', ['mousemove', '--sync', String(options.x), String(options.y)]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async mouseDrag(options: MouseDragOptions): Promise<void> {
|
|
118
|
+
const button = BUTTON_MAP[options.button || 'left'] || '1';
|
|
119
|
+
|
|
120
|
+
await exec('xdotool', ['mousemove', '--sync', String(options.fromX), String(options.fromY)]);
|
|
121
|
+
await exec('xdotool', ['mousedown', button]);
|
|
122
|
+
await exec('xdotool', ['mousemove', '--sync', String(options.toX), String(options.toY)]);
|
|
123
|
+
await exec('xdotool', ['mouseup', button]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async mouseScroll(options: MouseScrollOptions): Promise<void> {
|
|
127
|
+
await exec('xdotool', ['mousemove', '--sync', String(options.x), String(options.y)]);
|
|
128
|
+
|
|
129
|
+
const dy = options.deltaY || 0;
|
|
130
|
+
const dx = options.deltaX || 0;
|
|
131
|
+
|
|
132
|
+
if (dy !== 0) {
|
|
133
|
+
const btn = dy > 0 ? SCROLL_MAP.down : SCROLL_MAP.up;
|
|
134
|
+
const count = Math.abs(dy);
|
|
135
|
+
for (let i = 0; i < count; i++) {
|
|
136
|
+
await exec('xdotool', ['click', btn]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (dx !== 0) {
|
|
141
|
+
const btn = dx > 0 ? SCROLL_MAP.right : SCROLL_MAP.left;
|
|
142
|
+
const count = Math.abs(dx);
|
|
143
|
+
for (let i = 0; i < count; i++) {
|
|
144
|
+
await exec('xdotool', ['click', btn]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async mousePosition(): Promise<MousePosition> {
|
|
150
|
+
const out = await exec('xdotool', ['getmouselocation']);
|
|
151
|
+
const match = out.match(/x:(\d+)\s+y:(\d+)/);
|
|
152
|
+
if (!match) throw new Error(`Failed to parse mouse location: ${out}`);
|
|
153
|
+
return { x: parseInt(match[1]), y: parseInt(match[2]) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async keyboardType(options: KeyboardTypeOptions): Promise<void> {
|
|
157
|
+
const args = ['type'];
|
|
158
|
+
if (options.delay) {
|
|
159
|
+
args.push('--delay', String(options.delay));
|
|
160
|
+
}
|
|
161
|
+
args.push('--', options.text);
|
|
162
|
+
await exec('xdotool', args);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async keyboardKey(options: KeyboardKeyOptions): Promise<void> {
|
|
166
|
+
const combo = options.keys.map(k => {
|
|
167
|
+
const map: Record<string, string> = {
|
|
168
|
+
cmd: 'super', command: 'super',
|
|
169
|
+
ctrl: 'ctrl', control: 'ctrl',
|
|
170
|
+
alt: 'alt', option: 'alt',
|
|
171
|
+
shift: 'shift',
|
|
172
|
+
enter: 'Return', return: 'Return',
|
|
173
|
+
tab: 'Tab', space: 'space',
|
|
174
|
+
escape: 'Escape', esc: 'Escape',
|
|
175
|
+
delete: 'BackSpace', backspace: 'BackSpace',
|
|
176
|
+
up: 'Up', down: 'Down', left: 'Left', right: 'Right',
|
|
177
|
+
home: 'Home', end: 'End',
|
|
178
|
+
pageup: 'Prior', pagedown: 'Next',
|
|
179
|
+
};
|
|
180
|
+
return map[k.toLowerCase()] || k;
|
|
181
|
+
}).join('+');
|
|
182
|
+
|
|
183
|
+
await exec('xdotool', ['key', combo]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async windowList(): Promise<WindowInfo[]> {
|
|
187
|
+
const out = await exec('wmctrl', ['-l', '-G', '-p']);
|
|
188
|
+
const lines = out.trim().split('\n').filter(Boolean);
|
|
189
|
+
|
|
190
|
+
return lines.map(line => {
|
|
191
|
+
const parts = line.split(/\s+/);
|
|
192
|
+
const id = parseInt(parts[0], 16);
|
|
193
|
+
const x = parseInt(parts[3]);
|
|
194
|
+
const y = parseInt(parts[4]);
|
|
195
|
+
const width = parseInt(parts[5]);
|
|
196
|
+
const height = parseInt(parts[6]);
|
|
197
|
+
const title = parts.slice(8).join(' ');
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
id,
|
|
201
|
+
app: parts[7] || '',
|
|
202
|
+
title,
|
|
203
|
+
bounds: { x, y, width, height },
|
|
204
|
+
minimized: false,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async windowFocus(windowId: number): Promise<void> {
|
|
210
|
+
await exec('wmctrl', ['-i', '-a', `0x${windowId.toString(16)}`]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async windowResize(windowId: number, bounds: Partial<WindowBounds>): Promise<void> {
|
|
214
|
+
const windows = await this.windowList();
|
|
215
|
+
const win = windows.find(w => w.id === windowId);
|
|
216
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
217
|
+
|
|
218
|
+
const x = bounds.x ?? win.bounds.x;
|
|
219
|
+
const y = bounds.y ?? win.bounds.y;
|
|
220
|
+
const w = bounds.width ?? win.bounds.width;
|
|
221
|
+
const h = bounds.height ?? win.bounds.height;
|
|
222
|
+
|
|
223
|
+
await exec('wmctrl', ['-i', '-r', `0x${windowId.toString(16)}`, '-e', `0,${x},${y},${w},${h}`]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async windowClose(windowId: number): Promise<void> {
|
|
227
|
+
await exec('wmctrl', ['-i', '-c', `0x${windowId.toString(16)}`]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async windowMinimize(windowId: number): Promise<void> {
|
|
231
|
+
await exec('xdotool', ['windowminimize', String(windowId)]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async appLaunch(name: string): Promise<void> {
|
|
235
|
+
const proc = spawn('xdg-open', [name], {
|
|
236
|
+
stdio: 'ignore',
|
|
237
|
+
detached: true,
|
|
238
|
+
});
|
|
239
|
+
proc.unref();
|
|
240
|
+
await new Promise(r => setTimeout(r, 500));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async appQuit(name: string): Promise<void> {
|
|
244
|
+
const out = await exec('pgrep', ['-f', name]).catch(() => '');
|
|
245
|
+
const pids = out.trim().split('\n').filter(Boolean);
|
|
246
|
+
|
|
247
|
+
for (const pid of pids) {
|
|
248
|
+
await exec('kill', [pid]).catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async appList(): Promise<AppInfo[]> {
|
|
253
|
+
const out = await exec('wmctrl', ['-l', '-p']);
|
|
254
|
+
const lines = out.trim().split('\n').filter(Boolean);
|
|
255
|
+
|
|
256
|
+
const seen = new Set<number>();
|
|
257
|
+
const apps: AppInfo[] = [];
|
|
258
|
+
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
const parts = line.split(/\s+/);
|
|
261
|
+
const pid = parseInt(parts[2]);
|
|
262
|
+
if (pid && !seen.has(pid)) {
|
|
263
|
+
seen.add(pid);
|
|
264
|
+
let name = parts.slice(4).join(' ');
|
|
265
|
+
try {
|
|
266
|
+
const cmdline = await exec('cat', [`/proc/${pid}/comm`]);
|
|
267
|
+
name = cmdline.trim() || name;
|
|
268
|
+
} catch {}
|
|
269
|
+
apps.push({ name, pid });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return apps;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async clipboardRead(): Promise<string> {
|
|
277
|
+
return exec('xclip', ['-selection', 'clipboard', '-o']);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async clipboardWrite(text: string): Promise<void> {
|
|
281
|
+
await new Promise<void>((resolve, reject) => {
|
|
282
|
+
const proc = spawn('xclip', ['-selection', 'clipboard'], {
|
|
283
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
284
|
+
});
|
|
285
|
+
proc.on('close', (code) => {
|
|
286
|
+
if (code !== 0) reject(new Error(`xclip failed (${code})`));
|
|
287
|
+
else resolve();
|
|
288
|
+
});
|
|
289
|
+
proc.on('error', () => reject(new Error('xclip not found. Install: sudo apt install xclip')));
|
|
290
|
+
proc.stdin.write(text);
|
|
291
|
+
proc.stdin.end();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async screenInfo(): Promise<ScreenInfo> {
|
|
296
|
+
const out = await exec('xrandr', ['--current']);
|
|
297
|
+
const match = out.match(/(\d+)x(\d+)\+/);
|
|
298
|
+
if (!match) throw new Error('Failed to parse xrandr output');
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
width: parseInt(match[1]),
|
|
302
|
+
height: parseInt(match[2]),
|
|
303
|
+
scaleFactor: 1,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async cursorImage(radius: number = 50): Promise<ScreenshotResult> {
|
|
308
|
+
const pos = await this.mousePosition();
|
|
309
|
+
const x = Math.max(0, pos.x - radius);
|
|
310
|
+
const y = Math.max(0, pos.y - radius);
|
|
311
|
+
const size = radius * 2;
|
|
312
|
+
|
|
313
|
+
return this.screenshot({ region: { x, y, width: size, height: size } });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async axTree(options: AXTreeOptions): Promise<AXTreeResult> {
|
|
317
|
+
const res = await execAtspiHelper({
|
|
318
|
+
action: 'ax_tree',
|
|
319
|
+
pid: options.pid,
|
|
320
|
+
maxDepth: options.maxDepth ?? 8,
|
|
321
|
+
roles: options.roles,
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
root: res.root,
|
|
325
|
+
elementCount: res.elementCount!,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async axAction(options: AXActionOptions): Promise<AXActionResult> {
|
|
330
|
+
const res = await execAtspiHelper({
|
|
331
|
+
action: 'ax_action',
|
|
332
|
+
elementId: options.elementId,
|
|
333
|
+
action_name: options.action,
|
|
334
|
+
pid: options.pid,
|
|
335
|
+
});
|
|
336
|
+
return res as unknown as AXActionResult;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async axSetValue(options: AXSetValueOptions): Promise<AXSetValueResult> {
|
|
340
|
+
const res = await execAtspiHelper({
|
|
341
|
+
action: 'ax_set_value',
|
|
342
|
+
elementId: options.elementId,
|
|
343
|
+
value: options.value,
|
|
344
|
+
pid: options.pid,
|
|
345
|
+
});
|
|
346
|
+
return res as unknown as AXSetValueResult;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async axFocus(options: AXFocusOptions): Promise<AXFocusResult> {
|
|
350
|
+
const res = await execAtspiHelper({
|
|
351
|
+
action: 'ax_focus',
|
|
352
|
+
elementId: options.elementId,
|
|
353
|
+
pid: options.pid,
|
|
354
|
+
});
|
|
355
|
+
return res as unknown as AXFocusResult;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async axSearch(options: AXSearchOptions): Promise<AXSearchResult> {
|
|
359
|
+
const res = await execAtspiHelper({
|
|
360
|
+
action: 'ax_search',
|
|
361
|
+
query: options.query,
|
|
362
|
+
role: options.role,
|
|
363
|
+
pid: options.pid,
|
|
364
|
+
maxResults: options.maxResults ?? 20,
|
|
365
|
+
});
|
|
366
|
+
return { elements: res.elements || [] };
|
|
367
|
+
}
|
|
368
|
+
}
|