@kortix/agent-tunnel 0.1.2 → 0.1.3
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/dist/agent/index.d.ts +140 -0
- package/dist/agent/index.js +21 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent-cli.js +4061 -0
- package/dist/chunk-7N7GSU6K.js +34 -0
- package/dist/client/index.d.ts +183 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-cli.js +752 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +89 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -0
- package/dist/shared/index.d.ts +10 -0
- package/dist/shared/index.js +20 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types-Dpwrd8Ai.d.ts +194 -0
- package/package.json +32 -12
- package/src/agent/cli.ts +1 -1
- package/src/client/cli.ts +2 -3
- package/src/node-ws-polyfill.ts +17 -0
|
@@ -0,0 +1,4061 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
__esm,
|
|
4
|
+
__export,
|
|
5
|
+
__require,
|
|
6
|
+
__toCommonJS
|
|
7
|
+
} from "./chunk-7N7GSU6K.js";
|
|
8
|
+
|
|
9
|
+
// src/agent/capabilities/desktop/swift-helper.ts
|
|
10
|
+
import { spawn as spawn2 } from "child_process";
|
|
11
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync, chmodSync } from "fs";
|
|
12
|
+
import { join as join3 } from "path";
|
|
13
|
+
import { homedir as homedir2 } from "os";
|
|
14
|
+
async function ensureHelper() {
|
|
15
|
+
if (compiled && existsSync2(HELPER_PATH)) return HELPER_PATH;
|
|
16
|
+
if (existsSync2(HELPER_PATH)) {
|
|
17
|
+
compiled = true;
|
|
18
|
+
return HELPER_PATH;
|
|
19
|
+
}
|
|
20
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
21
|
+
const srcPath = join3(BIN_DIR, `desktop-helper-${HELPER_VERSION}.swift`);
|
|
22
|
+
writeFileSync(srcPath, SWIFT_SOURCE);
|
|
23
|
+
await new Promise((resolve2, reject) => {
|
|
24
|
+
const proc = spawn2("swiftc", ["-O", "-o", HELPER_PATH, srcPath], {
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
26
|
+
});
|
|
27
|
+
let stderr = "";
|
|
28
|
+
proc.stderr?.on("data", (d) => {
|
|
29
|
+
stderr += d.toString();
|
|
30
|
+
});
|
|
31
|
+
proc.on("close", (code) => {
|
|
32
|
+
if (code === 0) {
|
|
33
|
+
chmodSync(HELPER_PATH, 493);
|
|
34
|
+
compiled = true;
|
|
35
|
+
resolve2();
|
|
36
|
+
} else {
|
|
37
|
+
reject(new Error(`swiftc failed (exit ${code}): ${stderr}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
proc.on("error", (err) => {
|
|
41
|
+
reject(new Error(`swiftc not found: ${err.message}. Install Xcode CLI tools: xcode-select --install`));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return HELPER_PATH;
|
|
45
|
+
}
|
|
46
|
+
async function execHelper(request) {
|
|
47
|
+
const helperPath = await ensureHelper();
|
|
48
|
+
return new Promise((resolve2, reject) => {
|
|
49
|
+
const proc = spawn2(helperPath, [], {
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
51
|
+
});
|
|
52
|
+
let stdout = "";
|
|
53
|
+
let stderr = "";
|
|
54
|
+
proc.stdout.on("data", (d) => {
|
|
55
|
+
stdout += d.toString();
|
|
56
|
+
});
|
|
57
|
+
proc.stderr.on("data", (d) => {
|
|
58
|
+
stderr += d.toString();
|
|
59
|
+
});
|
|
60
|
+
proc.on("close", (code) => {
|
|
61
|
+
if (code !== 0) {
|
|
62
|
+
if (stderr.includes("accessibility") || stderr.includes("kAXError")) {
|
|
63
|
+
reject(new Error(
|
|
64
|
+
"Accessibility permission required. Open System Settings \u2192 Privacy & Security \u2192 Accessibility \u2192 Enable your terminal app."
|
|
65
|
+
));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
reject(new Error(`Helper failed (exit ${code}): ${stderr}`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const response = JSON.parse(stdout.trim());
|
|
73
|
+
if (!response.ok && response.error) {
|
|
74
|
+
reject(new Error(response.error));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
resolve2(response);
|
|
78
|
+
} catch {
|
|
79
|
+
reject(new Error(`Invalid helper output: ${stdout}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
proc.on("error", reject);
|
|
83
|
+
proc.stdin.write(JSON.stringify(request));
|
|
84
|
+
proc.stdin.end();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
var HELPER_VERSION, BIN_DIR, HELPER_PATH, SWIFT_SOURCE, compiled;
|
|
88
|
+
var init_swift_helper = __esm({
|
|
89
|
+
"src/agent/capabilities/desktop/swift-helper.ts"() {
|
|
90
|
+
"use strict";
|
|
91
|
+
HELPER_VERSION = "v4";
|
|
92
|
+
BIN_DIR = join3(homedir2(), ".agent-tunnel", "bin");
|
|
93
|
+
HELPER_PATH = join3(BIN_DIR, `desktop-helper-${HELPER_VERSION}`);
|
|
94
|
+
SWIFT_SOURCE = `
|
|
95
|
+
import Foundation
|
|
96
|
+
import CoreGraphics
|
|
97
|
+
|
|
98
|
+
struct Request: Decodable {
|
|
99
|
+
let action: String
|
|
100
|
+
let x: Double?
|
|
101
|
+
let y: Double?
|
|
102
|
+
let toX: Double?
|
|
103
|
+
let toY: Double?
|
|
104
|
+
let button: String?
|
|
105
|
+
let clicks: Int?
|
|
106
|
+
let modifiers: [String]?
|
|
107
|
+
let deltaX: Int?
|
|
108
|
+
let deltaY: Int?
|
|
109
|
+
let keys: [String]?
|
|
110
|
+
let pid: Int?
|
|
111
|
+
let maxDepth: Int?
|
|
112
|
+
let roles: [String]?
|
|
113
|
+
let elementId: String?
|
|
114
|
+
let action_name: String?
|
|
115
|
+
let query: String?
|
|
116
|
+
let role: String?
|
|
117
|
+
let maxResults: Int?
|
|
118
|
+
let value: String?
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
struct Response: Encodable {
|
|
122
|
+
let ok: Bool
|
|
123
|
+
let x: Double?
|
|
124
|
+
let y: Double?
|
|
125
|
+
let error: String?
|
|
126
|
+
let elements: String?
|
|
127
|
+
let elementCount: Int?
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// \u2500\u2500\u2500 AX Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
131
|
+
import ApplicationServices
|
|
132
|
+
|
|
133
|
+
var axElementCount = 0
|
|
134
|
+
|
|
135
|
+
func esc(_ s: String) -> String {
|
|
136
|
+
return s.replacingOccurrences(of: "\\\\", with: "\\\\\\\\")
|
|
137
|
+
.replacingOccurrences(of: "\\"", with: "\\\\\\"")
|
|
138
|
+
.replacingOccurrences(of: "\\n", with: "\\\\n")
|
|
139
|
+
.replacingOccurrences(of: "\\r", with: "\\\\r")
|
|
140
|
+
.replacingOccurrences(of: "\\t", with: "\\\\t")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func axStr(_ element: AXUIElement, _ attr: String) -> String {
|
|
144
|
+
var ref: AnyObject?
|
|
145
|
+
let err = AXUIElementCopyAttributeValue(element, attr as CFString, &ref)
|
|
146
|
+
if err != .success { return "" }
|
|
147
|
+
if let s = ref as? String { return s }
|
|
148
|
+
if let n = ref as? NSNumber { return n.stringValue }
|
|
149
|
+
if ref != nil { return "\\(ref!)" }
|
|
150
|
+
return ""
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
struct AXProps {
|
|
154
|
+
var role: String
|
|
155
|
+
var subrole: String
|
|
156
|
+
var title: String
|
|
157
|
+
var value: String
|
|
158
|
+
var description: String
|
|
159
|
+
var label: String
|
|
160
|
+
var roleDescription: String
|
|
161
|
+
var placeholder: String
|
|
162
|
+
var identifier: String
|
|
163
|
+
var help: String
|
|
164
|
+
var bounds: (x: Int, y: Int, w: Int, h: Int)
|
|
165
|
+
var enabled: Bool
|
|
166
|
+
var focused: Bool
|
|
167
|
+
var actions: [String]
|
|
168
|
+
var children: [AXUIElement]
|
|
169
|
+
|
|
170
|
+
// All searchable text combined
|
|
171
|
+
var searchText: String {
|
|
172
|
+
return [title, value, description, label, roleDescription, placeholder, identifier, help]
|
|
173
|
+
.joined(separator: " ")
|
|
174
|
+
.lowercased()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Best display label
|
|
178
|
+
var displayLabel: String {
|
|
179
|
+
if !title.isEmpty { return title }
|
|
180
|
+
if !label.isEmpty { return label }
|
|
181
|
+
if !value.isEmpty {
|
|
182
|
+
let v = value.count > 60 ? String(value.prefix(60)) + "\u2026" : value
|
|
183
|
+
return v
|
|
184
|
+
}
|
|
185
|
+
if !description.isEmpty { return description }
|
|
186
|
+
if !roleDescription.isEmpty { return roleDescription }
|
|
187
|
+
if !placeholder.isEmpty { return "[\\(placeholder)]" }
|
|
188
|
+
if !help.isEmpty { return help }
|
|
189
|
+
return "(unnamed)"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func toJson(id: String) -> String {
|
|
193
|
+
var json = "{"
|
|
194
|
+
json += "\\"id\\":\\"\\(esc(id))\\""
|
|
195
|
+
json += ",\\"role\\":\\"\\(esc(role))\\""
|
|
196
|
+
if !subrole.isEmpty { json += ",\\"subrole\\":\\"\\(esc(subrole))\\"" }
|
|
197
|
+
json += ",\\"title\\":\\"\\(esc(displayLabel))\\""
|
|
198
|
+
json += ",\\"value\\":\\"\\(esc(value))\\""
|
|
199
|
+
json += ",\\"description\\":\\"\\(esc(description))\\""
|
|
200
|
+
if !label.isEmpty { json += ",\\"label\\":\\"\\(esc(label))\\"" }
|
|
201
|
+
if !placeholder.isEmpty { json += ",\\"placeholder\\":\\"\\(esc(placeholder))\\"" }
|
|
202
|
+
if !identifier.isEmpty { json += ",\\"identifier\\":\\"\\(esc(identifier))\\"" }
|
|
203
|
+
json += ",\\"bounds\\":{\\"x\\":\\(bounds.x),\\"y\\":\\(bounds.y),\\"width\\":\\(bounds.w),\\"height\\":\\(bounds.h)}"
|
|
204
|
+
json += ",\\"enabled\\":\\(enabled)"
|
|
205
|
+
json += ",\\"focused\\":\\(focused)"
|
|
206
|
+
json += ",\\"actions\\":["
|
|
207
|
+
json += actions.map { "\\"\\(esc($0))\\"" }.joined(separator: ",")
|
|
208
|
+
json += "]"
|
|
209
|
+
return json
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func readAXProps(_ element: AXUIElement) -> AXProps {
|
|
214
|
+
let role = axStr(element, kAXRoleAttribute as String)
|
|
215
|
+
let subrole = axStr(element, kAXSubroleAttribute as String)
|
|
216
|
+
let title = axStr(element, kAXTitleAttribute as String)
|
|
217
|
+
let description = axStr(element, kAXDescriptionAttribute as String)
|
|
218
|
+
let label = axStr(element, "AXLabel")
|
|
219
|
+
let roleDescription = axStr(element, kAXRoleDescriptionAttribute as String)
|
|
220
|
+
let placeholder = axStr(element, kAXPlaceholderValueAttribute as String)
|
|
221
|
+
let identifier = axStr(element, "AXIdentifier")
|
|
222
|
+
let help = axStr(element, kAXHelpAttribute as String)
|
|
223
|
+
|
|
224
|
+
// Value: read carefully, handle different types
|
|
225
|
+
var valueStr = ""
|
|
226
|
+
var valueRef: AnyObject?
|
|
227
|
+
let valErr = AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &valueRef)
|
|
228
|
+
if valErr == .success, let v = valueRef {
|
|
229
|
+
if let s = v as? String { valueStr = s }
|
|
230
|
+
else if let n = v as? NSNumber { valueStr = n.stringValue }
|
|
231
|
+
else { valueStr = "\\(v)" }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Bounds
|
|
235
|
+
var posPoint = CGPoint.zero
|
|
236
|
+
var posRef: AnyObject?
|
|
237
|
+
if AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &posRef) == .success, let p = posRef {
|
|
238
|
+
AXValueGetValue(p as! AXValue, .cgPoint, &posPoint)
|
|
239
|
+
}
|
|
240
|
+
var sizVal = CGSize.zero
|
|
241
|
+
var sizRef: AnyObject?
|
|
242
|
+
if AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &sizRef) == .success, let s = sizRef {
|
|
243
|
+
AXValueGetValue(s as! AXValue, .cgSize, &sizVal)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// States
|
|
247
|
+
var en: AnyObject?
|
|
248
|
+
AXUIElementCopyAttributeValue(element, kAXEnabledAttribute as CFString, &en)
|
|
249
|
+
let enabled = (en as? Bool) ?? true
|
|
250
|
+
var foc: AnyObject?
|
|
251
|
+
AXUIElementCopyAttributeValue(element, kAXFocusedAttribute as CFString, &foc)
|
|
252
|
+
let focused = (foc as? Bool) ?? false
|
|
253
|
+
|
|
254
|
+
// Actions
|
|
255
|
+
var actionsArray: CFArray?
|
|
256
|
+
AXUIElementCopyActionNames(element, &actionsArray)
|
|
257
|
+
let actions = (actionsArray as? [String]) ?? []
|
|
258
|
+
|
|
259
|
+
// Children
|
|
260
|
+
var childrenRef: AnyObject?
|
|
261
|
+
AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &childrenRef)
|
|
262
|
+
let children = (childrenRef as? [AXUIElement]) ?? []
|
|
263
|
+
|
|
264
|
+
return AXProps(
|
|
265
|
+
role: role, subrole: subrole, title: title, value: valueStr,
|
|
266
|
+
description: description, label: label, roleDescription: roleDescription,
|
|
267
|
+
placeholder: placeholder, identifier: identifier, help: help,
|
|
268
|
+
bounds: (Int(posPoint.x), Int(posPoint.y), Int(sizVal.width), Int(sizVal.height)),
|
|
269
|
+
enabled: enabled, focused: focused, actions: actions, children: children
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
func axTreeToJson(_ element: AXUIElement, depth: Int, maxDepth: Int, roles: [String]?, pathPrefix: String) -> String? {
|
|
274
|
+
if depth > maxDepth { return nil }
|
|
275
|
+
axElementCount += 1
|
|
276
|
+
|
|
277
|
+
let p = readAXProps(element)
|
|
278
|
+
|
|
279
|
+
// If role filter active and this element doesn't match, skip but walk children
|
|
280
|
+
if let r = roles, !r.isEmpty, !r.contains(p.role.lowercased()) {
|
|
281
|
+
var childJsons: [String] = []
|
|
282
|
+
for (i, child) in p.children.enumerated() {
|
|
283
|
+
let childPath = pathPrefix.isEmpty ? "\\(i)" : "\\(pathPrefix).\\(i)"
|
|
284
|
+
if let cj = axTreeToJson(child, depth: depth, maxDepth: maxDepth, roles: roles, pathPrefix: childPath) {
|
|
285
|
+
childJsons.append(cj)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return childJsons.isEmpty ? nil : childJsons.joined(separator: ",")
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var json = p.toJson(id: pathPrefix)
|
|
292
|
+
|
|
293
|
+
json += ",\\"children\\":["
|
|
294
|
+
if depth < maxDepth {
|
|
295
|
+
var childJsons: [String] = []
|
|
296
|
+
for (i, child) in p.children.enumerated() {
|
|
297
|
+
let childPath = pathPrefix.isEmpty ? "\\(i)" : "\\(pathPrefix).\\(i)"
|
|
298
|
+
if let cj = axTreeToJson(child, depth: depth + 1, maxDepth: maxDepth, roles: roles, pathPrefix: childPath) {
|
|
299
|
+
childJsons.append(cj)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
json += childJsons.joined(separator: ",")
|
|
303
|
+
}
|
|
304
|
+
json += "]}"
|
|
305
|
+
return json
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func navigateToElement(_ root: AXUIElement, path: String) -> AXUIElement? {
|
|
309
|
+
let parts = path.split(separator: ".").compactMap { Int($0) }
|
|
310
|
+
var current = root
|
|
311
|
+
for idx in parts {
|
|
312
|
+
var childrenRef: AnyObject?
|
|
313
|
+
AXUIElementCopyAttributeValue(current, kAXChildrenAttribute as CFString, &childrenRef)
|
|
314
|
+
guard let children = childrenRef as? [AXUIElement], idx < children.count else { return nil }
|
|
315
|
+
current = children[idx]
|
|
316
|
+
}
|
|
317
|
+
return current
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
func resolveAppElement(_ pid: Int) -> AXUIElement {
|
|
321
|
+
if pid > 0 {
|
|
322
|
+
return AXUIElementCreateApplication(pid_t(pid))
|
|
323
|
+
}
|
|
324
|
+
// pid=0: get the focused (frontmost) application
|
|
325
|
+
let systemWide = AXUIElementCreateSystemWide()
|
|
326
|
+
var focusedApp: AnyObject?
|
|
327
|
+
let err = AXUIElementCopyAttributeValue(systemWide, kAXFocusedApplicationAttribute as CFString, &focusedApp)
|
|
328
|
+
if err == .success, let app = focusedApp {
|
|
329
|
+
return (app as! AXUIElement)
|
|
330
|
+
}
|
|
331
|
+
// fallback to system-wide (limited)
|
|
332
|
+
return systemWide
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func searchAXTree(_ element: AXUIElement, query: String, roleFilter: String?, maxResults: Int, results: inout [String], pathPrefix: String, depth: Int, maxDepth: Int) {
|
|
336
|
+
if results.count >= maxResults || depth > maxDepth { return }
|
|
337
|
+
|
|
338
|
+
let p = readAXProps(element)
|
|
339
|
+
let q = query.lowercased()
|
|
340
|
+
|
|
341
|
+
// Search across ALL text attributes
|
|
342
|
+
var match = p.searchText.contains(q)
|
|
343
|
+
|
|
344
|
+
if let rf = roleFilter, !rf.isEmpty, p.role.lowercased() != rf.lowercased() {
|
|
345
|
+
match = false
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if match {
|
|
349
|
+
results.append(p.toJson(id: pathPrefix) + ",\\"children\\":[]}")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Walk children
|
|
353
|
+
for (i, child) in p.children.enumerated() {
|
|
354
|
+
if results.count >= maxResults { break }
|
|
355
|
+
let childPath = pathPrefix.isEmpty ? "\\(i)" : "\\(pathPrefix).\\(i)"
|
|
356
|
+
searchAXTree(child, query: query, roleFilter: roleFilter, maxResults: maxResults, results: &results, pathPrefix: childPath, depth: depth + 1, maxDepth: maxDepth)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func modifierFlags(_ names: [String]) -> CGEventFlags {
|
|
361
|
+
var flags = CGEventFlags()
|
|
362
|
+
for name in names {
|
|
363
|
+
switch name.lowercased() {
|
|
364
|
+
case "cmd", "command": flags.insert(.maskCommand)
|
|
365
|
+
case "shift": flags.insert(.maskShift)
|
|
366
|
+
case "alt", "option": flags.insert(.maskAlternate)
|
|
367
|
+
case "ctrl", "control": flags.insert(.maskControl)
|
|
368
|
+
case "fn": flags.insert(.maskSecondaryFn)
|
|
369
|
+
default: break
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return flags
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let keyMap: [String: UInt16] = [
|
|
376
|
+
"return": 36, "enter": 36, "tab": 48, "space": 49, "delete": 51, "backspace": 51,
|
|
377
|
+
"escape": 53, "esc": 53,
|
|
378
|
+
"up": 126, "down": 125, "left": 123, "right": 124,
|
|
379
|
+
"f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97,
|
|
380
|
+
"f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
|
|
381
|
+
"home": 115, "end": 119, "pageup": 116, "pagedown": 121,
|
|
382
|
+
"a": 0, "b": 11, "c": 8, "d": 2, "e": 14, "f": 3, "g": 5, "h": 4,
|
|
383
|
+
"i": 34, "j": 38, "k": 40, "l": 37, "m": 46, "n": 45, "o": 31,
|
|
384
|
+
"p": 35, "q": 12, "r": 15, "s": 1, "t": 17, "u": 32, "v": 9,
|
|
385
|
+
"w": 13, "x": 7, "y": 16, "z": 6,
|
|
386
|
+
"0": 29, "1": 18, "2": 19, "3": 20, "4": 21, "5": 23,
|
|
387
|
+
"6": 22, "7": 26, "8": 28, "9": 25,
|
|
388
|
+
"-": 27, "=": 24, "[": 33, "]": 30, "\\\\": 42, ";": 41,
|
|
389
|
+
"'": 39, ",": 43, ".": 47, "/": 44, "\`": 50,
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
func mouseButton(_ name: String?) -> CGMouseButton {
|
|
393
|
+
switch name?.lowercased() {
|
|
394
|
+
case "right": return .right
|
|
395
|
+
case "middle": return .center
|
|
396
|
+
default: return .left
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func mouseDownType(_ btn: CGMouseButton) -> CGEventType {
|
|
401
|
+
switch btn {
|
|
402
|
+
case .right: return .rightMouseDown
|
|
403
|
+
case .center: return .otherMouseDown
|
|
404
|
+
default: return .leftMouseDown
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func mouseUpType(_ btn: CGMouseButton) -> CGEventType {
|
|
409
|
+
switch btn {
|
|
410
|
+
case .right: return .rightMouseUp
|
|
411
|
+
case .center: return .otherMouseUp
|
|
412
|
+
default: return .leftMouseUp
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func mouseDragType(_ btn: CGMouseButton) -> CGEventType {
|
|
417
|
+
switch btn {
|
|
418
|
+
case .right: return .rightMouseDragged
|
|
419
|
+
case .center: return .otherMouseDragged
|
|
420
|
+
default: return .leftMouseDragged
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
func respond(_ r: Response) {
|
|
425
|
+
let data = try! JSONEncoder().encode(r)
|
|
426
|
+
FileHandle.standardOutput.write(data)
|
|
427
|
+
FileHandle.standardOutput.write("\\n".data(using: .utf8)!)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
func handleRequest(_ req: Request) {
|
|
431
|
+
switch req.action {
|
|
432
|
+
case "click":
|
|
433
|
+
let point = CGPoint(x: req.x ?? 0, y: req.y ?? 0)
|
|
434
|
+
let btn = mouseButton(req.button)
|
|
435
|
+
let clicks = req.clicks ?? 1
|
|
436
|
+
let mods = modifierFlags(req.modifiers ?? [])
|
|
437
|
+
|
|
438
|
+
for i in 0..<clicks {
|
|
439
|
+
if let down = CGEvent(mouseEventSource: nil, mouseType: mouseDownType(btn), mouseCursorPosition: point, mouseButton: btn) {
|
|
440
|
+
down.setIntegerValueField(.mouseEventClickState, value: Int64(i + 1))
|
|
441
|
+
if !mods.isEmpty { down.flags = mods }
|
|
442
|
+
down.post(tap: .cghidEventTap)
|
|
443
|
+
}
|
|
444
|
+
if let up = CGEvent(mouseEventSource: nil, mouseType: mouseUpType(btn), mouseCursorPosition: point, mouseButton: btn) {
|
|
445
|
+
up.setIntegerValueField(.mouseEventClickState, value: Int64(i + 1))
|
|
446
|
+
if !mods.isEmpty { up.flags = mods }
|
|
447
|
+
up.post(tap: .cghidEventTap)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
respond(Response(ok: true, x: nil, y: nil, error: nil, elements: nil, elementCount: nil))
|
|
451
|
+
|
|
452
|
+
case "move":
|
|
453
|
+
let point = CGPoint(x: req.x ?? 0, y: req.y ?? 0)
|
|
454
|
+
if let event = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left) {
|
|
455
|
+
event.post(tap: .cghidEventTap)
|
|
456
|
+
}
|
|
457
|
+
respond(Response(ok: true, x: nil, y: nil, error: nil, elements: nil, elementCount: nil))
|
|
458
|
+
|
|
459
|
+
case "drag":
|
|
460
|
+
let from = CGPoint(x: req.x ?? 0, y: req.y ?? 0)
|
|
461
|
+
let to = CGPoint(x: req.toX ?? 0, y: req.toY ?? 0)
|
|
462
|
+
let btn = mouseButton(req.button)
|
|
463
|
+
|
|
464
|
+
if let down = CGEvent(mouseEventSource: nil, mouseType: mouseDownType(btn), mouseCursorPosition: from, mouseButton: btn) {
|
|
465
|
+
down.post(tap: .cghidEventTap)
|
|
466
|
+
}
|
|
467
|
+
usleep(50000)
|
|
468
|
+
|
|
469
|
+
let steps = 10
|
|
470
|
+
for i in 1...steps {
|
|
471
|
+
let t = Double(i) / Double(steps)
|
|
472
|
+
let mid = CGPoint(x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t)
|
|
473
|
+
if let drag = CGEvent(mouseEventSource: nil, mouseType: mouseDragType(btn), mouseCursorPosition: mid, mouseButton: btn) {
|
|
474
|
+
drag.post(tap: .cghidEventTap)
|
|
475
|
+
}
|
|
476
|
+
usleep(10000)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if let up = CGEvent(mouseEventSource: nil, mouseType: mouseUpType(btn), mouseCursorPosition: to, mouseButton: btn) {
|
|
480
|
+
up.post(tap: .cghidEventTap)
|
|
481
|
+
}
|
|
482
|
+
respond(Response(ok: true, x: nil, y: nil, error: nil, elements: nil, elementCount: nil))
|
|
483
|
+
|
|
484
|
+
case "scroll":
|
|
485
|
+
let point = CGPoint(x: req.x ?? 0, y: req.y ?? 0)
|
|
486
|
+
if let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left) {
|
|
487
|
+
move.post(tap: .cghidEventTap)
|
|
488
|
+
}
|
|
489
|
+
usleep(10000)
|
|
490
|
+
|
|
491
|
+
let dy = Int32(req.deltaY ?? 0)
|
|
492
|
+
let dx = Int32(req.deltaX ?? 0)
|
|
493
|
+
if let scroll = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 3, wheel1: dy, wheel2: dx, wheel3: 0) {
|
|
494
|
+
scroll.post(tap: .cghidEventTap)
|
|
495
|
+
}
|
|
496
|
+
respond(Response(ok: true, x: nil, y: nil, error: nil, elements: nil, elementCount: nil))
|
|
497
|
+
|
|
498
|
+
case "key":
|
|
499
|
+
let keys = req.keys ?? []
|
|
500
|
+
var mods: [String] = []
|
|
501
|
+
var mainKeys: [String] = []
|
|
502
|
+
|
|
503
|
+
for k in keys {
|
|
504
|
+
let lower = k.lowercased()
|
|
505
|
+
if ["cmd", "command", "shift", "alt", "option", "ctrl", "control", "fn"].contains(lower) {
|
|
506
|
+
mods.append(lower)
|
|
507
|
+
} else {
|
|
508
|
+
mainKeys.append(lower)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
let flags = modifierFlags(mods)
|
|
513
|
+
|
|
514
|
+
for key in mainKeys {
|
|
515
|
+
guard let code = keyMap[key] else {
|
|
516
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Unknown key: \\(key)", elements: nil, elementCount: nil))
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
if let down = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: true) {
|
|
520
|
+
if !flags.isEmpty { down.flags = flags }
|
|
521
|
+
down.post(tap: .cghidEventTap)
|
|
522
|
+
}
|
|
523
|
+
if let up = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: false) {
|
|
524
|
+
if !flags.isEmpty { up.flags = flags }
|
|
525
|
+
up.post(tap: .cghidEventTap)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
respond(Response(ok: true, x: nil, y: nil, error: nil, elements: nil, elementCount: nil))
|
|
529
|
+
|
|
530
|
+
case "position":
|
|
531
|
+
let loc = CGEvent(source: nil)!.location
|
|
532
|
+
respond(Response(ok: true, x: Double(loc.x), y: Double(loc.y), error: nil, elements: nil, elementCount: nil))
|
|
533
|
+
|
|
534
|
+
case "ax_tree":
|
|
535
|
+
let pid = req.pid ?? 0
|
|
536
|
+
let maxD = req.maxDepth ?? 8
|
|
537
|
+
let rolesFilter = req.roles?.map { $0.lowercased() }
|
|
538
|
+
|
|
539
|
+
let appElement = resolveAppElement(pid)
|
|
540
|
+
|
|
541
|
+
axElementCount = 0
|
|
542
|
+
let treeJson = axTreeToJson(appElement, depth: 0, maxDepth: maxD, roles: rolesFilter, pathPrefix: "0") ?? "null"
|
|
543
|
+
let treeOut = "{\\"ok\\":true,\\"root\\":" + treeJson + ",\\"elementCount\\":" + "\\(axElementCount)" + "}"
|
|
544
|
+
FileHandle.standardOutput.write(treeOut.data(using: .utf8)!)
|
|
545
|
+
FileHandle.standardOutput.write("\\n".data(using: .utf8)!)
|
|
546
|
+
|
|
547
|
+
case "ax_action":
|
|
548
|
+
let pid = req.pid ?? 0
|
|
549
|
+
let elementId = req.elementId ?? "0"
|
|
550
|
+
let actionName = req.action_name ?? ""
|
|
551
|
+
|
|
552
|
+
let appElement = resolveAppElement(pid)
|
|
553
|
+
|
|
554
|
+
guard let target = navigateToElement(appElement, path: elementId) else {
|
|
555
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Element not found: \\(elementId)", elements: nil, elementCount: nil))
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Read state BEFORE action
|
|
560
|
+
let beforeProps = readAXProps(target)
|
|
561
|
+
let beforeFocused = beforeProps.focused
|
|
562
|
+
let beforeValue = beforeProps.value
|
|
563
|
+
|
|
564
|
+
let result = AXUIElementPerformAction(target, actionName as CFString)
|
|
565
|
+
if result != .success {
|
|
566
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Action failed: \\(actionName) (error \\(result.rawValue))", elements: nil, elementCount: nil))
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Brief pause for state to settle
|
|
571
|
+
usleep(50000)
|
|
572
|
+
|
|
573
|
+
// Read state AFTER action for verification
|
|
574
|
+
let afterProps = readAXProps(target)
|
|
575
|
+
var verifyJson = "{\\"ok\\":true"
|
|
576
|
+
verifyJson += ",\\"action\\":\\"\\(esc(actionName))\\""
|
|
577
|
+
verifyJson += ",\\"elementId\\":\\"\\(esc(elementId))\\""
|
|
578
|
+
verifyJson += ",\\"before\\":{\\"focused\\":\\(beforeFocused),\\"value\\":\\"\\(esc(beforeValue))\\"}"
|
|
579
|
+
verifyJson += ",\\"after\\":{\\"focused\\":\\(afterProps.focused),\\"value\\":\\"\\(esc(afterProps.value))\\"}"
|
|
580
|
+
verifyJson += ",\\"role\\":\\"\\(esc(afterProps.role))\\""
|
|
581
|
+
verifyJson += ",\\"title\\":\\"\\(esc(afterProps.displayLabel))\\""
|
|
582
|
+
let changed = (beforeFocused != afterProps.focused) || (beforeValue != afterProps.value)
|
|
583
|
+
verifyJson += ",\\"stateChanged\\":\\(changed)"
|
|
584
|
+
verifyJson += "}"
|
|
585
|
+
FileHandle.standardOutput.write(verifyJson.data(using: .utf8)!)
|
|
586
|
+
FileHandle.standardOutput.write("\\n".data(using: .utf8)!)
|
|
587
|
+
|
|
588
|
+
case "ax_set_value":
|
|
589
|
+
let pid = req.pid ?? 0
|
|
590
|
+
let elementId = req.elementId ?? "0"
|
|
591
|
+
let newValue = req.value ?? ""
|
|
592
|
+
|
|
593
|
+
let appElement = resolveAppElement(pid)
|
|
594
|
+
|
|
595
|
+
guard let target = navigateToElement(appElement, path: elementId) else {
|
|
596
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Element not found: \\(elementId)", elements: nil, elementCount: nil))
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// First focus the element
|
|
601
|
+
AXUIElementSetAttributeValue(target, kAXFocusedAttribute as CFString, kCFBooleanTrue)
|
|
602
|
+
usleep(30000)
|
|
603
|
+
|
|
604
|
+
// Set the value directly
|
|
605
|
+
let setResult = AXUIElementSetAttributeValue(target, kAXValueAttribute as CFString, newValue as CFTypeRef)
|
|
606
|
+
usleep(50000)
|
|
607
|
+
|
|
608
|
+
// Verify by reading back
|
|
609
|
+
let verifyValue = axStr(target, kAXValueAttribute as String)
|
|
610
|
+
let success = (setResult == .success) && (verifyValue == newValue || verifyValue.contains(newValue))
|
|
611
|
+
|
|
612
|
+
var svJson = "{\\"ok\\":\\(success)"
|
|
613
|
+
svJson += ",\\"elementId\\":\\"\\(esc(elementId))\\""
|
|
614
|
+
svJson += ",\\"requestedValue\\":\\"\\(esc(newValue))\\""
|
|
615
|
+
svJson += ",\\"actualValue\\":\\"\\(esc(verifyValue))\\""
|
|
616
|
+
if !success {
|
|
617
|
+
if setResult != .success {
|
|
618
|
+
svJson += ",\\"error\\":\\"SetAttributeValue failed (error \\(setResult.rawValue)). Element may not support direct value setting.\\""
|
|
619
|
+
} else {
|
|
620
|
+
svJson += ",\\"error\\":\\"Value was set but verification failed. Expected \\\\\\"\\" + esc(newValue) + \\"\\\\\\", got \\\\\\"\\"+ esc(verifyValue) + \\"\\\\\\"\\""
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
svJson += "}"
|
|
624
|
+
FileHandle.standardOutput.write(svJson.data(using: .utf8)!)
|
|
625
|
+
FileHandle.standardOutput.write("\\n".data(using: .utf8)!)
|
|
626
|
+
|
|
627
|
+
case "ax_focus":
|
|
628
|
+
let pid = req.pid ?? 0
|
|
629
|
+
let elementId = req.elementId ?? "0"
|
|
630
|
+
|
|
631
|
+
let appElement = resolveAppElement(pid)
|
|
632
|
+
|
|
633
|
+
guard let target = navigateToElement(appElement, path: elementId) else {
|
|
634
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Element not found: \\(elementId)", elements: nil, elementCount: nil))
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Read focus state before
|
|
639
|
+
let beforeFocProps = readAXProps(target)
|
|
640
|
+
|
|
641
|
+
// Set focused attribute
|
|
642
|
+
let focResult = AXUIElementSetAttributeValue(target, kAXFocusedAttribute as CFString, kCFBooleanTrue)
|
|
643
|
+
usleep(50000)
|
|
644
|
+
|
|
645
|
+
// Verify focus
|
|
646
|
+
let afterFocProps = readAXProps(target)
|
|
647
|
+
let focSuccess = afterFocProps.focused
|
|
648
|
+
|
|
649
|
+
var focJson = "{\\"ok\\":\\(focSuccess)"
|
|
650
|
+
focJson += ",\\"elementId\\":\\"\\(esc(elementId))\\""
|
|
651
|
+
focJson += ",\\"role\\":\\"\\(esc(afterFocProps.role))\\""
|
|
652
|
+
focJson += ",\\"title\\":\\"\\(esc(afterFocProps.displayLabel))\\""
|
|
653
|
+
focJson += ",\\"before\\":{\\"focused\\":\\(beforeFocProps.focused)}"
|
|
654
|
+
focJson += ",\\"after\\":{\\"focused\\":\\(afterFocProps.focused)}"
|
|
655
|
+
if !focSuccess {
|
|
656
|
+
if focResult != .success {
|
|
657
|
+
focJson += ",\\"error\\":\\"SetAttributeValue(kAXFocusedAttribute) failed (error \\(focResult.rawValue))\\""
|
|
658
|
+
} else {
|
|
659
|
+
focJson += ",\\"error\\":\\"Focus was requested but element reports not focused. It may not be focusable.\\""
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
focJson += "}"
|
|
663
|
+
FileHandle.standardOutput.write(focJson.data(using: .utf8)!)
|
|
664
|
+
FileHandle.standardOutput.write("\\n".data(using: .utf8)!)
|
|
665
|
+
|
|
666
|
+
case "ax_search":
|
|
667
|
+
let pid = req.pid ?? 0
|
|
668
|
+
let query = req.query ?? ""
|
|
669
|
+
let roleFilter = req.role
|
|
670
|
+
let maxRes = req.maxResults ?? 20
|
|
671
|
+
|
|
672
|
+
let appElement = resolveAppElement(pid)
|
|
673
|
+
|
|
674
|
+
var searchResults: [String] = []
|
|
675
|
+
searchAXTree(appElement, query: query, roleFilter: roleFilter, maxResults: maxRes, results: &searchResults, pathPrefix: "0", depth: 0, maxDepth: 20)
|
|
676
|
+
|
|
677
|
+
let searchOut = "{\\"ok\\":true,\\"elements\\":[" + searchResults.joined(separator: ",") + "]}"
|
|
678
|
+
FileHandle.standardOutput.write(searchOut.data(using: .utf8)!)
|
|
679
|
+
FileHandle.standardOutput.write("\\n".data(using: .utf8)!)
|
|
680
|
+
|
|
681
|
+
default:
|
|
682
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Unknown action: \\(req.action)", elements: nil, elementCount: nil))
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let input = FileHandle.standardInput.readDataToEndOfFile()
|
|
687
|
+
guard let req = try? JSONDecoder().decode(Request.self, from: input) else {
|
|
688
|
+
respond(Response(ok: false, x: nil, y: nil, error: "Invalid JSON input", elements: nil, elementCount: nil))
|
|
689
|
+
exit(1)
|
|
690
|
+
}
|
|
691
|
+
handleRequest(req)
|
|
692
|
+
`;
|
|
693
|
+
compiled = false;
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// src/agent/capabilities/desktop/macos-driver.ts
|
|
698
|
+
var macos_driver_exports = {};
|
|
699
|
+
__export(macos_driver_exports, {
|
|
700
|
+
MacOSDriver: () => MacOSDriver
|
|
701
|
+
});
|
|
702
|
+
import { spawn as spawn3 } from "child_process";
|
|
703
|
+
import { readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
704
|
+
import { join as join4 } from "path";
|
|
705
|
+
import { tmpdir } from "os";
|
|
706
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
707
|
+
function tmpPath(ext = ".png") {
|
|
708
|
+
return join4(tmpdir(), `tunnel-ss-${randomBytes2(6).toString("hex")}${ext}`);
|
|
709
|
+
}
|
|
710
|
+
function exec(cmd, args, timeoutMs = 15e3) {
|
|
711
|
+
return new Promise((resolve2, reject) => {
|
|
712
|
+
const proc = spawn3(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
713
|
+
let stdout = "";
|
|
714
|
+
let stderr = "";
|
|
715
|
+
let killed = false;
|
|
716
|
+
const timer = setTimeout(() => {
|
|
717
|
+
killed = true;
|
|
718
|
+
proc.kill("SIGKILL");
|
|
719
|
+
reject(new Error(`${cmd} timed out after ${timeoutMs}ms`));
|
|
720
|
+
}, timeoutMs);
|
|
721
|
+
proc.stdout.on("data", (d) => {
|
|
722
|
+
stdout += d.toString();
|
|
723
|
+
});
|
|
724
|
+
proc.stderr.on("data", (d) => {
|
|
725
|
+
stderr += d.toString();
|
|
726
|
+
});
|
|
727
|
+
proc.on("close", (code) => {
|
|
728
|
+
clearTimeout(timer);
|
|
729
|
+
if (killed) return;
|
|
730
|
+
if (code !== 0) reject(new Error(`${cmd} failed (${code}): ${stderr}`));
|
|
731
|
+
else resolve2(stdout);
|
|
732
|
+
});
|
|
733
|
+
proc.on("error", (err) => {
|
|
734
|
+
clearTimeout(timer);
|
|
735
|
+
if (!killed) reject(err);
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function osascript(script, timeoutMs = 15e3) {
|
|
740
|
+
return exec("osascript", ["-l", "JavaScript", "-e", script], timeoutMs);
|
|
741
|
+
}
|
|
742
|
+
async function runAx(params) {
|
|
743
|
+
const paramsJson = JSON.stringify(params);
|
|
744
|
+
const result = await exec("osascript", ["-l", "JavaScript", "-e", JXA_AX_SCRIPT, "--", paramsJson], 3e4);
|
|
745
|
+
const parsed = JSON.parse(result.trim());
|
|
746
|
+
if (!parsed.ok && parsed.error) throw new Error(parsed.error);
|
|
747
|
+
return parsed;
|
|
748
|
+
}
|
|
749
|
+
async function captureToBase64(args) {
|
|
750
|
+
const capturePath = tmpPath(".png");
|
|
751
|
+
const jpegPath = tmpPath(".jpg");
|
|
752
|
+
await exec("screencapture", ["-x", "-t", "png", ...args, capturePath]);
|
|
753
|
+
await exec("sips", [
|
|
754
|
+
"-s",
|
|
755
|
+
"format",
|
|
756
|
+
"jpeg",
|
|
757
|
+
"-s",
|
|
758
|
+
"formatOptions",
|
|
759
|
+
"60",
|
|
760
|
+
"--resampleHeightWidthMax",
|
|
761
|
+
"1920",
|
|
762
|
+
capturePath,
|
|
763
|
+
"--out",
|
|
764
|
+
jpegPath
|
|
765
|
+
]);
|
|
766
|
+
let width = 0, height = 0;
|
|
767
|
+
try {
|
|
768
|
+
const info = await exec("sips", ["-g", "pixelWidth", "-g", "pixelHeight", jpegPath]);
|
|
769
|
+
const wm = info.match(/pixelWidth:\s*(\d+)/);
|
|
770
|
+
const hm = info.match(/pixelHeight:\s*(\d+)/);
|
|
771
|
+
if (wm) width = parseInt(wm[1], 10);
|
|
772
|
+
if (hm) height = parseInt(hm[1], 10);
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
const buf = await readFile2(jpegPath);
|
|
776
|
+
await unlink2(capturePath).catch(() => {
|
|
777
|
+
});
|
|
778
|
+
await unlink2(jpegPath).catch(() => {
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
image: buf.toString("base64"),
|
|
782
|
+
width,
|
|
783
|
+
height,
|
|
784
|
+
format: "jpeg"
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
var JXA_AX_SCRIPT, MacOSDriver;
|
|
788
|
+
var init_macos_driver = __esm({
|
|
789
|
+
"src/agent/capabilities/desktop/macos-driver.ts"() {
|
|
790
|
+
"use strict";
|
|
791
|
+
init_swift_helper();
|
|
792
|
+
JXA_AX_SCRIPT = `function run(argv) {
|
|
793
|
+
try {
|
|
794
|
+
var p = JSON.parse(argv[0]);
|
|
795
|
+
var se = Application("System Events");
|
|
796
|
+
|
|
797
|
+
// Resolve target process: by PID or frontmost
|
|
798
|
+
var proc;
|
|
799
|
+
if (p.pid && p.pid > 0) {
|
|
800
|
+
var m = se.processes.whose({unixId: p.pid})();
|
|
801
|
+
if (!m.length) return JSON.stringify({ok:false, error:"Process with PID "+p.pid+" not found"});
|
|
802
|
+
proc = m[0];
|
|
803
|
+
} else {
|
|
804
|
+
var m = se.processes.whose({frontmost: true})();
|
|
805
|
+
if (!m.length) return JSON.stringify({ok:false, error:"No frontmost application found"});
|
|
806
|
+
proc = m[0];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Navigate to element by dot-path (e.g. "0.3.1")
|
|
810
|
+
function nav(path) {
|
|
811
|
+
var el = proc, parts = path.split(".");
|
|
812
|
+
for (var i = 0; i < parts.length; i++) {
|
|
813
|
+
try { el = el.uiElements()[parseInt(parts[i])]; }
|
|
814
|
+
catch(e) { return null; }
|
|
815
|
+
}
|
|
816
|
+
return el;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Read all useful properties, safely
|
|
820
|
+
function props(el) {
|
|
821
|
+
var r = {role:"",title:"",value:"",desc:"",pos:[0,0],sz:[0,0],en:true,foc:false,acts:[]};
|
|
822
|
+
try { r.role = el.role() || ""; } catch(e) {}
|
|
823
|
+
try { r.title = el.title() || ""; } catch(e) {}
|
|
824
|
+
try { var v = el.value(); r.value = (v == null) ? "" : String(v); } catch(e) {}
|
|
825
|
+
try { r.desc = el.description() || ""; } catch(e) {}
|
|
826
|
+
try { r.pos = el.position() || [0,0]; } catch(e) {}
|
|
827
|
+
try { r.sz = el.size() || [0,0]; } catch(e) {}
|
|
828
|
+
try { r.en = el.enabled(); } catch(e) {}
|
|
829
|
+
try { r.foc = el.focused(); } catch(e) {}
|
|
830
|
+
try { r.acts = el.actions().map(function(a) { return a.name(); }); } catch(e) {}
|
|
831
|
+
return r;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Convert to output node
|
|
835
|
+
function toNode(id, pr) {
|
|
836
|
+
var label = pr.title || pr.value || pr.desc || "(unnamed)";
|
|
837
|
+
if (label.length > 120) label = label.substring(0, 120) + "...";
|
|
838
|
+
return {
|
|
839
|
+
id:id, role:pr.role, title:label, value:pr.value, description:pr.desc,
|
|
840
|
+
bounds:{x:pr.pos[0]||0, y:pr.pos[1]||0, width:pr.sz[0]||0, height:pr.sz[1]||0},
|
|
841
|
+
enabled:pr.en, focused:pr.foc, actions:pr.acts, children:[]
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// \u2500\u2500 TREE \u2500\u2500
|
|
846
|
+
if (p.op === "tree") {
|
|
847
|
+
var cnt = 0, maxD = p.maxDepth || 8;
|
|
848
|
+
function walk(el, id, depth) {
|
|
849
|
+
if (depth > maxD) return null;
|
|
850
|
+
cnt++;
|
|
851
|
+
var pr = props(el);
|
|
852
|
+
var node = toNode(id, pr);
|
|
853
|
+
if (depth < maxD) {
|
|
854
|
+
try {
|
|
855
|
+
var kids = el.uiElements();
|
|
856
|
+
for (var i = 0; i < kids.length; i++) {
|
|
857
|
+
var c = walk(kids[i], id + "." + i, depth + 1);
|
|
858
|
+
if (c) node.children.push(c);
|
|
859
|
+
}
|
|
860
|
+
} catch(e) {}
|
|
861
|
+
}
|
|
862
|
+
return node;
|
|
863
|
+
}
|
|
864
|
+
var root = walk(proc, "0", 0);
|
|
865
|
+
return JSON.stringify({ok:true, root:root, elementCount:cnt});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// \u2500\u2500 ACTION (with before/after verification) \u2500\u2500
|
|
869
|
+
if (p.op === "action") {
|
|
870
|
+
var el = nav(p.elementId || "0");
|
|
871
|
+
if (!el) return JSON.stringify({ok:false, error:"Element not found: " + p.elementId});
|
|
872
|
+
var bPr = props(el);
|
|
873
|
+
try { el.actions.byName(p.actionName).perform(); }
|
|
874
|
+
catch(e) { return JSON.stringify({ok:false, error:"Action failed: " + String(e)}); }
|
|
875
|
+
delay(0.05);
|
|
876
|
+
var aPr = props(el);
|
|
877
|
+
var changed = (bPr.foc !== aPr.foc) || (bPr.value !== aPr.value);
|
|
878
|
+
return JSON.stringify({
|
|
879
|
+
ok:true, action:p.actionName, elementId:p.elementId,
|
|
880
|
+
before:{focused:bPr.foc, value:bPr.value},
|
|
881
|
+
after:{focused:aPr.foc, value:aPr.value},
|
|
882
|
+
stateChanged:changed, role:aPr.role, title:aPr.title||aPr.value||""
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// \u2500\u2500 SET VALUE (direct + verify) \u2500\u2500
|
|
887
|
+
if (p.op === "set_value") {
|
|
888
|
+
var el = nav(p.elementId || "0");
|
|
889
|
+
if (!el) return JSON.stringify({ok:false, error:"Element not found: " + p.elementId});
|
|
890
|
+
try { el.focused = true; } catch(e) {}
|
|
891
|
+
delay(0.03);
|
|
892
|
+
try { el.value = p.value; }
|
|
893
|
+
catch(e) {
|
|
894
|
+
return JSON.stringify({ok:false, elementId:p.elementId, requestedValue:p.value,
|
|
895
|
+
actualValue:"", error:"Cannot set value: " + String(e)});
|
|
896
|
+
}
|
|
897
|
+
delay(0.05);
|
|
898
|
+
var actual = "";
|
|
899
|
+
try { var v = el.value(); actual = (v == null) ? "" : String(v); } catch(e) {}
|
|
900
|
+
var ok = (actual === p.value) || actual.indexOf(p.value) >= 0;
|
|
901
|
+
return JSON.stringify({ok:ok, elementId:p.elementId, requestedValue:p.value,
|
|
902
|
+
actualValue:actual, error:ok ? undefined : "Verification failed: value is " + JSON.stringify(actual)});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// \u2500\u2500 FOCUS (direct + verify) \u2500\u2500
|
|
906
|
+
if (p.op === "focus") {
|
|
907
|
+
var el = nav(p.elementId || "0");
|
|
908
|
+
if (!el) return JSON.stringify({ok:false, error:"Element not found: " + p.elementId});
|
|
909
|
+
var bFoc = false;
|
|
910
|
+
try { bFoc = el.focused(); } catch(e) {}
|
|
911
|
+
try { el.focused = true; }
|
|
912
|
+
catch(e) {
|
|
913
|
+
return JSON.stringify({ok:false, elementId:p.elementId, role:"", title:"",
|
|
914
|
+
before:{focused:bFoc}, after:{focused:false}, error:"Cannot set focus: " + String(e)});
|
|
915
|
+
}
|
|
916
|
+
delay(0.05);
|
|
917
|
+
var pr = props(el);
|
|
918
|
+
return JSON.stringify({ok:pr.foc, elementId:p.elementId, role:pr.role,
|
|
919
|
+
title:pr.title||pr.value||"",
|
|
920
|
+
before:{focused:bFoc}, after:{focused:pr.foc},
|
|
921
|
+
error:pr.foc ? undefined : "Element does not report as focused after setting"});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// \u2500\u2500 SEARCH \u2500\u2500
|
|
925
|
+
if (p.op === "search") {
|
|
926
|
+
var results = [], q = (p.query || "").toLowerCase(), maxR = p.maxResults || 20;
|
|
927
|
+
function srch(el, id, depth) {
|
|
928
|
+
if (results.length >= maxR || depth > 20) return;
|
|
929
|
+
var pr = props(el);
|
|
930
|
+
var txt = (pr.title + " " + pr.value + " " + pr.desc).toLowerCase();
|
|
931
|
+
if (txt.indexOf(q) >= 0) {
|
|
932
|
+
if (!p.role || pr.role.toLowerCase() === p.role.toLowerCase()) {
|
|
933
|
+
results.push(toNode(id, pr));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
var kids = el.uiElements();
|
|
938
|
+
for (var i = 0; i < kids.length; i++) { srch(kids[i], id+"."+i, depth+1); }
|
|
939
|
+
} catch(e) {}
|
|
940
|
+
}
|
|
941
|
+
srch(proc, "0", 0);
|
|
942
|
+
return JSON.stringify({ok:true, elements:results});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return JSON.stringify({ok:false, error:"Unknown op: " + p.op});
|
|
946
|
+
} catch(e) {
|
|
947
|
+
return JSON.stringify({ok:false, error:String(e)});
|
|
948
|
+
}
|
|
949
|
+
}`;
|
|
950
|
+
MacOSDriver = class {
|
|
951
|
+
async screenshot(options) {
|
|
952
|
+
const args = [];
|
|
953
|
+
if (options.region) {
|
|
954
|
+
const { x, y, width, height } = options.region;
|
|
955
|
+
args.push("-R", `${x},${y},${width},${height}`);
|
|
956
|
+
} else if (options.windowId) {
|
|
957
|
+
args.push("-l", String(options.windowId));
|
|
958
|
+
}
|
|
959
|
+
return captureToBase64(args);
|
|
960
|
+
}
|
|
961
|
+
async mouseClick(options) {
|
|
962
|
+
await execHelper({
|
|
963
|
+
action: "click",
|
|
964
|
+
x: options.x,
|
|
965
|
+
y: options.y,
|
|
966
|
+
button: options.button || "left",
|
|
967
|
+
clicks: options.clicks || 1,
|
|
968
|
+
modifiers: options.modifiers
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
async mouseMove(options) {
|
|
972
|
+
await execHelper({ action: "move", x: options.x, y: options.y });
|
|
973
|
+
}
|
|
974
|
+
async mouseDrag(options) {
|
|
975
|
+
await execHelper({
|
|
976
|
+
action: "drag",
|
|
977
|
+
x: options.fromX,
|
|
978
|
+
y: options.fromY,
|
|
979
|
+
toX: options.toX,
|
|
980
|
+
toY: options.toY,
|
|
981
|
+
button: options.button || "left"
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
async mouseScroll(options) {
|
|
985
|
+
await execHelper({
|
|
986
|
+
action: "scroll",
|
|
987
|
+
x: options.x,
|
|
988
|
+
y: options.y,
|
|
989
|
+
deltaX: options.deltaX || 0,
|
|
990
|
+
deltaY: options.deltaY || 0
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
async mousePosition() {
|
|
994
|
+
const res = await execHelper({ action: "position" });
|
|
995
|
+
return { x: res.x, y: res.y };
|
|
996
|
+
}
|
|
997
|
+
async keyboardType(options) {
|
|
998
|
+
const escaped = options.text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
999
|
+
const script = `
|
|
1000
|
+
const se = Application("System Events");
|
|
1001
|
+
se.keystroke("${escaped}");
|
|
1002
|
+
`;
|
|
1003
|
+
await osascript(script);
|
|
1004
|
+
if (options.delay) {
|
|
1005
|
+
await new Promise((r) => setTimeout(r, options.delay));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async keyboardKey(options) {
|
|
1009
|
+
await execHelper({ action: "key", keys: options.keys });
|
|
1010
|
+
}
|
|
1011
|
+
async windowList() {
|
|
1012
|
+
const script = `
|
|
1013
|
+
ObjC.import("CoreGraphics");
|
|
1014
|
+
ObjC.import("Foundation");
|
|
1015
|
+
const kOnScreen = (1 << 0);
|
|
1016
|
+
const kExclDesk = (1 << 4);
|
|
1017
|
+
const raw = $.CGWindowListCopyWindowInfo(kOnScreen | kExclDesk, 0);
|
|
1018
|
+
const list = ObjC.unwrap(raw);
|
|
1019
|
+
const result = [];
|
|
1020
|
+
for (let i = 0; i < list.length; i++) {
|
|
1021
|
+
const w = list[i];
|
|
1022
|
+
const layer = w["kCGWindowLayer"];
|
|
1023
|
+
if (layer !== 0) continue;
|
|
1024
|
+
const owner = w["kCGWindowOwnerName"] || "";
|
|
1025
|
+
const name = w["kCGWindowName"];
|
|
1026
|
+
if (name === undefined || name === null) continue;
|
|
1027
|
+
const num = w["kCGWindowNumber"];
|
|
1028
|
+
const b = w["kCGWindowBounds"];
|
|
1029
|
+
result.push({
|
|
1030
|
+
id: num,
|
|
1031
|
+
app: owner,
|
|
1032
|
+
title: name || "",
|
|
1033
|
+
bounds: { x: b.X, y: b.Y, width: b.Width, height: b.Height },
|
|
1034
|
+
minimized: false,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
JSON.stringify(result);
|
|
1038
|
+
`;
|
|
1039
|
+
const out = await osascript(script);
|
|
1040
|
+
return JSON.parse(out.trim());
|
|
1041
|
+
}
|
|
1042
|
+
async windowFocus(windowId) {
|
|
1043
|
+
const windows = await this.windowList();
|
|
1044
|
+
const win = windows.find((w) => w.id === windowId);
|
|
1045
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
1046
|
+
const script = `
|
|
1047
|
+
const app = Application("${win.app}");
|
|
1048
|
+
app.activate();
|
|
1049
|
+
`;
|
|
1050
|
+
await osascript(script);
|
|
1051
|
+
}
|
|
1052
|
+
async windowResize(windowId, bounds) {
|
|
1053
|
+
const windows = await this.windowList();
|
|
1054
|
+
const win = windows.find((w) => w.id === windowId);
|
|
1055
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
1056
|
+
const parts = [];
|
|
1057
|
+
if (bounds.x !== void 0 || bounds.y !== void 0) {
|
|
1058
|
+
const x = bounds.x ?? win.bounds.x;
|
|
1059
|
+
const y = bounds.y ?? win.bounds.y;
|
|
1060
|
+
parts.push(`w.position = [${x}, ${y}];`);
|
|
1061
|
+
}
|
|
1062
|
+
if (bounds.width !== void 0 || bounds.height !== void 0) {
|
|
1063
|
+
const w = bounds.width ?? win.bounds.width;
|
|
1064
|
+
const h = bounds.height ?? win.bounds.height;
|
|
1065
|
+
parts.push(`w.size = [${w}, ${h}];`);
|
|
1066
|
+
}
|
|
1067
|
+
if (parts.length === 0) return;
|
|
1068
|
+
const title = win.title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1069
|
+
const script = `
|
|
1070
|
+
const se = Application("System Events");
|
|
1071
|
+
const proc = se.processes.byName("${win.app}");
|
|
1072
|
+
const wins = proc.windows();
|
|
1073
|
+
for (const w of wins) {
|
|
1074
|
+
try {
|
|
1075
|
+
const pos = w.position();
|
|
1076
|
+
if (w.title() === "${title}" && pos[0] === ${win.bounds.x} && pos[1] === ${win.bounds.y}) {
|
|
1077
|
+
${parts.join("\n ")}
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
} catch(e) {}
|
|
1081
|
+
}
|
|
1082
|
+
`;
|
|
1083
|
+
await osascript(script);
|
|
1084
|
+
}
|
|
1085
|
+
async windowClose(windowId) {
|
|
1086
|
+
const windows = await this.windowList();
|
|
1087
|
+
const win = windows.find((w) => w.id === windowId);
|
|
1088
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
1089
|
+
const title = win.title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1090
|
+
const script = `
|
|
1091
|
+
const se = Application("System Events");
|
|
1092
|
+
const proc = se.processes.byName("${win.app}");
|
|
1093
|
+
const wins = proc.windows();
|
|
1094
|
+
for (const w of wins) {
|
|
1095
|
+
try {
|
|
1096
|
+
const pos = w.position();
|
|
1097
|
+
if (w.title() === "${title}" && pos[0] === ${win.bounds.x} && pos[1] === ${win.bounds.y}) {
|
|
1098
|
+
w.buttons.whose({subrole: "AXCloseButton"})()[0].click();
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
} catch(e) {}
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
await osascript(script);
|
|
1105
|
+
}
|
|
1106
|
+
async windowMinimize(windowId) {
|
|
1107
|
+
const windows = await this.windowList();
|
|
1108
|
+
const win = windows.find((w) => w.id === windowId);
|
|
1109
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
1110
|
+
const title = win.title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1111
|
+
const script = `
|
|
1112
|
+
const se = Application("System Events");
|
|
1113
|
+
const proc = se.processes.byName("${win.app}");
|
|
1114
|
+
const wins = proc.windows();
|
|
1115
|
+
for (const w of wins) {
|
|
1116
|
+
try {
|
|
1117
|
+
const pos = w.position();
|
|
1118
|
+
if (w.title() === "${title}" && pos[0] === ${win.bounds.x} && pos[1] === ${win.bounds.y}) {
|
|
1119
|
+
w.minimized = true;
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
} catch(e) {}
|
|
1123
|
+
}
|
|
1124
|
+
`;
|
|
1125
|
+
await osascript(script);
|
|
1126
|
+
}
|
|
1127
|
+
async appLaunch(name) {
|
|
1128
|
+
await exec("open", ["-a", name]);
|
|
1129
|
+
}
|
|
1130
|
+
async appQuit(name) {
|
|
1131
|
+
const script = `
|
|
1132
|
+
try {
|
|
1133
|
+
const app = Application("${name}");
|
|
1134
|
+
app.quit();
|
|
1135
|
+
"ok";
|
|
1136
|
+
} catch(e) {
|
|
1137
|
+
"error: " + e.message;
|
|
1138
|
+
}
|
|
1139
|
+
`;
|
|
1140
|
+
const result = await osascript(script);
|
|
1141
|
+
if (result.trim().startsWith("error:")) {
|
|
1142
|
+
throw new Error(result.trim());
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async appList() {
|
|
1146
|
+
const script = `
|
|
1147
|
+
const se = Application("System Events");
|
|
1148
|
+
const procs = se.processes.whose({backgroundOnly: false})();
|
|
1149
|
+
const result = [];
|
|
1150
|
+
for (const proc of procs) {
|
|
1151
|
+
try {
|
|
1152
|
+
result.push({
|
|
1153
|
+
name: proc.name(),
|
|
1154
|
+
pid: proc.unixId(),
|
|
1155
|
+
bundleId: proc.bundleIdentifier() || undefined,
|
|
1156
|
+
});
|
|
1157
|
+
} catch(e) {}
|
|
1158
|
+
}
|
|
1159
|
+
JSON.stringify(result);
|
|
1160
|
+
`;
|
|
1161
|
+
const out = await osascript(script);
|
|
1162
|
+
return JSON.parse(out.trim());
|
|
1163
|
+
}
|
|
1164
|
+
async clipboardRead() {
|
|
1165
|
+
return exec("pbpaste", []);
|
|
1166
|
+
}
|
|
1167
|
+
async clipboardWrite(text) {
|
|
1168
|
+
await new Promise((resolve2, reject) => {
|
|
1169
|
+
const proc = spawn3("pbcopy", [], { stdio: ["pipe", "ignore", "pipe"] });
|
|
1170
|
+
proc.on("close", (code) => {
|
|
1171
|
+
if (code !== 0) reject(new Error(`pbcopy failed (${code})`));
|
|
1172
|
+
else resolve2();
|
|
1173
|
+
});
|
|
1174
|
+
proc.on("error", reject);
|
|
1175
|
+
proc.stdin.write(text);
|
|
1176
|
+
proc.stdin.end();
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
async screenInfo() {
|
|
1180
|
+
const script = `
|
|
1181
|
+
ObjC.import("AppKit");
|
|
1182
|
+
const screen = $.NSScreen.mainScreen;
|
|
1183
|
+
const frame = screen.frame;
|
|
1184
|
+
const scale = screen.backingScaleFactor;
|
|
1185
|
+
JSON.stringify({
|
|
1186
|
+
width: frame.size.width,
|
|
1187
|
+
height: frame.size.height,
|
|
1188
|
+
scaleFactor: scale,
|
|
1189
|
+
});
|
|
1190
|
+
`;
|
|
1191
|
+
const out = await osascript(script);
|
|
1192
|
+
return JSON.parse(out.trim());
|
|
1193
|
+
}
|
|
1194
|
+
async cursorImage(radius = 50) {
|
|
1195
|
+
const pos = await this.mousePosition();
|
|
1196
|
+
const x = Math.max(0, Math.round(pos.x - radius));
|
|
1197
|
+
const y = Math.max(0, Math.round(pos.y - radius));
|
|
1198
|
+
const size = radius * 2;
|
|
1199
|
+
return captureToBase64(["-R", `${x},${y},${size},${size}`]);
|
|
1200
|
+
}
|
|
1201
|
+
async axTree(options) {
|
|
1202
|
+
const res = await runAx({
|
|
1203
|
+
op: "tree",
|
|
1204
|
+
pid: options.pid || 0,
|
|
1205
|
+
maxDepth: options.maxDepth ?? 8
|
|
1206
|
+
});
|
|
1207
|
+
return { root: res.root, elementCount: res.elementCount };
|
|
1208
|
+
}
|
|
1209
|
+
async axAction(options) {
|
|
1210
|
+
return await runAx({
|
|
1211
|
+
op: "action",
|
|
1212
|
+
elementId: options.elementId,
|
|
1213
|
+
actionName: options.action,
|
|
1214
|
+
pid: options.pid || 0
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
async axSetValue(options) {
|
|
1218
|
+
return await runAx({
|
|
1219
|
+
op: "set_value",
|
|
1220
|
+
elementId: options.elementId,
|
|
1221
|
+
value: options.value,
|
|
1222
|
+
pid: options.pid || 0
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
async axFocus(options) {
|
|
1226
|
+
return await runAx({
|
|
1227
|
+
op: "focus",
|
|
1228
|
+
elementId: options.elementId,
|
|
1229
|
+
pid: options.pid || 0
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async axSearch(options) {
|
|
1233
|
+
const res = await runAx({
|
|
1234
|
+
op: "search",
|
|
1235
|
+
query: options.query,
|
|
1236
|
+
role: options.role,
|
|
1237
|
+
pid: options.pid || 0,
|
|
1238
|
+
maxResults: options.maxResults ?? 20
|
|
1239
|
+
});
|
|
1240
|
+
return { elements: res.elements || [] };
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// src/agent/capabilities/desktop/csharp-helper.ts
|
|
1247
|
+
import { spawn as spawn4 } from "child_process";
|
|
1248
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1249
|
+
import { join as join5 } from "path";
|
|
1250
|
+
import { homedir as homedir3 } from "os";
|
|
1251
|
+
async function ensureHelper2() {
|
|
1252
|
+
if (compiled2 && existsSync3(HELPER_PATH2)) return HELPER_PATH2;
|
|
1253
|
+
if (existsSync3(HELPER_PATH2)) {
|
|
1254
|
+
compiled2 = true;
|
|
1255
|
+
return HELPER_PATH2;
|
|
1256
|
+
}
|
|
1257
|
+
mkdirSync2(BIN_DIR2, { recursive: true });
|
|
1258
|
+
const srcPath = join5(BIN_DIR2, `desktop-helper-win-${HELPER_VERSION2}.cs`);
|
|
1259
|
+
writeFileSync2(srcPath, CSHARP_SOURCE);
|
|
1260
|
+
const cscPath = join5(
|
|
1261
|
+
process.env.WINDIR || "C:\\Windows",
|
|
1262
|
+
"Microsoft.NET",
|
|
1263
|
+
"Framework64",
|
|
1264
|
+
"v4.0.30319",
|
|
1265
|
+
"csc.exe"
|
|
1266
|
+
);
|
|
1267
|
+
await new Promise((resolve2, reject) => {
|
|
1268
|
+
const proc = spawn4(cscPath, [
|
|
1269
|
+
"/nologo",
|
|
1270
|
+
"/optimize+",
|
|
1271
|
+
`/out:${HELPER_PATH2}`,
|
|
1272
|
+
"/r:System.Windows.Forms.dll",
|
|
1273
|
+
"/r:System.Drawing.dll",
|
|
1274
|
+
"/r:UIAutomationClient.dll",
|
|
1275
|
+
"/r:UIAutomationTypes.dll",
|
|
1276
|
+
"/r:WindowsBase.dll",
|
|
1277
|
+
"/r:PresentationCore.dll",
|
|
1278
|
+
srcPath
|
|
1279
|
+
], {
|
|
1280
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1281
|
+
});
|
|
1282
|
+
let stderr = "";
|
|
1283
|
+
proc.stderr?.on("data", (d) => {
|
|
1284
|
+
stderr += d.toString();
|
|
1285
|
+
});
|
|
1286
|
+
proc.on("close", (code) => {
|
|
1287
|
+
if (code === 0) {
|
|
1288
|
+
compiled2 = true;
|
|
1289
|
+
resolve2();
|
|
1290
|
+
} else {
|
|
1291
|
+
reject(new Error(`csc.exe failed (exit ${code}): ${stderr}`));
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
proc.on("error", (err) => {
|
|
1295
|
+
reject(new Error(`csc.exe not found: ${err.message}. Ensure .NET Framework 4.x is installed.`));
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
return HELPER_PATH2;
|
|
1299
|
+
}
|
|
1300
|
+
async function execHelper2(request) {
|
|
1301
|
+
const helperPath = await ensureHelper2();
|
|
1302
|
+
return new Promise((resolve2, reject) => {
|
|
1303
|
+
const proc = spawn4(helperPath, [], {
|
|
1304
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1305
|
+
});
|
|
1306
|
+
let stdout = "";
|
|
1307
|
+
let stderr = "";
|
|
1308
|
+
proc.stdout.on("data", (d) => {
|
|
1309
|
+
stdout += d.toString();
|
|
1310
|
+
});
|
|
1311
|
+
proc.stderr.on("data", (d) => {
|
|
1312
|
+
stderr += d.toString();
|
|
1313
|
+
});
|
|
1314
|
+
proc.on("close", (code) => {
|
|
1315
|
+
if (code !== 0) {
|
|
1316
|
+
reject(new Error(`Helper failed (exit ${code}): ${stderr}`));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
const response = JSON.parse(stdout.trim());
|
|
1321
|
+
if (!response.ok && response.error) {
|
|
1322
|
+
reject(new Error(response.error));
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
resolve2(response);
|
|
1326
|
+
} catch {
|
|
1327
|
+
reject(new Error(`Invalid helper output: ${stdout}`));
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
proc.on("error", reject);
|
|
1331
|
+
proc.stdin.write(JSON.stringify(request));
|
|
1332
|
+
proc.stdin.end();
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
var HELPER_VERSION2, BIN_DIR2, HELPER_PATH2, CSHARP_SOURCE, compiled2;
|
|
1336
|
+
var init_csharp_helper = __esm({
|
|
1337
|
+
"src/agent/capabilities/desktop/csharp-helper.ts"() {
|
|
1338
|
+
"use strict";
|
|
1339
|
+
HELPER_VERSION2 = "v1";
|
|
1340
|
+
BIN_DIR2 = join5(homedir3(), ".agent-tunnel", "bin");
|
|
1341
|
+
HELPER_PATH2 = join5(BIN_DIR2, `desktop-helper-win-${HELPER_VERSION2}.exe`);
|
|
1342
|
+
CSHARP_SOURCE = `
|
|
1343
|
+
using System;
|
|
1344
|
+
using System.Collections.Generic;
|
|
1345
|
+
using System.Diagnostics;
|
|
1346
|
+
using System.Drawing;
|
|
1347
|
+
using System.Drawing.Imaging;
|
|
1348
|
+
using System.IO;
|
|
1349
|
+
using System.Linq;
|
|
1350
|
+
using System.Runtime.InteropServices;
|
|
1351
|
+
using System.Text;
|
|
1352
|
+
using System.Threading;
|
|
1353
|
+
using System.Windows.Automation;
|
|
1354
|
+
using System.Windows.Forms;
|
|
1355
|
+
|
|
1356
|
+
class Helper
|
|
1357
|
+
{
|
|
1358
|
+
// \u2500\u2500\u2500 P/Invoke \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1359
|
+
[DllImport("user32.dll")] static extern bool SetCursorPos(int X, int Y);
|
|
1360
|
+
[DllImport("user32.dll")] static extern bool GetCursorPos(out POINT lpPoint);
|
|
1361
|
+
[DllImport("user32.dll")] static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
|
1362
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
1363
|
+
[DllImport("user32.dll")] static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
1364
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
1365
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
|
|
1366
|
+
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
1367
|
+
[DllImport("user32.dll")] static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
|
1368
|
+
[DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
1369
|
+
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
|
|
1370
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
|
1371
|
+
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hWnd);
|
|
1372
|
+
|
|
1373
|
+
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
1374
|
+
|
|
1375
|
+
const int SW_MINIMIZE = 6;
|
|
1376
|
+
const uint WM_CLOSE = 0x0010;
|
|
1377
|
+
const int INPUT_MOUSE = 0;
|
|
1378
|
+
const int INPUT_KEYBOARD = 1;
|
|
1379
|
+
const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
|
|
1380
|
+
const uint MOUSEEVENTF_LEFTUP = 0x0004;
|
|
1381
|
+
const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
|
|
1382
|
+
const uint MOUSEEVENTF_RIGHTUP = 0x0010;
|
|
1383
|
+
const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
|
|
1384
|
+
const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
|
|
1385
|
+
const uint MOUSEEVENTF_WHEEL = 0x0800;
|
|
1386
|
+
const uint MOUSEEVENTF_HWHEEL = 0x1000;
|
|
1387
|
+
const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
|
|
1388
|
+
const uint MOUSEEVENTF_MOVE = 0x0001;
|
|
1389
|
+
const uint KEYEVENTF_KEYUP = 0x0002;
|
|
1390
|
+
const uint KEYEVENTF_UNICODE = 0x0004;
|
|
1391
|
+
|
|
1392
|
+
[StructLayout(LayoutKind.Sequential)] struct POINT { public int X; public int Y; }
|
|
1393
|
+
[StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; }
|
|
1394
|
+
|
|
1395
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
1396
|
+
struct INPUT { public int type; public INPUTUNION u; }
|
|
1397
|
+
|
|
1398
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
1399
|
+
struct INPUTUNION
|
|
1400
|
+
{
|
|
1401
|
+
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
1402
|
+
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
1406
|
+
struct MOUSEINPUT { public int dx, dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
|
1407
|
+
|
|
1408
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
1409
|
+
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
|
1410
|
+
|
|
1411
|
+
// \u2500\u2500\u2500 JSON helpers (minimal, no dependencies) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1412
|
+
static string JsonStr(string s) => "\\"" + s.Replace("\\\\", "\\\\\\\\").Replace("\\"", "\\\\\\"").Replace("\\n", "\\\\n").Replace("\\r", "\\\\r").Replace("\\t", "\\\\t") + "\\"";
|
|
1413
|
+
|
|
1414
|
+
static Dictionary<string, object> ParseJson(string json)
|
|
1415
|
+
{
|
|
1416
|
+
var d = new Dictionary<string, object>();
|
|
1417
|
+
json = json.Trim();
|
|
1418
|
+
if (json.StartsWith("{")) json = json.Substring(1, json.Length - 2).Trim();
|
|
1419
|
+
|
|
1420
|
+
int i = 0;
|
|
1421
|
+
while (i < json.Length)
|
|
1422
|
+
{
|
|
1423
|
+
while (i < json.Length && (json[i] == ',' || json[i] == ' ' || json[i] == '\\n' || json[i] == '\\r' || json[i] == '\\t')) i++;
|
|
1424
|
+
if (i >= json.Length) break;
|
|
1425
|
+
|
|
1426
|
+
var key = ParseJsonString(json, ref i);
|
|
1427
|
+
while (i < json.Length && (json[i] == ' ' || json[i] == ':')) i++;
|
|
1428
|
+
var val = ParseJsonValue(json, ref i);
|
|
1429
|
+
d[key] = val;
|
|
1430
|
+
}
|
|
1431
|
+
return d;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
static string ParseJsonString(string json, ref int i)
|
|
1435
|
+
{
|
|
1436
|
+
if (json[i] != '\\"') throw new Exception("Expected string at " + i);
|
|
1437
|
+
i++;
|
|
1438
|
+
var sb = new StringBuilder();
|
|
1439
|
+
while (i < json.Length && json[i] != '\\"')
|
|
1440
|
+
{
|
|
1441
|
+
if (json[i] == '\\\\') { i++; sb.Append(json[i]); }
|
|
1442
|
+
else sb.Append(json[i]);
|
|
1443
|
+
i++;
|
|
1444
|
+
}
|
|
1445
|
+
i++; // skip closing quote
|
|
1446
|
+
return sb.ToString();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
static object ParseJsonValue(string json, ref int i)
|
|
1450
|
+
{
|
|
1451
|
+
while (i < json.Length && json[i] == ' ') i++;
|
|
1452
|
+
if (i >= json.Length) return null;
|
|
1453
|
+
|
|
1454
|
+
if (json[i] == '\\"') return ParseJsonString(json, ref i);
|
|
1455
|
+
if (json[i] == '[')
|
|
1456
|
+
{
|
|
1457
|
+
i++;
|
|
1458
|
+
var list = new List<object>();
|
|
1459
|
+
while (i < json.Length && json[i] != ']')
|
|
1460
|
+
{
|
|
1461
|
+
while (i < json.Length && (json[i] == ',' || json[i] == ' ' || json[i] == '\\n' || json[i] == '\\r' || json[i] == '\\t')) i++;
|
|
1462
|
+
if (i < json.Length && json[i] != ']')
|
|
1463
|
+
list.Add(ParseJsonValue(json, ref i));
|
|
1464
|
+
}
|
|
1465
|
+
if (i < json.Length) i++;
|
|
1466
|
+
return list;
|
|
1467
|
+
}
|
|
1468
|
+
if (json[i] == '{')
|
|
1469
|
+
{
|
|
1470
|
+
var start = i;
|
|
1471
|
+
int depth = 1; i++;
|
|
1472
|
+
while (i < json.Length && depth > 0) { if (json[i] == '{') depth++; if (json[i] == '}') depth--; i++; }
|
|
1473
|
+
return json.Substring(start, i - start);
|
|
1474
|
+
}
|
|
1475
|
+
if (json[i] == 'n' && json.Substring(i, 4) == "null") { i += 4; return null; }
|
|
1476
|
+
if (json[i] == 't' && json.Substring(i, 4) == "true") { i += 4; return true; }
|
|
1477
|
+
if (json[i] == 'f' && json.Substring(i, 5) == "false") { i += 5; return false; }
|
|
1478
|
+
|
|
1479
|
+
// number
|
|
1480
|
+
var numStart = i;
|
|
1481
|
+
while (i < json.Length && (char.IsDigit(json[i]) || json[i] == '.' || json[i] == '-' || json[i] == 'e' || json[i] == 'E' || json[i] == '+')) i++;
|
|
1482
|
+
var numStr = json.Substring(numStart, i - numStart);
|
|
1483
|
+
if (numStr.Contains(".")) return double.Parse(numStr, System.Globalization.CultureInfo.InvariantCulture);
|
|
1484
|
+
return int.Parse(numStr);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
static int GetInt(Dictionary<string, object> d, string k, int def = 0) { return d.ContainsKey(k) && d[k] != null ? Convert.ToInt32(d[k]) : def; }
|
|
1488
|
+
static double GetDbl(Dictionary<string, object> d, string k, double def = 0) { return d.ContainsKey(k) && d[k] != null ? Convert.ToDouble(d[k]) : def; }
|
|
1489
|
+
static string GetStr(Dictionary<string, object> d, string k, string def = "") { return d.ContainsKey(k) && d[k] is string ? (string)d[k] : def; }
|
|
1490
|
+
static List<object> GetList(Dictionary<string, object> d, string k) { return d.ContainsKey(k) && d[k] is List<object> ? (List<object>)d[k] : new List<object>(); }
|
|
1491
|
+
|
|
1492
|
+
// \u2500\u2500\u2500 Virtual key codes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1493
|
+
static Dictionary<string, ushort> VKMap = new Dictionary<string, ushort>(StringComparer.OrdinalIgnoreCase)
|
|
1494
|
+
{
|
|
1495
|
+
{"return", 0x0D}, {"enter", 0x0D}, {"tab", 0x09}, {"space", 0x20},
|
|
1496
|
+
{"backspace", 0x08}, {"delete", 0x2E}, {"escape", 0x1B}, {"esc", 0x1B},
|
|
1497
|
+
{"up", 0x26}, {"down", 0x28}, {"left", 0x25}, {"right", 0x27},
|
|
1498
|
+
{"home", 0x24}, {"end", 0x23}, {"pageup", 0x21}, {"pagedown", 0x22},
|
|
1499
|
+
{"f1", 0x70}, {"f2", 0x71}, {"f3", 0x72}, {"f4", 0x73},
|
|
1500
|
+
{"f5", 0x74}, {"f6", 0x75}, {"f7", 0x76}, {"f8", 0x77},
|
|
1501
|
+
{"f9", 0x78}, {"f10", 0x79}, {"f11", 0x7A}, {"f12", 0x7B},
|
|
1502
|
+
{"shift", 0x10}, {"ctrl", 0x11}, {"control", 0x11},
|
|
1503
|
+
{"alt", 0x12}, {"option", 0x12}, {"cmd", 0x5B}, {"command", 0x5B},
|
|
1504
|
+
{"a", 0x41}, {"b", 0x42}, {"c", 0x43}, {"d", 0x44}, {"e", 0x45}, {"f", 0x46},
|
|
1505
|
+
{"g", 0x47}, {"h", 0x48}, {"i", 0x49}, {"j", 0x4A}, {"k", 0x4B}, {"l", 0x4C},
|
|
1506
|
+
{"m", 0x4D}, {"n", 0x4E}, {"o", 0x4F}, {"p", 0x50}, {"q", 0x51}, {"r", 0x52},
|
|
1507
|
+
{"s", 0x53}, {"t", 0x54}, {"u", 0x55}, {"v", 0x56}, {"w", 0x57}, {"x", 0x58},
|
|
1508
|
+
{"y", 0x59}, {"z", 0x5A},
|
|
1509
|
+
{"0", 0x30}, {"1", 0x31}, {"2", 0x32}, {"3", 0x33}, {"4", 0x34},
|
|
1510
|
+
{"5", 0x35}, {"6", 0x36}, {"7", 0x37}, {"8", 0x38}, {"9", 0x39},
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
static ushort GetVK(string key)
|
|
1514
|
+
{
|
|
1515
|
+
ushort vk;
|
|
1516
|
+
if (VKMap.TryGetValue(key, out vk)) return vk;
|
|
1517
|
+
if (key.Length == 1) return (ushort)char.ToUpper(key[0]);
|
|
1518
|
+
return 0;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
static bool IsModifier(string key)
|
|
1522
|
+
{
|
|
1523
|
+
var k = key.ToLower();
|
|
1524
|
+
return k == "shift" || k == "ctrl" || k == "control" || k == "alt" || k == "option" || k == "cmd" || k == "command";
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// \u2500\u2500\u2500 Mouse helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1528
|
+
static void SendMouseClick(int x, int y, string button, int clicks)
|
|
1529
|
+
{
|
|
1530
|
+
SetCursorPos(x, y);
|
|
1531
|
+
Thread.Sleep(10);
|
|
1532
|
+
|
|
1533
|
+
uint downFlag, upFlag;
|
|
1534
|
+
switch (button)
|
|
1535
|
+
{
|
|
1536
|
+
case "right": downFlag = MOUSEEVENTF_RIGHTDOWN; upFlag = MOUSEEVENTF_RIGHTUP; break;
|
|
1537
|
+
case "middle": downFlag = MOUSEEVENTF_MIDDLEDOWN; upFlag = MOUSEEVENTF_MIDDLEUP; break;
|
|
1538
|
+
default: downFlag = MOUSEEVENTF_LEFTDOWN; upFlag = MOUSEEVENTF_LEFTUP; break;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
for (int c = 0; c < clicks; c++)
|
|
1542
|
+
{
|
|
1543
|
+
var inputs = new INPUT[]
|
|
1544
|
+
{
|
|
1545
|
+
new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = downFlag } } },
|
|
1546
|
+
new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = upFlag } } },
|
|
1547
|
+
};
|
|
1548
|
+
SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT)));
|
|
1549
|
+
if (c < clicks - 1) Thread.Sleep(50);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// \u2500\u2500\u2500 AX helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1554
|
+
static int axElementCount;
|
|
1555
|
+
|
|
1556
|
+
static string WalkAXTree(AutomationElement el, int depth, int maxDepth, List<string> roles, string pathPrefix)
|
|
1557
|
+
{
|
|
1558
|
+
if (el == null || depth > maxDepth) return "null";
|
|
1559
|
+
axElementCount++;
|
|
1560
|
+
|
|
1561
|
+
string role = "";
|
|
1562
|
+
string name = "";
|
|
1563
|
+
string val = "";
|
|
1564
|
+
string desc = "";
|
|
1565
|
+
var bounds = System.Windows.Rect.Empty;
|
|
1566
|
+
bool enabled = true;
|
|
1567
|
+
bool focused = false;
|
|
1568
|
+
var actionList = new List<string>();
|
|
1569
|
+
|
|
1570
|
+
try { role = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); } catch {}
|
|
1571
|
+
try { name = el.Current.Name ?? ""; } catch {}
|
|
1572
|
+
try { val = el.Current.AutomationId ?? ""; } catch {}
|
|
1573
|
+
try { desc = el.Current.HelpText ?? ""; } catch {}
|
|
1574
|
+
try { bounds = el.Current.BoundingRectangle; } catch {}
|
|
1575
|
+
try { enabled = el.Current.IsEnabled; } catch {}
|
|
1576
|
+
try { focused = el.Current.HasKeyboardFocus; } catch {}
|
|
1577
|
+
|
|
1578
|
+
// Check supported patterns for actions
|
|
1579
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) actionList.Add("invoke"); } catch {}
|
|
1580
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) actionList.Add("toggle"); } catch {}
|
|
1581
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) actionList.Add("expandcollapse"); } catch {}
|
|
1582
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) actionList.Add("setvalue"); } catch {}
|
|
1583
|
+
|
|
1584
|
+
if (roles != null && roles.Count > 0 && !roles.Contains(role.ToLower()))
|
|
1585
|
+
{
|
|
1586
|
+
// Skip this element but still walk children
|
|
1587
|
+
var sb2 = new StringBuilder();
|
|
1588
|
+
bool first2 = true;
|
|
1589
|
+
int childIdx = 0;
|
|
1590
|
+
try
|
|
1591
|
+
{
|
|
1592
|
+
var walker = TreeWalker.ControlViewWalker;
|
|
1593
|
+
var child = walker.GetFirstChild(el);
|
|
1594
|
+
while (child != null)
|
|
1595
|
+
{
|
|
1596
|
+
var childPath = pathPrefix.Length > 0 ? pathPrefix + "." + childIdx : childIdx.ToString();
|
|
1597
|
+
var childJson = WalkAXTree(child, depth, maxDepth, roles, childPath);
|
|
1598
|
+
if (childJson != "null")
|
|
1599
|
+
{
|
|
1600
|
+
if (!first2) sb2.Append(",");
|
|
1601
|
+
sb2.Append(childJson);
|
|
1602
|
+
first2 = false;
|
|
1603
|
+
}
|
|
1604
|
+
child = walker.GetNextSibling(child);
|
|
1605
|
+
childIdx++;
|
|
1606
|
+
}
|
|
1607
|
+
} catch {}
|
|
1608
|
+
if (sb2.Length == 0) return "null";
|
|
1609
|
+
return sb2.ToString();
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
var sb = new StringBuilder();
|
|
1613
|
+
sb.Append("{");
|
|
1614
|
+
sb.AppendFormat("\\"id\\":{0}", JsonStr(pathPrefix));
|
|
1615
|
+
sb.AppendFormat(",\\"role\\":{0}", JsonStr(role));
|
|
1616
|
+
sb.AppendFormat(",\\"title\\":{0}", JsonStr(name));
|
|
1617
|
+
sb.AppendFormat(",\\"value\\":{0}", JsonStr(val));
|
|
1618
|
+
sb.AppendFormat(",\\"description\\":{0}", JsonStr(desc));
|
|
1619
|
+
sb.AppendFormat(",\\"bounds\\":{{\\"x\\":{0},\\"y\\":{1},\\"width\\":{2},\\"height\\":{3}}}",
|
|
1620
|
+
bounds.IsEmpty ? 0 : (int)bounds.X,
|
|
1621
|
+
bounds.IsEmpty ? 0 : (int)bounds.Y,
|
|
1622
|
+
bounds.IsEmpty ? 0 : (int)bounds.Width,
|
|
1623
|
+
bounds.IsEmpty ? 0 : (int)bounds.Height);
|
|
1624
|
+
sb.AppendFormat(",\\"enabled\\":{0}", enabled ? "true" : "false");
|
|
1625
|
+
sb.AppendFormat(",\\"focused\\":{0}", focused ? "true" : "false");
|
|
1626
|
+
sb.Append(",\\"actions\\":[");
|
|
1627
|
+
for (int a = 0; a < actionList.Count; a++) { if (a > 0) sb.Append(","); sb.Append(JsonStr(actionList[a])); }
|
|
1628
|
+
sb.Append("]");
|
|
1629
|
+
|
|
1630
|
+
// Children
|
|
1631
|
+
sb.Append(",\\"children\\":[");
|
|
1632
|
+
if (depth < maxDepth)
|
|
1633
|
+
{
|
|
1634
|
+
bool first = true;
|
|
1635
|
+
int childIdx = 0;
|
|
1636
|
+
try
|
|
1637
|
+
{
|
|
1638
|
+
var walker = TreeWalker.ControlViewWalker;
|
|
1639
|
+
var child = walker.GetFirstChild(el);
|
|
1640
|
+
while (child != null)
|
|
1641
|
+
{
|
|
1642
|
+
var childPath = pathPrefix.Length > 0 ? pathPrefix + "." + childIdx : childIdx.ToString();
|
|
1643
|
+
var childJson = WalkAXTree(child, depth + 1, maxDepth, roles, childPath);
|
|
1644
|
+
if (childJson != "null")
|
|
1645
|
+
{
|
|
1646
|
+
if (!first) sb.Append(",");
|
|
1647
|
+
sb.Append(childJson);
|
|
1648
|
+
first = false;
|
|
1649
|
+
}
|
|
1650
|
+
child = walker.GetNextSibling(child);
|
|
1651
|
+
childIdx++;
|
|
1652
|
+
}
|
|
1653
|
+
} catch {}
|
|
1654
|
+
}
|
|
1655
|
+
sb.Append("]");
|
|
1656
|
+
sb.Append("}");
|
|
1657
|
+
return sb.ToString();
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
static AutomationElement NavigateToElement(AutomationElement root, string elementId)
|
|
1661
|
+
{
|
|
1662
|
+
var parts = elementId.Split('.');
|
|
1663
|
+
var current = root;
|
|
1664
|
+
|
|
1665
|
+
foreach (var part in parts)
|
|
1666
|
+
{
|
|
1667
|
+
int idx = int.Parse(part);
|
|
1668
|
+
var walker = TreeWalker.ControlViewWalker;
|
|
1669
|
+
var child = walker.GetFirstChild(current);
|
|
1670
|
+
for (int i = 0; i < idx && child != null; i++)
|
|
1671
|
+
child = walker.GetNextSibling(child);
|
|
1672
|
+
if (child == null) throw new Exception("Element not found at path: " + elementId);
|
|
1673
|
+
current = child;
|
|
1674
|
+
}
|
|
1675
|
+
return current;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
static void SearchAXTree(AutomationElement el, string query, string roleFilter, int maxResults, List<string> results, string pathPrefix, int depth, int maxDepth)
|
|
1679
|
+
{
|
|
1680
|
+
if (el == null || results.Count >= maxResults || depth > maxDepth) return;
|
|
1681
|
+
|
|
1682
|
+
string role = "";
|
|
1683
|
+
string name = "";
|
|
1684
|
+
string val = "";
|
|
1685
|
+
string desc = "";
|
|
1686
|
+
var bounds = System.Windows.Rect.Empty;
|
|
1687
|
+
bool enabled = true;
|
|
1688
|
+
bool focused = false;
|
|
1689
|
+
var actionList = new List<string>();
|
|
1690
|
+
|
|
1691
|
+
try { role = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); } catch {}
|
|
1692
|
+
try { name = el.Current.Name ?? ""; } catch {}
|
|
1693
|
+
try { val = el.Current.AutomationId ?? ""; } catch {}
|
|
1694
|
+
try { desc = el.Current.HelpText ?? ""; } catch {}
|
|
1695
|
+
try { bounds = el.Current.BoundingRectangle; } catch {}
|
|
1696
|
+
try { enabled = el.Current.IsEnabled; } catch {}
|
|
1697
|
+
try { focused = el.Current.HasKeyboardFocus; } catch {}
|
|
1698
|
+
|
|
1699
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) actionList.Add("invoke"); } catch {}
|
|
1700
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) actionList.Add("toggle"); } catch {}
|
|
1701
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) actionList.Add("expandcollapse"); } catch {}
|
|
1702
|
+
try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) actionList.Add("setvalue"); } catch {}
|
|
1703
|
+
|
|
1704
|
+
var queryLower = query.ToLower();
|
|
1705
|
+
bool match = name.ToLower().Contains(queryLower) || val.ToLower().Contains(queryLower) || desc.ToLower().Contains(queryLower);
|
|
1706
|
+
|
|
1707
|
+
if (roleFilter != null && roleFilter.Length > 0 && role.ToLower() != roleFilter.ToLower())
|
|
1708
|
+
match = false;
|
|
1709
|
+
|
|
1710
|
+
if (match)
|
|
1711
|
+
{
|
|
1712
|
+
var sb = new StringBuilder();
|
|
1713
|
+
sb.Append("{");
|
|
1714
|
+
sb.AppendFormat("\\"id\\":{0}", JsonStr(pathPrefix));
|
|
1715
|
+
sb.AppendFormat(",\\"role\\":{0}", JsonStr(role));
|
|
1716
|
+
sb.AppendFormat(",\\"title\\":{0}", JsonStr(name));
|
|
1717
|
+
sb.AppendFormat(",\\"value\\":{0}", JsonStr(val));
|
|
1718
|
+
sb.AppendFormat(",\\"description\\":{0}", JsonStr(desc));
|
|
1719
|
+
sb.AppendFormat(",\\"bounds\\":{{\\"x\\":{0},\\"y\\":{1},\\"width\\":{2},\\"height\\":{3}}}",
|
|
1720
|
+
bounds.IsEmpty ? 0 : (int)bounds.X, bounds.IsEmpty ? 0 : (int)bounds.Y,
|
|
1721
|
+
bounds.IsEmpty ? 0 : (int)bounds.Width, bounds.IsEmpty ? 0 : (int)bounds.Height);
|
|
1722
|
+
sb.AppendFormat(",\\"enabled\\":{0}", enabled ? "true" : "false");
|
|
1723
|
+
sb.AppendFormat(",\\"focused\\":{0}", focused ? "true" : "false");
|
|
1724
|
+
sb.Append(",\\"actions\\":[");
|
|
1725
|
+
for (int a = 0; a < actionList.Count; a++) { if (a > 0) sb.Append(","); sb.Append(JsonStr(actionList[a])); }
|
|
1726
|
+
sb.Append("],\\"children\\":[]}");
|
|
1727
|
+
results.Add(sb.ToString());
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
int childIdx = 0;
|
|
1731
|
+
try
|
|
1732
|
+
{
|
|
1733
|
+
var walker = TreeWalker.ControlViewWalker;
|
|
1734
|
+
var child = walker.GetFirstChild(el);
|
|
1735
|
+
while (child != null && results.Count < maxResults)
|
|
1736
|
+
{
|
|
1737
|
+
var childPath = pathPrefix.Length > 0 ? pathPrefix + "." + childIdx : childIdx.ToString();
|
|
1738
|
+
SearchAXTree(child, query, roleFilter, maxResults, results, childPath, depth + 1, maxDepth);
|
|
1739
|
+
child = walker.GetNextSibling(child);
|
|
1740
|
+
childIdx++;
|
|
1741
|
+
}
|
|
1742
|
+
} catch {}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
static AutomationElement FindAppRoot(int pid)
|
|
1746
|
+
{
|
|
1747
|
+
if (pid <= 0) return AutomationElement.RootElement;
|
|
1748
|
+
|
|
1749
|
+
var cond = new PropertyCondition(AutomationElement.ProcessIdProperty, pid);
|
|
1750
|
+
var el = AutomationElement.RootElement.FindFirst(TreeScope.Children, cond);
|
|
1751
|
+
if (el == null) throw new Exception("No UI Automation element found for PID " + pid);
|
|
1752
|
+
return el;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// \u2500\u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1756
|
+
[STAThread]
|
|
1757
|
+
static void Main()
|
|
1758
|
+
{
|
|
1759
|
+
var input = Console.In.ReadToEnd().Trim();
|
|
1760
|
+
Dictionary<string, object> req;
|
|
1761
|
+
try { req = ParseJson(input); }
|
|
1762
|
+
catch (Exception ex) { Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr("Invalid JSON: " + ex.Message) + "}"); return; }
|
|
1763
|
+
|
|
1764
|
+
var action = GetStr(req, "action");
|
|
1765
|
+
|
|
1766
|
+
try
|
|
1767
|
+
{
|
|
1768
|
+
switch (action)
|
|
1769
|
+
{
|
|
1770
|
+
case "click":
|
|
1771
|
+
{
|
|
1772
|
+
var x = (int)GetDbl(req, "x");
|
|
1773
|
+
var y = (int)GetDbl(req, "y");
|
|
1774
|
+
var button = GetStr(req, "button", "left");
|
|
1775
|
+
var clicks = GetInt(req, "clicks", 1);
|
|
1776
|
+
SendMouseClick(x, y, button, clicks);
|
|
1777
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
case "move":
|
|
1781
|
+
{
|
|
1782
|
+
SetCursorPos((int)GetDbl(req, "x"), (int)GetDbl(req, "y"));
|
|
1783
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
case "drag":
|
|
1787
|
+
{
|
|
1788
|
+
int fx = (int)GetDbl(req, "x"), fy = (int)GetDbl(req, "y");
|
|
1789
|
+
int tx = (int)GetDbl(req, "toX"), ty = (int)GetDbl(req, "toY");
|
|
1790
|
+
SetCursorPos(fx, fy);
|
|
1791
|
+
Thread.Sleep(50);
|
|
1792
|
+
var down = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTDOWN } } } };
|
|
1793
|
+
SendInput(1, down, Marshal.SizeOf(typeof(INPUT)));
|
|
1794
|
+
|
|
1795
|
+
for (int i = 1; i <= 10; i++)
|
|
1796
|
+
{
|
|
1797
|
+
double t = i / 10.0;
|
|
1798
|
+
int mx = fx + (int)((tx - fx) * t);
|
|
1799
|
+
int my = fy + (int)((ty - fy) * t);
|
|
1800
|
+
SetCursorPos(mx, my);
|
|
1801
|
+
Thread.Sleep(10);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
var up = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTUP } } } };
|
|
1805
|
+
SendInput(1, up, Marshal.SizeOf(typeof(INPUT)));
|
|
1806
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
case "scroll":
|
|
1810
|
+
{
|
|
1811
|
+
SetCursorPos((int)GetDbl(req, "x"), (int)GetDbl(req, "y"));
|
|
1812
|
+
Thread.Sleep(10);
|
|
1813
|
+
int dy = GetInt(req, "deltaY");
|
|
1814
|
+
int dx = GetInt(req, "deltaX");
|
|
1815
|
+
if (dy != 0)
|
|
1816
|
+
{
|
|
1817
|
+
var inputs = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { mouseData = (uint)(dy * 120), dwFlags = MOUSEEVENTF_WHEEL } } } };
|
|
1818
|
+
SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
|
|
1819
|
+
}
|
|
1820
|
+
if (dx != 0)
|
|
1821
|
+
{
|
|
1822
|
+
var inputs = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { mouseData = (uint)(dx * 120), dwFlags = MOUSEEVENTF_HWHEEL } } } };
|
|
1823
|
+
SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
|
|
1824
|
+
}
|
|
1825
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
case "position":
|
|
1829
|
+
{
|
|
1830
|
+
POINT p;
|
|
1831
|
+
GetCursorPos(out p);
|
|
1832
|
+
Console.WriteLine("{\\"ok\\":true,\\"x\\":" + p.X + ",\\"y\\":" + p.Y + "}");
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1835
|
+
case "key":
|
|
1836
|
+
{
|
|
1837
|
+
var keys = GetList(req, "keys");
|
|
1838
|
+
var mods = new List<ushort>();
|
|
1839
|
+
var mainKeys = new List<ushort>();
|
|
1840
|
+
|
|
1841
|
+
foreach (var k in keys)
|
|
1842
|
+
{
|
|
1843
|
+
var keyStr = k.ToString();
|
|
1844
|
+
var vk = GetVK(keyStr);
|
|
1845
|
+
if (vk == 0) { Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr("Unknown key: " + keyStr) + "}"); return; }
|
|
1846
|
+
if (IsModifier(keyStr)) mods.Add(vk); else mainKeys.Add(vk);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
var inputList = new List<INPUT>();
|
|
1850
|
+
foreach (var m in mods) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = m } } });
|
|
1851
|
+
foreach (var k in mainKeys) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = k } } });
|
|
1852
|
+
foreach (var k in mainKeys) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = k, dwFlags = KEYEVENTF_KEYUP } } });
|
|
1853
|
+
foreach (var m in mods) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = m, dwFlags = KEYEVENTF_KEYUP } } });
|
|
1854
|
+
|
|
1855
|
+
SendInput((uint)inputList.Count, inputList.ToArray(), Marshal.SizeOf(typeof(INPUT)));
|
|
1856
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
case "type":
|
|
1860
|
+
{
|
|
1861
|
+
var text = GetStr(req, "text");
|
|
1862
|
+
var inputList = new List<INPUT>();
|
|
1863
|
+
foreach (char c in text)
|
|
1864
|
+
{
|
|
1865
|
+
inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wScan = (ushort)c, dwFlags = KEYEVENTF_UNICODE } } });
|
|
1866
|
+
inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wScan = (ushort)c, dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP } } });
|
|
1867
|
+
}
|
|
1868
|
+
SendInput((uint)inputList.Count, inputList.ToArray(), Marshal.SizeOf(typeof(INPUT)));
|
|
1869
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
case "screenshot":
|
|
1873
|
+
{
|
|
1874
|
+
var bounds = Screen.PrimaryScreen.Bounds;
|
|
1875
|
+
using (var bmp = new Bitmap(bounds.Width, bounds.Height))
|
|
1876
|
+
using (var g = Graphics.FromImage(bmp))
|
|
1877
|
+
{
|
|
1878
|
+
g.CopyFromScreen(bounds.Location, System.Drawing.Point.Empty, bounds.Size);
|
|
1879
|
+
|
|
1880
|
+
// Downscale if > 1920
|
|
1881
|
+
Bitmap output = bmp;
|
|
1882
|
+
bool scaled = false;
|
|
1883
|
+
if (bmp.Width > 1920 || bmp.Height > 1920)
|
|
1884
|
+
{
|
|
1885
|
+
double scale = Math.Min(1920.0 / bmp.Width, 1920.0 / bmp.Height);
|
|
1886
|
+
int nw = (int)(bmp.Width * scale);
|
|
1887
|
+
int nh = (int)(bmp.Height * scale);
|
|
1888
|
+
output = new Bitmap(nw, nh);
|
|
1889
|
+
using (var g2 = Graphics.FromImage(output))
|
|
1890
|
+
{
|
|
1891
|
+
g2.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
1892
|
+
g2.DrawImage(bmp, 0, 0, nw, nh);
|
|
1893
|
+
}
|
|
1894
|
+
scaled = true;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
using (var ms = new MemoryStream())
|
|
1898
|
+
{
|
|
1899
|
+
var jpegEncoder = ImageCodecInfo.GetImageEncoders().First(e => e.FormatID == ImageFormat.Jpeg.Guid);
|
|
1900
|
+
var encoderParams = new EncoderParameters(1);
|
|
1901
|
+
encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
|
|
1902
|
+
output.Save(ms, jpegEncoder, encoderParams);
|
|
1903
|
+
|
|
1904
|
+
var b64 = Convert.ToBase64String(ms.ToArray());
|
|
1905
|
+
Console.WriteLine("{\\"ok\\":true,\\"image\\":" + JsonStr(b64) + ",\\"width\\":" + output.Width + ",\\"height\\":" + output.Height + ",\\"format\\":\\"jpeg\\"}");
|
|
1906
|
+
}
|
|
1907
|
+
if (scaled) output.Dispose();
|
|
1908
|
+
}
|
|
1909
|
+
break;
|
|
1910
|
+
}
|
|
1911
|
+
case "window_list":
|
|
1912
|
+
{
|
|
1913
|
+
var windows = new List<string>();
|
|
1914
|
+
EnumWindows((hWnd, _) =>
|
|
1915
|
+
{
|
|
1916
|
+
if (!IsWindowVisible(hWnd)) return true;
|
|
1917
|
+
int len = GetWindowTextLength(hWnd);
|
|
1918
|
+
if (len == 0) return true;
|
|
1919
|
+
var sb = new StringBuilder(len + 1);
|
|
1920
|
+
GetWindowText(hWnd, sb, sb.Capacity);
|
|
1921
|
+
var title = sb.ToString();
|
|
1922
|
+
|
|
1923
|
+
RECT r;
|
|
1924
|
+
GetWindowRect(hWnd, out r);
|
|
1925
|
+
|
|
1926
|
+
uint pid;
|
|
1927
|
+
GetWindowThreadProcessId(hWnd, out pid);
|
|
1928
|
+
string appName = "";
|
|
1929
|
+
try { appName = Process.GetProcessById((int)pid).ProcessName; } catch {}
|
|
1930
|
+
|
|
1931
|
+
windows.Add(String.Format("{{\\"id\\":{0},\\"app\\":{1},\\"title\\":{2},\\"bounds\\":{{\\"x\\":{3},\\"y\\":{4},\\"width\\":{5},\\"height\\":{6}}},\\"minimized\\":false}}",
|
|
1932
|
+
hWnd.ToInt64(), JsonStr(appName), JsonStr(title),
|
|
1933
|
+
r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top));
|
|
1934
|
+
return true;
|
|
1935
|
+
}, IntPtr.Zero);
|
|
1936
|
+
|
|
1937
|
+
Console.WriteLine("{\\"ok\\":true,\\"windows\\":[" + string.Join(",", windows) + "]}");
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
case "window_focus":
|
|
1941
|
+
{
|
|
1942
|
+
var wid = (IntPtr)(long)GetDbl(req, "windowId");
|
|
1943
|
+
SetForegroundWindow(wid);
|
|
1944
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1945
|
+
break;
|
|
1946
|
+
}
|
|
1947
|
+
case "window_resize":
|
|
1948
|
+
{
|
|
1949
|
+
var wid = (IntPtr)(long)GetDbl(req, "windowId");
|
|
1950
|
+
int x = (int)GetDbl(req, "x"), y = (int)GetDbl(req, "y");
|
|
1951
|
+
int w = (int)GetDbl(req, "width"), h = (int)GetDbl(req, "height");
|
|
1952
|
+
MoveWindow(wid, x, y, w, h, true);
|
|
1953
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
case "window_close":
|
|
1957
|
+
{
|
|
1958
|
+
var wid = (IntPtr)(long)GetDbl(req, "windowId");
|
|
1959
|
+
SendMessage(wid, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
|
|
1960
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
case "window_minimize":
|
|
1964
|
+
{
|
|
1965
|
+
var wid = (IntPtr)(long)GetDbl(req, "windowId");
|
|
1966
|
+
ShowWindow(wid, SW_MINIMIZE);
|
|
1967
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1970
|
+
case "app_launch":
|
|
1971
|
+
{
|
|
1972
|
+
var app = GetStr(req, "name");
|
|
1973
|
+
Process.Start(app);
|
|
1974
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1975
|
+
break;
|
|
1976
|
+
}
|
|
1977
|
+
case "app_quit":
|
|
1978
|
+
{
|
|
1979
|
+
var app = GetStr(req, "name").ToLower();
|
|
1980
|
+
foreach (var p in Process.GetProcesses())
|
|
1981
|
+
{
|
|
1982
|
+
try { if (p.ProcessName.ToLower() == app) p.Kill(); } catch {}
|
|
1983
|
+
}
|
|
1984
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
case "app_list":
|
|
1988
|
+
{
|
|
1989
|
+
var apps = new List<string>();
|
|
1990
|
+
var seen = new HashSet<int>();
|
|
1991
|
+
foreach (var p in Process.GetProcesses())
|
|
1992
|
+
{
|
|
1993
|
+
try
|
|
1994
|
+
{
|
|
1995
|
+
if (p.MainWindowHandle != IntPtr.Zero && !seen.Contains(p.Id))
|
|
1996
|
+
{
|
|
1997
|
+
seen.Add(p.Id);
|
|
1998
|
+
apps.Add(String.Format("{{\\"name\\":{0},\\"pid\\":{1}}}", JsonStr(p.ProcessName), p.Id));
|
|
1999
|
+
}
|
|
2000
|
+
} catch {}
|
|
2001
|
+
}
|
|
2002
|
+
Console.WriteLine("{\\"ok\\":true,\\"apps\\":[" + string.Join(",", apps) + "]}");
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
case "clipboard_read":
|
|
2006
|
+
{
|
|
2007
|
+
string text = Clipboard.GetText() ?? "";
|
|
2008
|
+
Console.WriteLine("{\\"ok\\":true,\\"text\\":" + JsonStr(text) + "}");
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
case "clipboard_write":
|
|
2012
|
+
{
|
|
2013
|
+
var text = GetStr(req, "text");
|
|
2014
|
+
if (string.IsNullOrEmpty(text)) Clipboard.Clear();
|
|
2015
|
+
else Clipboard.SetText(text);
|
|
2016
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
2017
|
+
break;
|
|
2018
|
+
}
|
|
2019
|
+
case "screen_info":
|
|
2020
|
+
{
|
|
2021
|
+
var screen = Screen.PrimaryScreen;
|
|
2022
|
+
float dpi;
|
|
2023
|
+
using (var g = Graphics.FromHwnd(IntPtr.Zero)) { dpi = g.DpiX; }
|
|
2024
|
+
double scale = Math.Round(dpi / 96.0, 2);
|
|
2025
|
+
Console.WriteLine("{\\"ok\\":true,\\"width\\":" + screen.Bounds.Width + ",\\"height\\":" + screen.Bounds.Height + ",\\"scaleFactor\\":" + scale + "}");
|
|
2026
|
+
break;
|
|
2027
|
+
}
|
|
2028
|
+
case "ax_tree":
|
|
2029
|
+
{
|
|
2030
|
+
int pid = GetInt(req, "pid");
|
|
2031
|
+
int maxDepth = GetInt(req, "maxDepth", 8);
|
|
2032
|
+
var rolesObj = GetList(req, "roles");
|
|
2033
|
+
var roles = rolesObj.Count > 0 ? rolesObj.Select(r => r.ToString().ToLower()).ToList() : null;
|
|
2034
|
+
|
|
2035
|
+
var root = FindAppRoot(pid);
|
|
2036
|
+
axElementCount = 0;
|
|
2037
|
+
var treeJson = WalkAXTree(root, 0, maxDepth, roles, "0");
|
|
2038
|
+
Console.WriteLine("{\\"ok\\":true,\\"root\\":" + treeJson + ",\\"elementCount\\":" + axElementCount + "}");
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
case "ax_action":
|
|
2042
|
+
{
|
|
2043
|
+
var elementId = GetStr(req, "elementId");
|
|
2044
|
+
var act = GetStr(req, "action_name");
|
|
2045
|
+
int pid = GetInt(req, "pid");
|
|
2046
|
+
|
|
2047
|
+
var root = FindAppRoot(pid);
|
|
2048
|
+
var el = NavigateToElement(root, elementId);
|
|
2049
|
+
|
|
2050
|
+
switch (act.ToLower())
|
|
2051
|
+
{
|
|
2052
|
+
case "invoke":
|
|
2053
|
+
case "press":
|
|
2054
|
+
case "click":
|
|
2055
|
+
((InvokePattern)el.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
|
|
2056
|
+
break;
|
|
2057
|
+
case "toggle":
|
|
2058
|
+
((TogglePattern)el.GetCurrentPattern(TogglePattern.Pattern)).Toggle();
|
|
2059
|
+
break;
|
|
2060
|
+
case "expand":
|
|
2061
|
+
((ExpandCollapsePattern)el.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Expand();
|
|
2062
|
+
break;
|
|
2063
|
+
case "collapse":
|
|
2064
|
+
((ExpandCollapsePattern)el.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Collapse();
|
|
2065
|
+
break;
|
|
2066
|
+
default:
|
|
2067
|
+
if (act.StartsWith("setvalue:"))
|
|
2068
|
+
{
|
|
2069
|
+
var value = act.Substring(9);
|
|
2070
|
+
((ValuePattern)el.GetCurrentPattern(ValuePattern.Pattern)).SetValue(value);
|
|
2071
|
+
}
|
|
2072
|
+
else
|
|
2073
|
+
{
|
|
2074
|
+
throw new Exception("Unsupported action: " + act);
|
|
2075
|
+
}
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
Console.WriteLine("{\\"ok\\":true}");
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
case "ax_search":
|
|
2082
|
+
{
|
|
2083
|
+
var query = GetStr(req, "query");
|
|
2084
|
+
var roleFilter = GetStr(req, "role", null);
|
|
2085
|
+
int pid = GetInt(req, "pid");
|
|
2086
|
+
int maxResults = GetInt(req, "maxResults", 20);
|
|
2087
|
+
|
|
2088
|
+
var root = FindAppRoot(pid);
|
|
2089
|
+
var results = new List<string>();
|
|
2090
|
+
SearchAXTree(root, query, roleFilter, maxResults, results, "0", 0, 20);
|
|
2091
|
+
Console.WriteLine("{\\"ok\\":true,\\"elements\\":[" + string.Join(",", results) + "]}");
|
|
2092
|
+
break;
|
|
2093
|
+
}
|
|
2094
|
+
default:
|
|
2095
|
+
Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr("Unknown action: " + action) + "}");
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
catch (Exception ex)
|
|
2100
|
+
{
|
|
2101
|
+
Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr(ex.Message) + "}");
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
`;
|
|
2106
|
+
compiled2 = false;
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// src/agent/capabilities/desktop/windows-driver.ts
|
|
2111
|
+
var windows_driver_exports = {};
|
|
2112
|
+
__export(windows_driver_exports, {
|
|
2113
|
+
WindowsDriver: () => WindowsDriver
|
|
2114
|
+
});
|
|
2115
|
+
var WindowsDriver;
|
|
2116
|
+
var init_windows_driver = __esm({
|
|
2117
|
+
"src/agent/capabilities/desktop/windows-driver.ts"() {
|
|
2118
|
+
"use strict";
|
|
2119
|
+
init_csharp_helper();
|
|
2120
|
+
WindowsDriver = class {
|
|
2121
|
+
async screenshot(_options) {
|
|
2122
|
+
const res = await execHelper2({ action: "screenshot" });
|
|
2123
|
+
return {
|
|
2124
|
+
image: res.image,
|
|
2125
|
+
width: res.width,
|
|
2126
|
+
height: res.height,
|
|
2127
|
+
format: res.format || "jpeg"
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
async mouseClick(options) {
|
|
2131
|
+
await execHelper2({
|
|
2132
|
+
action: "click",
|
|
2133
|
+
x: options.x,
|
|
2134
|
+
y: options.y,
|
|
2135
|
+
button: options.button || "left",
|
|
2136
|
+
clicks: options.clicks || 1
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
async mouseMove(options) {
|
|
2140
|
+
await execHelper2({ action: "move", x: options.x, y: options.y });
|
|
2141
|
+
}
|
|
2142
|
+
async mouseDrag(options) {
|
|
2143
|
+
await execHelper2({
|
|
2144
|
+
action: "drag",
|
|
2145
|
+
x: options.fromX,
|
|
2146
|
+
y: options.fromY,
|
|
2147
|
+
toX: options.toX,
|
|
2148
|
+
toY: options.toY,
|
|
2149
|
+
button: options.button || "left"
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
async mouseScroll(options) {
|
|
2153
|
+
await execHelper2({
|
|
2154
|
+
action: "scroll",
|
|
2155
|
+
x: options.x,
|
|
2156
|
+
y: options.y,
|
|
2157
|
+
deltaX: options.deltaX || 0,
|
|
2158
|
+
deltaY: options.deltaY || 0
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
async mousePosition() {
|
|
2162
|
+
const res = await execHelper2({ action: "position" });
|
|
2163
|
+
return { x: res.x, y: res.y };
|
|
2164
|
+
}
|
|
2165
|
+
async keyboardType(options) {
|
|
2166
|
+
await execHelper2({ action: "type", text: options.text });
|
|
2167
|
+
if (options.delay) {
|
|
2168
|
+
await new Promise((r) => setTimeout(r, options.delay));
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
async keyboardKey(options) {
|
|
2172
|
+
await execHelper2({ action: "key", keys: options.keys });
|
|
2173
|
+
}
|
|
2174
|
+
async windowList() {
|
|
2175
|
+
const res = await execHelper2({ action: "window_list" });
|
|
2176
|
+
return res.windows || [];
|
|
2177
|
+
}
|
|
2178
|
+
async windowFocus(windowId) {
|
|
2179
|
+
await execHelper2({ action: "window_focus", windowId });
|
|
2180
|
+
}
|
|
2181
|
+
async windowResize(windowId, bounds) {
|
|
2182
|
+
const windows = await this.windowList();
|
|
2183
|
+
const win = windows.find((w) => w.id === windowId);
|
|
2184
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
2185
|
+
await execHelper2({
|
|
2186
|
+
action: "window_resize",
|
|
2187
|
+
windowId,
|
|
2188
|
+
x: bounds.x ?? win.bounds.x,
|
|
2189
|
+
y: bounds.y ?? win.bounds.y,
|
|
2190
|
+
width: bounds.width ?? win.bounds.width,
|
|
2191
|
+
height: bounds.height ?? win.bounds.height
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
async windowClose(windowId) {
|
|
2195
|
+
await execHelper2({ action: "window_close", windowId });
|
|
2196
|
+
}
|
|
2197
|
+
async windowMinimize(windowId) {
|
|
2198
|
+
await execHelper2({ action: "window_minimize", windowId });
|
|
2199
|
+
}
|
|
2200
|
+
async appLaunch(name) {
|
|
2201
|
+
await execHelper2({ action: "app_launch", name });
|
|
2202
|
+
}
|
|
2203
|
+
async appQuit(name) {
|
|
2204
|
+
await execHelper2({ action: "app_quit", name });
|
|
2205
|
+
}
|
|
2206
|
+
async appList() {
|
|
2207
|
+
const res = await execHelper2({ action: "app_list" });
|
|
2208
|
+
return res.apps || [];
|
|
2209
|
+
}
|
|
2210
|
+
async clipboardRead() {
|
|
2211
|
+
const res = await execHelper2({ action: "clipboard_read" });
|
|
2212
|
+
return res.text || "";
|
|
2213
|
+
}
|
|
2214
|
+
async clipboardWrite(text) {
|
|
2215
|
+
await execHelper2({ action: "clipboard_write", text });
|
|
2216
|
+
}
|
|
2217
|
+
async screenInfo() {
|
|
2218
|
+
const res = await execHelper2({ action: "screen_info" });
|
|
2219
|
+
return {
|
|
2220
|
+
width: res.width,
|
|
2221
|
+
height: res.height,
|
|
2222
|
+
scaleFactor: res.scaleFactor
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
async cursorImage(radius = 50) {
|
|
2226
|
+
const pos = await this.mousePosition();
|
|
2227
|
+
return this.screenshot({
|
|
2228
|
+
region: {
|
|
2229
|
+
x: Math.max(0, Math.round(pos.x - radius)),
|
|
2230
|
+
y: Math.max(0, Math.round(pos.y - radius)),
|
|
2231
|
+
width: radius * 2,
|
|
2232
|
+
height: radius * 2
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
async axTree(options) {
|
|
2237
|
+
const res = await execHelper2({
|
|
2238
|
+
action: "ax_tree",
|
|
2239
|
+
pid: options.pid,
|
|
2240
|
+
maxDepth: options.maxDepth ?? 8,
|
|
2241
|
+
roles: options.roles
|
|
2242
|
+
});
|
|
2243
|
+
return {
|
|
2244
|
+
root: res.root,
|
|
2245
|
+
elementCount: res.elementCount
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
async axAction(options) {
|
|
2249
|
+
const res = await execHelper2({
|
|
2250
|
+
action: "ax_action",
|
|
2251
|
+
elementId: options.elementId,
|
|
2252
|
+
action_name: options.action,
|
|
2253
|
+
pid: options.pid
|
|
2254
|
+
});
|
|
2255
|
+
return res;
|
|
2256
|
+
}
|
|
2257
|
+
async axSetValue(options) {
|
|
2258
|
+
const res = await execHelper2({
|
|
2259
|
+
action: "ax_set_value",
|
|
2260
|
+
elementId: options.elementId,
|
|
2261
|
+
value: options.value,
|
|
2262
|
+
pid: options.pid
|
|
2263
|
+
});
|
|
2264
|
+
return res;
|
|
2265
|
+
}
|
|
2266
|
+
async axFocus(options) {
|
|
2267
|
+
const res = await execHelper2({
|
|
2268
|
+
action: "ax_focus",
|
|
2269
|
+
elementId: options.elementId,
|
|
2270
|
+
pid: options.pid
|
|
2271
|
+
});
|
|
2272
|
+
return res;
|
|
2273
|
+
}
|
|
2274
|
+
async axSearch(options) {
|
|
2275
|
+
const res = await execHelper2({
|
|
2276
|
+
action: "ax_search",
|
|
2277
|
+
query: options.query,
|
|
2278
|
+
role: options.role,
|
|
2279
|
+
pid: options.pid,
|
|
2280
|
+
maxResults: options.maxResults ?? 20
|
|
2281
|
+
});
|
|
2282
|
+
return { elements: res.elements || [] };
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
// src/agent/capabilities/desktop/atspi-helper.ts
|
|
2289
|
+
import { spawn as spawn5 } from "child_process";
|
|
2290
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2291
|
+
import { join as join6 } from "path";
|
|
2292
|
+
import { homedir as homedir4 } from "os";
|
|
2293
|
+
async function ensureHelper3() {
|
|
2294
|
+
if (written && existsSync4(HELPER_PATH3)) return HELPER_PATH3;
|
|
2295
|
+
if (existsSync4(HELPER_PATH3)) {
|
|
2296
|
+
written = true;
|
|
2297
|
+
return HELPER_PATH3;
|
|
2298
|
+
}
|
|
2299
|
+
mkdirSync3(BIN_DIR3, { recursive: true });
|
|
2300
|
+
writeFileSync3(HELPER_PATH3, PYTHON_SOURCE, { mode: 493 });
|
|
2301
|
+
written = true;
|
|
2302
|
+
return HELPER_PATH3;
|
|
2303
|
+
}
|
|
2304
|
+
async function execAtspiHelper(request) {
|
|
2305
|
+
const helperPath = await ensureHelper3();
|
|
2306
|
+
return new Promise((resolve2, reject) => {
|
|
2307
|
+
const proc = spawn5("python3", [helperPath], {
|
|
2308
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2309
|
+
});
|
|
2310
|
+
let stdout = "";
|
|
2311
|
+
let stderr = "";
|
|
2312
|
+
proc.stdout.on("data", (d) => {
|
|
2313
|
+
stdout += d.toString();
|
|
2314
|
+
});
|
|
2315
|
+
proc.stderr.on("data", (d) => {
|
|
2316
|
+
stderr += d.toString();
|
|
2317
|
+
});
|
|
2318
|
+
proc.on("close", (code) => {
|
|
2319
|
+
if (code !== 0) {
|
|
2320
|
+
reject(new Error(`AT-SPI helper failed (exit ${code}): ${stderr}`));
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
try {
|
|
2324
|
+
const response = JSON.parse(stdout.trim());
|
|
2325
|
+
if (!response.ok && response.error) {
|
|
2326
|
+
reject(new Error(response.error));
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
resolve2(response);
|
|
2330
|
+
} catch {
|
|
2331
|
+
reject(new Error(`Invalid helper output: ${stdout}`));
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
proc.on("error", (err) => {
|
|
2335
|
+
reject(new Error(`python3 not found: ${err.message}. Install: sudo apt install python3`));
|
|
2336
|
+
});
|
|
2337
|
+
proc.stdin.write(JSON.stringify(request));
|
|
2338
|
+
proc.stdin.end();
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
var HELPER_VERSION3, BIN_DIR3, HELPER_PATH3, PYTHON_SOURCE, written;
|
|
2342
|
+
var init_atspi_helper = __esm({
|
|
2343
|
+
"src/agent/capabilities/desktop/atspi-helper.ts"() {
|
|
2344
|
+
"use strict";
|
|
2345
|
+
HELPER_VERSION3 = "v1";
|
|
2346
|
+
BIN_DIR3 = join6(homedir4(), ".agent-tunnel", "bin");
|
|
2347
|
+
HELPER_PATH3 = join6(BIN_DIR3, `atspi-helper-${HELPER_VERSION3}.py`);
|
|
2348
|
+
PYTHON_SOURCE = `#!/usr/bin/env python3
|
|
2349
|
+
"""AT-SPI2 accessibility helper for Linux."""
|
|
2350
|
+
import json
|
|
2351
|
+
import sys
|
|
2352
|
+
|
|
2353
|
+
try:
|
|
2354
|
+
import gi
|
|
2355
|
+
gi.require_version('Atspi', '2.0')
|
|
2356
|
+
from gi.repository import Atspi
|
|
2357
|
+
except ImportError:
|
|
2358
|
+
print(json.dumps({"ok": False, "error": "python3-gi and gir1.2-atspi-2.0 required. Install: sudo apt install python3-gi gir1.2-atspi-2.0"}))
|
|
2359
|
+
sys.exit(0)
|
|
2360
|
+
|
|
2361
|
+
element_count = 0
|
|
2362
|
+
|
|
2363
|
+
def get_role_name(accessible):
|
|
2364
|
+
try:
|
|
2365
|
+
return Atspi.Accessible.get_role_name(accessible)
|
|
2366
|
+
except:
|
|
2367
|
+
return ""
|
|
2368
|
+
|
|
2369
|
+
def get_name(accessible):
|
|
2370
|
+
try:
|
|
2371
|
+
return Atspi.Accessible.get_name(accessible) or ""
|
|
2372
|
+
except:
|
|
2373
|
+
return ""
|
|
2374
|
+
|
|
2375
|
+
def get_description(accessible):
|
|
2376
|
+
try:
|
|
2377
|
+
return Atspi.Accessible.get_description(accessible) or ""
|
|
2378
|
+
except:
|
|
2379
|
+
return ""
|
|
2380
|
+
|
|
2381
|
+
def get_bounds(accessible):
|
|
2382
|
+
try:
|
|
2383
|
+
comp = accessible.get_component_iface()
|
|
2384
|
+
if comp:
|
|
2385
|
+
rect = comp.get_extents(Atspi.CoordType.SCREEN)
|
|
2386
|
+
return {"x": rect.x, "y": rect.y, "width": rect.width, "height": rect.height}
|
|
2387
|
+
except:
|
|
2388
|
+
pass
|
|
2389
|
+
return {"x": 0, "y": 0, "width": 0, "height": 0}
|
|
2390
|
+
|
|
2391
|
+
def get_value(accessible):
|
|
2392
|
+
try:
|
|
2393
|
+
val = accessible.get_value_iface()
|
|
2394
|
+
if val:
|
|
2395
|
+
return str(val.get_current_value())
|
|
2396
|
+
except:
|
|
2397
|
+
pass
|
|
2398
|
+
return ""
|
|
2399
|
+
|
|
2400
|
+
def get_actions(accessible):
|
|
2401
|
+
actions = []
|
|
2402
|
+
try:
|
|
2403
|
+
action_iface = accessible.get_action_iface()
|
|
2404
|
+
if action_iface:
|
|
2405
|
+
for i in range(action_iface.get_n_actions()):
|
|
2406
|
+
name = action_iface.get_action_name(i)
|
|
2407
|
+
if name:
|
|
2408
|
+
actions.append(name)
|
|
2409
|
+
except:
|
|
2410
|
+
pass
|
|
2411
|
+
return actions
|
|
2412
|
+
|
|
2413
|
+
def get_states(accessible):
|
|
2414
|
+
enabled = True
|
|
2415
|
+
focused = False
|
|
2416
|
+
try:
|
|
2417
|
+
state_set = accessible.get_state_set()
|
|
2418
|
+
enabled = state_set.contains(Atspi.StateType.ENABLED) or state_set.contains(Atspi.StateType.SENSITIVE)
|
|
2419
|
+
focused = state_set.contains(Atspi.StateType.FOCUSED)
|
|
2420
|
+
except:
|
|
2421
|
+
pass
|
|
2422
|
+
return enabled, focused
|
|
2423
|
+
|
|
2424
|
+
def walk_tree(accessible, depth, max_depth, roles, path_prefix):
|
|
2425
|
+
global element_count
|
|
2426
|
+
if accessible is None or depth > max_depth:
|
|
2427
|
+
return None
|
|
2428
|
+
element_count += 1
|
|
2429
|
+
|
|
2430
|
+
role = get_role_name(accessible)
|
|
2431
|
+
name = get_name(accessible)
|
|
2432
|
+
value = get_value(accessible)
|
|
2433
|
+
desc = get_description(accessible)
|
|
2434
|
+
bounds = get_bounds(accessible)
|
|
2435
|
+
actions = get_actions(accessible)
|
|
2436
|
+
enabled, focused = get_states(accessible)
|
|
2437
|
+
|
|
2438
|
+
children = []
|
|
2439
|
+
if depth < max_depth:
|
|
2440
|
+
try:
|
|
2441
|
+
count = accessible.get_child_count()
|
|
2442
|
+
for i in range(count):
|
|
2443
|
+
child = accessible.get_child_at_index(i)
|
|
2444
|
+
if child:
|
|
2445
|
+
child_path = f"{path_prefix}.{i}" if path_prefix else str(i)
|
|
2446
|
+
child_node = walk_tree(child, depth + 1, max_depth, roles, child_path)
|
|
2447
|
+
if child_node is not None:
|
|
2448
|
+
if isinstance(child_node, list):
|
|
2449
|
+
children.extend(child_node)
|
|
2450
|
+
else:
|
|
2451
|
+
children.append(child_node)
|
|
2452
|
+
except:
|
|
2453
|
+
pass
|
|
2454
|
+
|
|
2455
|
+
if roles and role.lower() not in [r.lower() for r in roles]:
|
|
2456
|
+
return children if children else None
|
|
2457
|
+
|
|
2458
|
+
return {
|
|
2459
|
+
"id": path_prefix,
|
|
2460
|
+
"role": role,
|
|
2461
|
+
"title": name,
|
|
2462
|
+
"value": value,
|
|
2463
|
+
"description": desc,
|
|
2464
|
+
"bounds": bounds,
|
|
2465
|
+
"children": children,
|
|
2466
|
+
"actions": actions,
|
|
2467
|
+
"enabled": enabled,
|
|
2468
|
+
"focused": focused,
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
def find_app_by_pid(pid):
|
|
2472
|
+
desktop = Atspi.get_desktop(0)
|
|
2473
|
+
count = desktop.get_child_count()
|
|
2474
|
+
for i in range(count):
|
|
2475
|
+
app = desktop.get_child_at_index(i)
|
|
2476
|
+
if app:
|
|
2477
|
+
try:
|
|
2478
|
+
if app.get_process_id() == pid:
|
|
2479
|
+
return app
|
|
2480
|
+
except:
|
|
2481
|
+
pass
|
|
2482
|
+
raise Exception(f"No AT-SPI application found for PID {pid}")
|
|
2483
|
+
|
|
2484
|
+
def navigate_to_element(root, element_id):
|
|
2485
|
+
parts = element_id.split(".")
|
|
2486
|
+
current = root
|
|
2487
|
+
for part in parts:
|
|
2488
|
+
idx = int(part)
|
|
2489
|
+
child = current.get_child_at_index(idx)
|
|
2490
|
+
if child is None:
|
|
2491
|
+
raise Exception(f"Element not found at path: {element_id}")
|
|
2492
|
+
current = child
|
|
2493
|
+
return current
|
|
2494
|
+
|
|
2495
|
+
def search_tree(accessible, query, role_filter, max_results, results, path_prefix, depth, max_depth):
|
|
2496
|
+
if accessible is None or len(results) >= max_results or depth > max_depth:
|
|
2497
|
+
return
|
|
2498
|
+
|
|
2499
|
+
role = get_role_name(accessible)
|
|
2500
|
+
name = get_name(accessible)
|
|
2501
|
+
value = get_value(accessible)
|
|
2502
|
+
desc = get_description(accessible)
|
|
2503
|
+
|
|
2504
|
+
query_lower = query.lower()
|
|
2505
|
+
match = (query_lower in name.lower() or query_lower in value.lower() or query_lower in desc.lower())
|
|
2506
|
+
|
|
2507
|
+
if role_filter and role.lower() != role_filter.lower():
|
|
2508
|
+
match = False
|
|
2509
|
+
|
|
2510
|
+
if match:
|
|
2511
|
+
bounds = get_bounds(accessible)
|
|
2512
|
+
actions = get_actions(accessible)
|
|
2513
|
+
enabled, focused = get_states(accessible)
|
|
2514
|
+
results.append({
|
|
2515
|
+
"id": path_prefix,
|
|
2516
|
+
"role": role,
|
|
2517
|
+
"title": name,
|
|
2518
|
+
"value": value,
|
|
2519
|
+
"description": desc,
|
|
2520
|
+
"bounds": bounds,
|
|
2521
|
+
"children": [],
|
|
2522
|
+
"actions": actions,
|
|
2523
|
+
"enabled": enabled,
|
|
2524
|
+
"focused": focused,
|
|
2525
|
+
})
|
|
2526
|
+
|
|
2527
|
+
try:
|
|
2528
|
+
count = accessible.get_child_count()
|
|
2529
|
+
for i in range(count):
|
|
2530
|
+
if len(results) >= max_results:
|
|
2531
|
+
break
|
|
2532
|
+
child = accessible.get_child_at_index(i)
|
|
2533
|
+
if child:
|
|
2534
|
+
child_path = f"{path_prefix}.{i}" if path_prefix else str(i)
|
|
2535
|
+
search_tree(child, query, role_filter, max_results, results, child_path, depth + 1, max_depth)
|
|
2536
|
+
except:
|
|
2537
|
+
pass
|
|
2538
|
+
|
|
2539
|
+
def main():
|
|
2540
|
+
raw = sys.stdin.read().strip()
|
|
2541
|
+
try:
|
|
2542
|
+
req = json.loads(raw)
|
|
2543
|
+
except:
|
|
2544
|
+
print(json.dumps({"ok": False, "error": "Invalid JSON input"}))
|
|
2545
|
+
return
|
|
2546
|
+
|
|
2547
|
+
action = req.get("action", "")
|
|
2548
|
+
|
|
2549
|
+
try:
|
|
2550
|
+
if action == "ax_tree":
|
|
2551
|
+
pid = req.get("pid", 0)
|
|
2552
|
+
max_depth = req.get("maxDepth", 8)
|
|
2553
|
+
roles = req.get("roles", [])
|
|
2554
|
+
|
|
2555
|
+
root = find_app_by_pid(pid) if pid > 0 else Atspi.get_desktop(0)
|
|
2556
|
+
|
|
2557
|
+
global element_count
|
|
2558
|
+
element_count = 0
|
|
2559
|
+
tree = walk_tree(root, 0, max_depth, roles, "0")
|
|
2560
|
+
print(json.dumps({"ok": True, "root": tree, "elementCount": element_count}))
|
|
2561
|
+
|
|
2562
|
+
elif action == "ax_action":
|
|
2563
|
+
element_id = req.get("elementId", "")
|
|
2564
|
+
action_name = req.get("action_name", "")
|
|
2565
|
+
pid = req.get("pid", 0)
|
|
2566
|
+
|
|
2567
|
+
root = find_app_by_pid(pid) if pid > 0 else Atspi.get_desktop(0)
|
|
2568
|
+
el = navigate_to_element(root, element_id)
|
|
2569
|
+
|
|
2570
|
+
action_iface = el.get_action_iface()
|
|
2571
|
+
if not action_iface:
|
|
2572
|
+
raise Exception("Element does not support actions")
|
|
2573
|
+
|
|
2574
|
+
performed = False
|
|
2575
|
+
for i in range(action_iface.get_n_actions()):
|
|
2576
|
+
if action_iface.get_action_name(i).lower() == action_name.lower():
|
|
2577
|
+
action_iface.do_action(i)
|
|
2578
|
+
performed = True
|
|
2579
|
+
break
|
|
2580
|
+
|
|
2581
|
+
if not performed:
|
|
2582
|
+
raise Exception(f"Action '{action_name}' not found on element")
|
|
2583
|
+
|
|
2584
|
+
print(json.dumps({"ok": True}))
|
|
2585
|
+
|
|
2586
|
+
elif action == "ax_search":
|
|
2587
|
+
query = req.get("query", "")
|
|
2588
|
+
role_filter = req.get("role", None)
|
|
2589
|
+
pid = req.get("pid", 0)
|
|
2590
|
+
max_results = req.get("maxResults", 20)
|
|
2591
|
+
|
|
2592
|
+
root = find_app_by_pid(pid) if pid > 0 else Atspi.get_desktop(0)
|
|
2593
|
+
results = []
|
|
2594
|
+
search_tree(root, query, role_filter, max_results, results, "0", 0, 20)
|
|
2595
|
+
print(json.dumps({"ok": True, "elements": results}))
|
|
2596
|
+
|
|
2597
|
+
else:
|
|
2598
|
+
print(json.dumps({"ok": False, "error": f"Unknown action: {action}"}))
|
|
2599
|
+
except Exception as e:
|
|
2600
|
+
print(json.dumps({"ok": False, "error": str(e)}))
|
|
2601
|
+
|
|
2602
|
+
if __name__ == "__main__":
|
|
2603
|
+
main()
|
|
2604
|
+
`;
|
|
2605
|
+
written = false;
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
|
|
2609
|
+
// src/agent/capabilities/desktop/linux-driver.ts
|
|
2610
|
+
var linux_driver_exports = {};
|
|
2611
|
+
__export(linux_driver_exports, {
|
|
2612
|
+
LinuxDriver: () => LinuxDriver
|
|
2613
|
+
});
|
|
2614
|
+
import { spawn as spawn6 } from "child_process";
|
|
2615
|
+
import { readFile as readFile3, unlink as unlink3 } from "fs/promises";
|
|
2616
|
+
import { join as join7 } from "path";
|
|
2617
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2618
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
2619
|
+
function tmpPath2() {
|
|
2620
|
+
return join7(tmpdir2(), `tunnel-ss-${randomBytes3(6).toString("hex")}.png`);
|
|
2621
|
+
}
|
|
2622
|
+
function exec2(cmd, args) {
|
|
2623
|
+
return new Promise((resolve2, reject) => {
|
|
2624
|
+
const proc = spawn6(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
2625
|
+
let stdout = "";
|
|
2626
|
+
let stderr = "";
|
|
2627
|
+
proc.stdout.on("data", (d) => {
|
|
2628
|
+
stdout += d.toString();
|
|
2629
|
+
});
|
|
2630
|
+
proc.stderr.on("data", (d) => {
|
|
2631
|
+
stderr += d.toString();
|
|
2632
|
+
});
|
|
2633
|
+
proc.on("close", (code) => {
|
|
2634
|
+
if (code !== 0) reject(new Error(`${cmd} failed (${code}): ${stderr}`));
|
|
2635
|
+
else resolve2(stdout);
|
|
2636
|
+
});
|
|
2637
|
+
proc.on("error", (err) => {
|
|
2638
|
+
reject(new Error(`${cmd} not found. Install it: sudo apt install ${cmd}`));
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
function parsePngDimensions(buf) {
|
|
2643
|
+
if (buf.length >= 24 && buf[0] === 137 && buf[1] === 80) {
|
|
2644
|
+
return {
|
|
2645
|
+
width: buf.readUInt32BE(16),
|
|
2646
|
+
height: buf.readUInt32BE(20)
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
return { width: 0, height: 0 };
|
|
2650
|
+
}
|
|
2651
|
+
var BUTTON_MAP, SCROLL_MAP, LinuxDriver;
|
|
2652
|
+
var init_linux_driver = __esm({
|
|
2653
|
+
"src/agent/capabilities/desktop/linux-driver.ts"() {
|
|
2654
|
+
"use strict";
|
|
2655
|
+
init_atspi_helper();
|
|
2656
|
+
BUTTON_MAP = {
|
|
2657
|
+
left: "1",
|
|
2658
|
+
middle: "2",
|
|
2659
|
+
right: "3"
|
|
2660
|
+
};
|
|
2661
|
+
SCROLL_MAP = {
|
|
2662
|
+
up: "4",
|
|
2663
|
+
down: "5",
|
|
2664
|
+
left: "6",
|
|
2665
|
+
right: "7"
|
|
2666
|
+
};
|
|
2667
|
+
LinuxDriver = class {
|
|
2668
|
+
async screenshot(options) {
|
|
2669
|
+
const path = tmpPath2();
|
|
2670
|
+
if (options.region) {
|
|
2671
|
+
const { x, y, width: width2, height: height2 } = options.region;
|
|
2672
|
+
await exec2("scrot", ["-a", `${x},${y},${width2},${height2}`, path]);
|
|
2673
|
+
} else if (options.windowId) {
|
|
2674
|
+
await exec2("scrot", ["-u", "-w", path]);
|
|
2675
|
+
} else {
|
|
2676
|
+
await exec2("scrot", [path]);
|
|
2677
|
+
}
|
|
2678
|
+
const buf = await readFile3(path);
|
|
2679
|
+
await unlink3(path).catch(() => {
|
|
2680
|
+
});
|
|
2681
|
+
const { width, height } = parsePngDimensions(buf);
|
|
2682
|
+
return {
|
|
2683
|
+
image: buf.toString("base64"),
|
|
2684
|
+
width,
|
|
2685
|
+
height,
|
|
2686
|
+
format: "png"
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
async mouseClick(options) {
|
|
2690
|
+
const button = BUTTON_MAP[options.button || "left"] || "1";
|
|
2691
|
+
const clicks = options.clicks || 1;
|
|
2692
|
+
await exec2("xdotool", ["mousemove", "--sync", String(options.x), String(options.y)]);
|
|
2693
|
+
const clickArgs = ["click", "--repeat", String(clicks), button];
|
|
2694
|
+
await exec2("xdotool", clickArgs);
|
|
2695
|
+
}
|
|
2696
|
+
async mouseMove(options) {
|
|
2697
|
+
await exec2("xdotool", ["mousemove", "--sync", String(options.x), String(options.y)]);
|
|
2698
|
+
}
|
|
2699
|
+
async mouseDrag(options) {
|
|
2700
|
+
const button = BUTTON_MAP[options.button || "left"] || "1";
|
|
2701
|
+
await exec2("xdotool", ["mousemove", "--sync", String(options.fromX), String(options.fromY)]);
|
|
2702
|
+
await exec2("xdotool", ["mousedown", button]);
|
|
2703
|
+
await exec2("xdotool", ["mousemove", "--sync", String(options.toX), String(options.toY)]);
|
|
2704
|
+
await exec2("xdotool", ["mouseup", button]);
|
|
2705
|
+
}
|
|
2706
|
+
async mouseScroll(options) {
|
|
2707
|
+
await exec2("xdotool", ["mousemove", "--sync", String(options.x), String(options.y)]);
|
|
2708
|
+
const dy = options.deltaY || 0;
|
|
2709
|
+
const dx = options.deltaX || 0;
|
|
2710
|
+
if (dy !== 0) {
|
|
2711
|
+
const btn = dy > 0 ? SCROLL_MAP.down : SCROLL_MAP.up;
|
|
2712
|
+
const count = Math.abs(dy);
|
|
2713
|
+
for (let i = 0; i < count; i++) {
|
|
2714
|
+
await exec2("xdotool", ["click", btn]);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (dx !== 0) {
|
|
2718
|
+
const btn = dx > 0 ? SCROLL_MAP.right : SCROLL_MAP.left;
|
|
2719
|
+
const count = Math.abs(dx);
|
|
2720
|
+
for (let i = 0; i < count; i++) {
|
|
2721
|
+
await exec2("xdotool", ["click", btn]);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
async mousePosition() {
|
|
2726
|
+
const out = await exec2("xdotool", ["getmouselocation"]);
|
|
2727
|
+
const match = out.match(/x:(\d+)\s+y:(\d+)/);
|
|
2728
|
+
if (!match) throw new Error(`Failed to parse mouse location: ${out}`);
|
|
2729
|
+
return { x: parseInt(match[1]), y: parseInt(match[2]) };
|
|
2730
|
+
}
|
|
2731
|
+
async keyboardType(options) {
|
|
2732
|
+
const args = ["type"];
|
|
2733
|
+
if (options.delay) {
|
|
2734
|
+
args.push("--delay", String(options.delay));
|
|
2735
|
+
}
|
|
2736
|
+
args.push("--", options.text);
|
|
2737
|
+
await exec2("xdotool", args);
|
|
2738
|
+
}
|
|
2739
|
+
async keyboardKey(options) {
|
|
2740
|
+
const combo = options.keys.map((k) => {
|
|
2741
|
+
const map = {
|
|
2742
|
+
cmd: "super",
|
|
2743
|
+
command: "super",
|
|
2744
|
+
ctrl: "ctrl",
|
|
2745
|
+
control: "ctrl",
|
|
2746
|
+
alt: "alt",
|
|
2747
|
+
option: "alt",
|
|
2748
|
+
shift: "shift",
|
|
2749
|
+
enter: "Return",
|
|
2750
|
+
return: "Return",
|
|
2751
|
+
tab: "Tab",
|
|
2752
|
+
space: "space",
|
|
2753
|
+
escape: "Escape",
|
|
2754
|
+
esc: "Escape",
|
|
2755
|
+
delete: "BackSpace",
|
|
2756
|
+
backspace: "BackSpace",
|
|
2757
|
+
up: "Up",
|
|
2758
|
+
down: "Down",
|
|
2759
|
+
left: "Left",
|
|
2760
|
+
right: "Right",
|
|
2761
|
+
home: "Home",
|
|
2762
|
+
end: "End",
|
|
2763
|
+
pageup: "Prior",
|
|
2764
|
+
pagedown: "Next"
|
|
2765
|
+
};
|
|
2766
|
+
return map[k.toLowerCase()] || k;
|
|
2767
|
+
}).join("+");
|
|
2768
|
+
await exec2("xdotool", ["key", combo]);
|
|
2769
|
+
}
|
|
2770
|
+
async windowList() {
|
|
2771
|
+
const out = await exec2("wmctrl", ["-l", "-G", "-p"]);
|
|
2772
|
+
const lines = out.trim().split("\n").filter(Boolean);
|
|
2773
|
+
return lines.map((line) => {
|
|
2774
|
+
const parts = line.split(/\s+/);
|
|
2775
|
+
const id = parseInt(parts[0], 16);
|
|
2776
|
+
const x = parseInt(parts[3]);
|
|
2777
|
+
const y = parseInt(parts[4]);
|
|
2778
|
+
const width = parseInt(parts[5]);
|
|
2779
|
+
const height = parseInt(parts[6]);
|
|
2780
|
+
const title = parts.slice(8).join(" ");
|
|
2781
|
+
return {
|
|
2782
|
+
id,
|
|
2783
|
+
app: parts[7] || "",
|
|
2784
|
+
title,
|
|
2785
|
+
bounds: { x, y, width, height },
|
|
2786
|
+
minimized: false
|
|
2787
|
+
};
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
async windowFocus(windowId) {
|
|
2791
|
+
await exec2("wmctrl", ["-i", "-a", `0x${windowId.toString(16)}`]);
|
|
2792
|
+
}
|
|
2793
|
+
async windowResize(windowId, bounds) {
|
|
2794
|
+
const windows = await this.windowList();
|
|
2795
|
+
const win = windows.find((w2) => w2.id === windowId);
|
|
2796
|
+
if (!win) throw new Error(`Window ${windowId} not found`);
|
|
2797
|
+
const x = bounds.x ?? win.bounds.x;
|
|
2798
|
+
const y = bounds.y ?? win.bounds.y;
|
|
2799
|
+
const w = bounds.width ?? win.bounds.width;
|
|
2800
|
+
const h = bounds.height ?? win.bounds.height;
|
|
2801
|
+
await exec2("wmctrl", ["-i", "-r", `0x${windowId.toString(16)}`, "-e", `0,${x},${y},${w},${h}`]);
|
|
2802
|
+
}
|
|
2803
|
+
async windowClose(windowId) {
|
|
2804
|
+
await exec2("wmctrl", ["-i", "-c", `0x${windowId.toString(16)}`]);
|
|
2805
|
+
}
|
|
2806
|
+
async windowMinimize(windowId) {
|
|
2807
|
+
await exec2("xdotool", ["windowminimize", String(windowId)]);
|
|
2808
|
+
}
|
|
2809
|
+
async appLaunch(name) {
|
|
2810
|
+
const proc = spawn6("xdg-open", [name], {
|
|
2811
|
+
stdio: "ignore",
|
|
2812
|
+
detached: true
|
|
2813
|
+
});
|
|
2814
|
+
proc.unref();
|
|
2815
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2816
|
+
}
|
|
2817
|
+
async appQuit(name) {
|
|
2818
|
+
const out = await exec2("pgrep", ["-f", name]).catch(() => "");
|
|
2819
|
+
const pids = out.trim().split("\n").filter(Boolean);
|
|
2820
|
+
for (const pid of pids) {
|
|
2821
|
+
await exec2("kill", [pid]).catch(() => {
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
async appList() {
|
|
2826
|
+
const out = await exec2("wmctrl", ["-l", "-p"]);
|
|
2827
|
+
const lines = out.trim().split("\n").filter(Boolean);
|
|
2828
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2829
|
+
const apps = [];
|
|
2830
|
+
for (const line of lines) {
|
|
2831
|
+
const parts = line.split(/\s+/);
|
|
2832
|
+
const pid = parseInt(parts[2]);
|
|
2833
|
+
if (pid && !seen.has(pid)) {
|
|
2834
|
+
seen.add(pid);
|
|
2835
|
+
let name = parts.slice(4).join(" ");
|
|
2836
|
+
try {
|
|
2837
|
+
const cmdline = await exec2("cat", [`/proc/${pid}/comm`]);
|
|
2838
|
+
name = cmdline.trim() || name;
|
|
2839
|
+
} catch {
|
|
2840
|
+
}
|
|
2841
|
+
apps.push({ name, pid });
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
return apps;
|
|
2845
|
+
}
|
|
2846
|
+
async clipboardRead() {
|
|
2847
|
+
return exec2("xclip", ["-selection", "clipboard", "-o"]);
|
|
2848
|
+
}
|
|
2849
|
+
async clipboardWrite(text) {
|
|
2850
|
+
await new Promise((resolve2, reject) => {
|
|
2851
|
+
const proc = spawn6("xclip", ["-selection", "clipboard"], {
|
|
2852
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
2853
|
+
});
|
|
2854
|
+
proc.on("close", (code) => {
|
|
2855
|
+
if (code !== 0) reject(new Error(`xclip failed (${code})`));
|
|
2856
|
+
else resolve2();
|
|
2857
|
+
});
|
|
2858
|
+
proc.on("error", () => reject(new Error("xclip not found. Install: sudo apt install xclip")));
|
|
2859
|
+
proc.stdin.write(text);
|
|
2860
|
+
proc.stdin.end();
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
async screenInfo() {
|
|
2864
|
+
const out = await exec2("xrandr", ["--current"]);
|
|
2865
|
+
const match = out.match(/(\d+)x(\d+)\+/);
|
|
2866
|
+
if (!match) throw new Error("Failed to parse xrandr output");
|
|
2867
|
+
return {
|
|
2868
|
+
width: parseInt(match[1]),
|
|
2869
|
+
height: parseInt(match[2]),
|
|
2870
|
+
scaleFactor: 1
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
async cursorImage(radius = 50) {
|
|
2874
|
+
const pos = await this.mousePosition();
|
|
2875
|
+
const x = Math.max(0, pos.x - radius);
|
|
2876
|
+
const y = Math.max(0, pos.y - radius);
|
|
2877
|
+
const size = radius * 2;
|
|
2878
|
+
return this.screenshot({ region: { x, y, width: size, height: size } });
|
|
2879
|
+
}
|
|
2880
|
+
async axTree(options) {
|
|
2881
|
+
const res = await execAtspiHelper({
|
|
2882
|
+
action: "ax_tree",
|
|
2883
|
+
pid: options.pid,
|
|
2884
|
+
maxDepth: options.maxDepth ?? 8,
|
|
2885
|
+
roles: options.roles
|
|
2886
|
+
});
|
|
2887
|
+
return {
|
|
2888
|
+
root: res.root,
|
|
2889
|
+
elementCount: res.elementCount
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
async axAction(options) {
|
|
2893
|
+
const res = await execAtspiHelper({
|
|
2894
|
+
action: "ax_action",
|
|
2895
|
+
elementId: options.elementId,
|
|
2896
|
+
action_name: options.action,
|
|
2897
|
+
pid: options.pid
|
|
2898
|
+
});
|
|
2899
|
+
return res;
|
|
2900
|
+
}
|
|
2901
|
+
async axSetValue(options) {
|
|
2902
|
+
const res = await execAtspiHelper({
|
|
2903
|
+
action: "ax_set_value",
|
|
2904
|
+
elementId: options.elementId,
|
|
2905
|
+
value: options.value,
|
|
2906
|
+
pid: options.pid
|
|
2907
|
+
});
|
|
2908
|
+
return res;
|
|
2909
|
+
}
|
|
2910
|
+
async axFocus(options) {
|
|
2911
|
+
const res = await execAtspiHelper({
|
|
2912
|
+
action: "ax_focus",
|
|
2913
|
+
elementId: options.elementId,
|
|
2914
|
+
pid: options.pid
|
|
2915
|
+
});
|
|
2916
|
+
return res;
|
|
2917
|
+
}
|
|
2918
|
+
async axSearch(options) {
|
|
2919
|
+
const res = await execAtspiHelper({
|
|
2920
|
+
action: "ax_search",
|
|
2921
|
+
query: options.query,
|
|
2922
|
+
role: options.role,
|
|
2923
|
+
pid: options.pid,
|
|
2924
|
+
maxResults: options.maxResults ?? 20
|
|
2925
|
+
});
|
|
2926
|
+
return { elements: res.elements || [] };
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
// src/node-ws-polyfill.ts
|
|
2933
|
+
if (typeof globalThis.WebSocket === "undefined") {
|
|
2934
|
+
try {
|
|
2935
|
+
const ws = __require("ws");
|
|
2936
|
+
globalThis.WebSocket = ws.default || ws;
|
|
2937
|
+
} catch {
|
|
2938
|
+
console.error(
|
|
2939
|
+
'[agent-tunnel] WebSocket is not available. Install the "ws" package or use Node.js 22+.'
|
|
2940
|
+
);
|
|
2941
|
+
process.exit(1);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
// src/agent/config.ts
|
|
2946
|
+
import { existsSync, readFileSync } from "fs";
|
|
2947
|
+
import { join } from "path";
|
|
2948
|
+
import { homedir } from "os";
|
|
2949
|
+
var CONFIG_DIR = join(homedir(), ".agent-tunnel");
|
|
2950
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
2951
|
+
var DEFAULTS = {
|
|
2952
|
+
apiUrl: "http://localhost:8080",
|
|
2953
|
+
wsPath: "/ws",
|
|
2954
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
2955
|
+
allowedPaths: [homedir()],
|
|
2956
|
+
allowedCommands: [],
|
|
2957
|
+
blockedCommands: [],
|
|
2958
|
+
blockedPaths: [
|
|
2959
|
+
"/etc/shadow",
|
|
2960
|
+
"/etc/passwd",
|
|
2961
|
+
"/etc/sudoers",
|
|
2962
|
+
"/etc/ssh",
|
|
2963
|
+
"/root/.ssh",
|
|
2964
|
+
"/proc",
|
|
2965
|
+
"/sys",
|
|
2966
|
+
"/dev"
|
|
2967
|
+
],
|
|
2968
|
+
workingDir: homedir(),
|
|
2969
|
+
shellTimeout: 3e4,
|
|
2970
|
+
shellMaxTimeout: 12e4,
|
|
2971
|
+
shellMaxOutputSize: 1024 * 1024,
|
|
2972
|
+
shellEnvPassthrough: ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "NODE_ENV", "HOSTNAME"]
|
|
2973
|
+
};
|
|
2974
|
+
function loadConfig(overrides = {}) {
|
|
2975
|
+
let fileConfig = {};
|
|
2976
|
+
if (existsSync(CONFIG_FILE)) {
|
|
2977
|
+
try {
|
|
2978
|
+
fileConfig = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
2979
|
+
} catch (err) {
|
|
2980
|
+
console.warn(`[config] Failed to parse ${CONFIG_FILE}:`, err);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
const envConfig = {};
|
|
2984
|
+
if (process.env.TUNNEL_TOKEN) envConfig.token = process.env.TUNNEL_TOKEN;
|
|
2985
|
+
if (process.env.TUNNEL_ID) envConfig.tunnelId = process.env.TUNNEL_ID;
|
|
2986
|
+
if (process.env.TUNNEL_API_URL) envConfig.apiUrl = process.env.TUNNEL_API_URL;
|
|
2987
|
+
if (process.env.TUNNEL_WS_PATH) envConfig.wsPath = process.env.TUNNEL_WS_PATH;
|
|
2988
|
+
if (process.env.TUNNEL_MAX_FILE_SIZE) envConfig.maxFileSize = parseInt(process.env.TUNNEL_MAX_FILE_SIZE, 10);
|
|
2989
|
+
const merged = {
|
|
2990
|
+
...DEFAULTS,
|
|
2991
|
+
...fileConfig,
|
|
2992
|
+
...envConfig,
|
|
2993
|
+
...overrides
|
|
2994
|
+
};
|
|
2995
|
+
return merged;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
// src/agent/agent.ts
|
|
2999
|
+
import { hostname, platform, arch, release } from "os";
|
|
3000
|
+
|
|
3001
|
+
// src/agent/security/permission-guard.ts
|
|
3002
|
+
var PermissionGuard = class {
|
|
3003
|
+
permissions = /* @__PURE__ */ new Map();
|
|
3004
|
+
hasSynced = false;
|
|
3005
|
+
/** Bulk-load permissions from server sync notification. */
|
|
3006
|
+
syncPermissions(permissions) {
|
|
3007
|
+
this.permissions.clear();
|
|
3008
|
+
for (const perm of permissions) {
|
|
3009
|
+
this.permissions.set(perm.permissionId, perm);
|
|
3010
|
+
}
|
|
3011
|
+
this.hasSynced = true;
|
|
3012
|
+
}
|
|
3013
|
+
addPermission(permission) {
|
|
3014
|
+
this.permissions.set(permission.permissionId, permission);
|
|
3015
|
+
}
|
|
3016
|
+
revokePermission(permissionId) {
|
|
3017
|
+
this.permissions.delete(permissionId);
|
|
3018
|
+
}
|
|
3019
|
+
checkPermission(permissionId) {
|
|
3020
|
+
if (!permissionId) {
|
|
3021
|
+
return false;
|
|
3022
|
+
}
|
|
3023
|
+
const perm = this.permissions.get(permissionId);
|
|
3024
|
+
if (!perm) {
|
|
3025
|
+
return false;
|
|
3026
|
+
}
|
|
3027
|
+
if (perm.expiresAt) {
|
|
3028
|
+
const expiry = new Date(perm.expiresAt).getTime();
|
|
3029
|
+
if (isNaN(expiry) || expiry < Date.now()) {
|
|
3030
|
+
this.permissions.delete(permissionId);
|
|
3031
|
+
return false;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
return true;
|
|
3035
|
+
}
|
|
3036
|
+
clear() {
|
|
3037
|
+
this.permissions.clear();
|
|
3038
|
+
this.hasSynced = false;
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
|
|
3042
|
+
// src/shared/crypto.ts
|
|
3043
|
+
import { createHash, createHmac, timingSafeEqual, randomBytes } from "crypto";
|
|
3044
|
+
function signMessage(signingKey, payload, nonce) {
|
|
3045
|
+
return createHmac("sha256", signingKey).update(`${nonce}:${payload}`).digest("hex");
|
|
3046
|
+
}
|
|
3047
|
+
function verifyMessageSignature(signingKey, payload, nonce, signature) {
|
|
3048
|
+
try {
|
|
3049
|
+
const expected = signMessage(signingKey, payload, nonce);
|
|
3050
|
+
const sigBuffer = Buffer.from(signature, "hex");
|
|
3051
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
3052
|
+
if (sigBuffer.length !== expectedBuffer.length) return false;
|
|
3053
|
+
return timingSafeEqual(sigBuffer, expectedBuffer);
|
|
3054
|
+
} catch {
|
|
3055
|
+
return false;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/agent/agent.ts
|
|
3060
|
+
var c = {
|
|
3061
|
+
reset: "\x1B[0m",
|
|
3062
|
+
bold: "\x1B[1m",
|
|
3063
|
+
dim: "\x1B[2m",
|
|
3064
|
+
cyan: "\x1B[36m",
|
|
3065
|
+
green: "\x1B[32m",
|
|
3066
|
+
yellow: "\x1B[33m",
|
|
3067
|
+
red: "\x1B[31m",
|
|
3068
|
+
white: "\x1B[97m",
|
|
3069
|
+
gray: "\x1B[90m"
|
|
3070
|
+
};
|
|
3071
|
+
function log(icon, msg) {
|
|
3072
|
+
console.log(` ${icon} ${c.dim}${msg}${c.reset}`);
|
|
3073
|
+
}
|
|
3074
|
+
var TunnelAgent = class {
|
|
3075
|
+
ws = null;
|
|
3076
|
+
registry;
|
|
3077
|
+
permissionGuard;
|
|
3078
|
+
config;
|
|
3079
|
+
reconnectAttempts = 0;
|
|
3080
|
+
maxReconnectDelay = 3e4;
|
|
3081
|
+
baseReconnectDelay = 1e3;
|
|
3082
|
+
reconnectTimer = null;
|
|
3083
|
+
isShuttingDown = false;
|
|
3084
|
+
uptime = 0;
|
|
3085
|
+
uptimeInterval = null;
|
|
3086
|
+
// HMAC signature verification
|
|
3087
|
+
signingKey = null;
|
|
3088
|
+
lastNonce = 0;
|
|
3089
|
+
responseNonce = 0;
|
|
3090
|
+
constructor(config, registry) {
|
|
3091
|
+
this.config = config;
|
|
3092
|
+
this.registry = registry;
|
|
3093
|
+
this.permissionGuard = new PermissionGuard();
|
|
3094
|
+
}
|
|
3095
|
+
connect() {
|
|
3096
|
+
if (this.ws) {
|
|
3097
|
+
this.ws.close();
|
|
3098
|
+
}
|
|
3099
|
+
const wsUrl = this.buildWsUrl();
|
|
3100
|
+
log(`${c.cyan}\u25C6${c.reset}`, `Connecting\u2026`);
|
|
3101
|
+
try {
|
|
3102
|
+
this.ws = new WebSocket(wsUrl);
|
|
3103
|
+
this.setupWsHandlers();
|
|
3104
|
+
} catch (err) {
|
|
3105
|
+
log(`${c.red}\u2717${c.reset}`, `Connection failed`);
|
|
3106
|
+
this.scheduleReconnect();
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
disconnect() {
|
|
3110
|
+
this.isShuttingDown = true;
|
|
3111
|
+
if (this.reconnectTimer) {
|
|
3112
|
+
clearTimeout(this.reconnectTimer);
|
|
3113
|
+
this.reconnectTimer = null;
|
|
3114
|
+
}
|
|
3115
|
+
if (this.uptimeInterval) {
|
|
3116
|
+
clearInterval(this.uptimeInterval);
|
|
3117
|
+
this.uptimeInterval = null;
|
|
3118
|
+
}
|
|
3119
|
+
if (this.ws) {
|
|
3120
|
+
try {
|
|
3121
|
+
this.ws.close(1e3, "client shutdown");
|
|
3122
|
+
} catch {
|
|
3123
|
+
}
|
|
3124
|
+
this.ws = null;
|
|
3125
|
+
}
|
|
3126
|
+
this.permissionGuard.clear();
|
|
3127
|
+
log(`${c.gray}\u25CB${c.reset}`, `Disconnected`);
|
|
3128
|
+
}
|
|
3129
|
+
isConnected() {
|
|
3130
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
3131
|
+
}
|
|
3132
|
+
setupWsHandlers() {
|
|
3133
|
+
if (!this.ws) return;
|
|
3134
|
+
this.ws.addEventListener("open", () => {
|
|
3135
|
+
this.reconnectAttempts = 0;
|
|
3136
|
+
this.uptime = 0;
|
|
3137
|
+
this.lastNonce = 0;
|
|
3138
|
+
this.responseNonce = 0;
|
|
3139
|
+
this.signingKey = null;
|
|
3140
|
+
this.uptimeInterval = setInterval(() => {
|
|
3141
|
+
this.uptime++;
|
|
3142
|
+
}, 1e3);
|
|
3143
|
+
this.send({ type: "auth", token: this.config.token });
|
|
3144
|
+
});
|
|
3145
|
+
this.ws.addEventListener("message", (event) => {
|
|
3146
|
+
this.handleMessage(event.data);
|
|
3147
|
+
});
|
|
3148
|
+
this.ws.addEventListener("close", (event) => {
|
|
3149
|
+
if (this.uptimeInterval) {
|
|
3150
|
+
clearInterval(this.uptimeInterval);
|
|
3151
|
+
this.uptimeInterval = null;
|
|
3152
|
+
}
|
|
3153
|
+
if (!this.isShuttingDown) {
|
|
3154
|
+
if (event.code === 4001) {
|
|
3155
|
+
log(`${c.red}\u2717${c.reset}`, `Authentication failed \u2014 check your token`);
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
log(`${c.yellow}\u25CB${c.reset}`, `Disconnected ${c.gray}(code: ${event.code})${c.reset}`);
|
|
3159
|
+
this.scheduleReconnect();
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
this.ws.addEventListener("error", (event) => {
|
|
3163
|
+
log(`${c.red}\u2717${c.reset}`, `WebSocket error`);
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
async handleMessage(raw) {
|
|
3167
|
+
let msg;
|
|
3168
|
+
try {
|
|
3169
|
+
msg = JSON.parse(raw);
|
|
3170
|
+
} catch {
|
|
3171
|
+
log(`${c.yellow}!${c.reset}`, `Received invalid JSON`);
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
if (msg.type === "auth_ok" && msg.signingKey) {
|
|
3175
|
+
this.signingKey = msg.signingKey;
|
|
3176
|
+
log(`${c.green}\u25CF${c.reset}`, `Connected ${c.reset}${c.gray}(${this.registry.getCapabilityNames().join(", ")})${c.reset}`);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (!this.signingKey) {
|
|
3180
|
+
log(`${c.yellow}!${c.reset}`, `Message received before auth completed`);
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
if (!this.verifyIncomingSignature(msg, raw)) {
|
|
3184
|
+
if ("id" in msg && msg.id) {
|
|
3185
|
+
this.sendSignedError(msg.id, -32e3, "Invalid message signature");
|
|
3186
|
+
}
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
if ("method" in msg && msg.method === "tunnel.ping") {
|
|
3190
|
+
this.sendPong();
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if ("method" in msg && msg.method === "tunnel.permissions.sync") {
|
|
3194
|
+
const permissions = msg.params?.permissions || [];
|
|
3195
|
+
this.permissionGuard.syncPermissions(permissions);
|
|
3196
|
+
log(`${c.green}\u25CF${c.reset}`, `Synced ${c.reset}${c.white}${permissions.length}${c.dim} permissions`);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
if ("method" in msg && msg.method === "tunnel.permission.granted") {
|
|
3200
|
+
const p = msg.params;
|
|
3201
|
+
if (p?.permissionId) {
|
|
3202
|
+
this.permissionGuard.addPermission(p);
|
|
3203
|
+
log(`${c.green}+${c.reset}`, `Permission granted: ${p.capability} (${p.permissionId.slice(0, 12)}\u2026)`);
|
|
3204
|
+
}
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
if ("method" in msg && msg.method === "tunnel.permission.revoked") {
|
|
3208
|
+
const permissionId = msg.params?.permissionId;
|
|
3209
|
+
if (permissionId) {
|
|
3210
|
+
this.permissionGuard.revokePermission(permissionId);
|
|
3211
|
+
log(`${c.yellow}\u25CB${c.reset}`, `Permission revoked: ${permissionId.slice(0, 12)}\u2026`);
|
|
3212
|
+
}
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
if ("method" in msg && msg.method === "tunnel.token.rotated") {
|
|
3216
|
+
log(`${c.yellow}!${c.reset}`, `Token rotated \u2014 reconnecting with new token`);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
if ("id" in msg && msg.id) {
|
|
3220
|
+
await this.handleRpcRequest(msg);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* Verify HMAC signature on incoming messages (excluding pings).
|
|
3226
|
+
*/
|
|
3227
|
+
verifyIncomingSignature(msg, _raw) {
|
|
3228
|
+
const sig = msg._sig;
|
|
3229
|
+
const nonce = msg._nonce;
|
|
3230
|
+
if (sig === void 0 || nonce === void 0) {
|
|
3231
|
+
log(`${c.yellow}!${c.reset}`, `Message missing signature fields`);
|
|
3232
|
+
return false;
|
|
3233
|
+
}
|
|
3234
|
+
if (nonce <= this.lastNonce) {
|
|
3235
|
+
log(`${c.red}\u2717${c.reset}`, `Replay detected: nonce ${nonce} <= ${this.lastNonce}`);
|
|
3236
|
+
return false;
|
|
3237
|
+
}
|
|
3238
|
+
const { _sig, _nonce, ...payloadObj } = msg;
|
|
3239
|
+
const payload = JSON.stringify(payloadObj);
|
|
3240
|
+
if (!verifyMessageSignature(this.signingKey, payload, nonce, sig)) {
|
|
3241
|
+
log(`${c.red}\u2717${c.reset}`, `Invalid HMAC signature`);
|
|
3242
|
+
return false;
|
|
3243
|
+
}
|
|
3244
|
+
this.lastNonce = nonce;
|
|
3245
|
+
return true;
|
|
3246
|
+
}
|
|
3247
|
+
async handleRpcRequest(request) {
|
|
3248
|
+
const { id, method, params = {} } = request;
|
|
3249
|
+
const permissionId = params.permissionId;
|
|
3250
|
+
if (!this.permissionGuard.checkPermission(permissionId)) {
|
|
3251
|
+
this.sendSignedError(id, -32e3, `Permission denied: ${permissionId ? "invalid or expired permission" : "no permissionId provided"}`);
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
const handler = this.registry.getHandler(method);
|
|
3255
|
+
if (!handler) {
|
|
3256
|
+
this.sendSignedError(id, -32001, `Capability not registered for method: ${method}`);
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
try {
|
|
3260
|
+
const result = await handler(params);
|
|
3261
|
+
this.sendSignedResult(id, result);
|
|
3262
|
+
} catch (err) {
|
|
3263
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3264
|
+
this.sendSignedError(id, -32003, message);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
/** Send HMAC-signed RPC result. */
|
|
3268
|
+
sendSignedResult(id, result) {
|
|
3269
|
+
const data = { jsonrpc: "2.0", id, result };
|
|
3270
|
+
this.sendSigned(data);
|
|
3271
|
+
}
|
|
3272
|
+
/** Send HMAC-signed RPC error. */
|
|
3273
|
+
sendSignedError(id, code, message) {
|
|
3274
|
+
const data = { jsonrpc: "2.0", id, error: { code, message } };
|
|
3275
|
+
this.sendSigned(data);
|
|
3276
|
+
}
|
|
3277
|
+
sendSigned(data) {
|
|
3278
|
+
if (this.ws?.readyState === WebSocket.OPEN && this.signingKey) {
|
|
3279
|
+
const nonce = ++this.responseNonce;
|
|
3280
|
+
const payload = JSON.stringify(data);
|
|
3281
|
+
const sig = signMessage(this.signingKey, payload, nonce);
|
|
3282
|
+
const signed = { ...data, _sig: sig, _nonce: nonce };
|
|
3283
|
+
try {
|
|
3284
|
+
this.ws.send(JSON.stringify(signed));
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
log(`${c.red}\u2717${c.reset}`, `Send failed`);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
send(data) {
|
|
3291
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
3292
|
+
try {
|
|
3293
|
+
this.ws.send(JSON.stringify(data));
|
|
3294
|
+
} catch (err) {
|
|
3295
|
+
log(`${c.red}\u2717${c.reset}`, `Send failed`);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
sendPong() {
|
|
3300
|
+
this.sendSigned({
|
|
3301
|
+
jsonrpc: "2.0",
|
|
3302
|
+
method: "tunnel.pong",
|
|
3303
|
+
params: {
|
|
3304
|
+
uptime: this.uptime,
|
|
3305
|
+
capabilities: this.registry.getCapabilityNames(),
|
|
3306
|
+
machineInfo: {
|
|
3307
|
+
hostname: hostname(),
|
|
3308
|
+
platform: platform(),
|
|
3309
|
+
arch: arch(),
|
|
3310
|
+
osVersion: release(),
|
|
3311
|
+
agentVersion: "0.1.2"
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
scheduleReconnect() {
|
|
3317
|
+
if (this.isShuttingDown) return;
|
|
3318
|
+
this.reconnectAttempts++;
|
|
3319
|
+
const delay = Math.min(
|
|
3320
|
+
this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
3321
|
+
this.maxReconnectDelay
|
|
3322
|
+
);
|
|
3323
|
+
log(`${c.cyan}\u25C6${c.reset}`, `Reconnecting in ${c.reset}${c.white}${(delay / 1e3).toFixed(1)}s${c.dim} (attempt ${this.reconnectAttempts})`);
|
|
3324
|
+
this.reconnectTimer = setTimeout(() => {
|
|
3325
|
+
this.connect();
|
|
3326
|
+
}, delay);
|
|
3327
|
+
}
|
|
3328
|
+
buildWsUrl() {
|
|
3329
|
+
const base = this.config.apiUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
3330
|
+
if (base.startsWith("ws://") && !base.includes("localhost") && !base.includes("127.0.0.1")) {
|
|
3331
|
+
log(`${c.red}!${c.reset}`, `${c.red}WARNING: Connecting over unencrypted ws:// to a remote host. Token will be sent in plaintext. Use https:// API URL for production.${c.reset}`);
|
|
3332
|
+
}
|
|
3333
|
+
const wsPath = this.config.wsPath || "/ws";
|
|
3334
|
+
const params = new URLSearchParams({
|
|
3335
|
+
tunnelId: this.config.tunnelId
|
|
3336
|
+
});
|
|
3337
|
+
return `${base}${wsPath}?${params.toString()}`;
|
|
3338
|
+
}
|
|
3339
|
+
};
|
|
3340
|
+
|
|
3341
|
+
// src/agent/capabilities/index.ts
|
|
3342
|
+
var CapabilityRegistry = class {
|
|
3343
|
+
capabilities = /* @__PURE__ */ new Map();
|
|
3344
|
+
register(capability) {
|
|
3345
|
+
this.capabilities.set(capability.name, capability);
|
|
3346
|
+
}
|
|
3347
|
+
unregister(name) {
|
|
3348
|
+
this.capabilities.delete(name);
|
|
3349
|
+
}
|
|
3350
|
+
getHandler(method) {
|
|
3351
|
+
for (const cap of this.capabilities.values()) {
|
|
3352
|
+
const handler = cap.methods.get(method);
|
|
3353
|
+
if (handler) return handler;
|
|
3354
|
+
}
|
|
3355
|
+
return null;
|
|
3356
|
+
}
|
|
3357
|
+
getCapabilityNames() {
|
|
3358
|
+
return Array.from(this.capabilities.keys());
|
|
3359
|
+
}
|
|
3360
|
+
has(name) {
|
|
3361
|
+
return this.capabilities.has(name);
|
|
3362
|
+
}
|
|
3363
|
+
};
|
|
3364
|
+
|
|
3365
|
+
// src/agent/capabilities/filesystem.ts
|
|
3366
|
+
import { readFile, writeFile, readdir, stat, unlink, mkdir } from "fs/promises";
|
|
3367
|
+
import { join as join2, dirname } from "path";
|
|
3368
|
+
|
|
3369
|
+
// src/agent/security/path-validator.ts
|
|
3370
|
+
import { resolve, normalize } from "path";
|
|
3371
|
+
import { realpathSync } from "fs";
|
|
3372
|
+
function validatePath(path, allowedPaths, blockedPaths = []) {
|
|
3373
|
+
if (!path) {
|
|
3374
|
+
throw new Error("Path is required");
|
|
3375
|
+
}
|
|
3376
|
+
const normalized = normalize(resolve(path));
|
|
3377
|
+
let resolved;
|
|
3378
|
+
try {
|
|
3379
|
+
resolved = realpathSync(normalized);
|
|
3380
|
+
} catch (err) {
|
|
3381
|
+
const code = err.code;
|
|
3382
|
+
if (code === "ENOENT") {
|
|
3383
|
+
resolved = normalized;
|
|
3384
|
+
} else {
|
|
3385
|
+
throw new Error(`Access denied: cannot resolve path "${path}" (${code})`);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
for (const blocked of blockedPaths) {
|
|
3389
|
+
if (resolved === blocked || resolved.startsWith(blocked + "/")) {
|
|
3390
|
+
throw new Error(`Access denied: blocked path "${path}"`);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
if (allowedPaths.length > 0) {
|
|
3394
|
+
const withinAllowed = allowedPaths.some((allowed) => {
|
|
3395
|
+
const normalizedAllowed = normalize(resolve(allowed));
|
|
3396
|
+
return resolved === normalizedAllowed || resolved.startsWith(normalizedAllowed + "/");
|
|
3397
|
+
});
|
|
3398
|
+
if (!withinAllowed) {
|
|
3399
|
+
throw new Error(`Access denied: path "${path}" is outside allowed directories`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// src/agent/capabilities/filesystem.ts
|
|
3405
|
+
function createFilesystemCapability(config) {
|
|
3406
|
+
const methods = /* @__PURE__ */ new Map();
|
|
3407
|
+
methods.set("fs.read", async (params) => {
|
|
3408
|
+
const path = params.path;
|
|
3409
|
+
const encoding = params.encoding || "utf-8";
|
|
3410
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
3411
|
+
const stats = await stat(path);
|
|
3412
|
+
if (stats.size > config.maxFileSize) {
|
|
3413
|
+
throw new Error(`File exceeds max size (${stats.size} > ${config.maxFileSize})`);
|
|
3414
|
+
}
|
|
3415
|
+
const content = await readFile(path, { encoding });
|
|
3416
|
+
return {
|
|
3417
|
+
content,
|
|
3418
|
+
size: stats.size,
|
|
3419
|
+
encoding
|
|
3420
|
+
};
|
|
3421
|
+
});
|
|
3422
|
+
methods.set("fs.write", async (params) => {
|
|
3423
|
+
const path = params.path;
|
|
3424
|
+
const content = params.content;
|
|
3425
|
+
const encoding = params.encoding || "utf-8";
|
|
3426
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
3427
|
+
if (content.length > config.maxFileSize) {
|
|
3428
|
+
throw new Error(`Content exceeds max size (${content.length} > ${config.maxFileSize})`);
|
|
3429
|
+
}
|
|
3430
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3431
|
+
await writeFile(path, content, { encoding });
|
|
3432
|
+
const stats = await stat(path);
|
|
3433
|
+
return {
|
|
3434
|
+
size: stats.size,
|
|
3435
|
+
path
|
|
3436
|
+
};
|
|
3437
|
+
});
|
|
3438
|
+
methods.set("fs.list", async (params) => {
|
|
3439
|
+
const path = params.path;
|
|
3440
|
+
const recursive = params.recursive || false;
|
|
3441
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
3442
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
3443
|
+
const result = entries.map((entry) => ({
|
|
3444
|
+
name: entry.name,
|
|
3445
|
+
path: join2(path, entry.name),
|
|
3446
|
+
isDirectory: entry.isDirectory(),
|
|
3447
|
+
isFile: entry.isFile(),
|
|
3448
|
+
isSymlink: entry.isSymbolicLink()
|
|
3449
|
+
}));
|
|
3450
|
+
if (recursive) {
|
|
3451
|
+
const dirs = result.filter((e) => e.isDirectory);
|
|
3452
|
+
for (const dir of dirs) {
|
|
3453
|
+
try {
|
|
3454
|
+
const subEntries = await readdir(dir.path, { withFileTypes: true });
|
|
3455
|
+
for (const sub of subEntries) {
|
|
3456
|
+
result.push({
|
|
3457
|
+
name: sub.name,
|
|
3458
|
+
path: join2(dir.path, sub.name),
|
|
3459
|
+
isDirectory: sub.isDirectory(),
|
|
3460
|
+
isFile: sub.isFile(),
|
|
3461
|
+
isSymlink: sub.isSymbolicLink()
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
} catch {
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return { entries: result, count: result.length };
|
|
3469
|
+
});
|
|
3470
|
+
methods.set("fs.stat", async (params) => {
|
|
3471
|
+
const path = params.path;
|
|
3472
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
3473
|
+
const stats = await stat(path);
|
|
3474
|
+
return {
|
|
3475
|
+
size: stats.size,
|
|
3476
|
+
isDirectory: stats.isDirectory(),
|
|
3477
|
+
isFile: stats.isFile(),
|
|
3478
|
+
isSymlink: stats.isSymbolicLink(),
|
|
3479
|
+
mode: stats.mode,
|
|
3480
|
+
mtime: stats.mtime.toISOString(),
|
|
3481
|
+
ctime: stats.ctime.toISOString(),
|
|
3482
|
+
atime: stats.atime.toISOString()
|
|
3483
|
+
};
|
|
3484
|
+
});
|
|
3485
|
+
methods.set("fs.delete", async (params) => {
|
|
3486
|
+
const path = params.path;
|
|
3487
|
+
validatePath(path, config.allowedPaths, config.blockedPaths);
|
|
3488
|
+
await unlink(path);
|
|
3489
|
+
return { deleted: true, path };
|
|
3490
|
+
});
|
|
3491
|
+
return {
|
|
3492
|
+
name: "filesystem",
|
|
3493
|
+
methods
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// src/agent/capabilities/shell.ts
|
|
3498
|
+
import { spawn } from "child_process";
|
|
3499
|
+
|
|
3500
|
+
// src/agent/security/command-validator.ts
|
|
3501
|
+
var SHELL_METACHAR_REGEX = /[;&|`$(){}[\]<>!#~]/;
|
|
3502
|
+
function validateCommand(command2, allowedCommands, blockedCommands) {
|
|
3503
|
+
if (!command2 || typeof command2 !== "string") {
|
|
3504
|
+
throw new Error("Command is required");
|
|
3505
|
+
}
|
|
3506
|
+
const trimmed = command2.trim();
|
|
3507
|
+
if (SHELL_METACHAR_REGEX.test(trimmed)) {
|
|
3508
|
+
throw new Error(`Command contains disallowed characters: "${trimmed}"`);
|
|
3509
|
+
}
|
|
3510
|
+
const executable = trimmed.split(/\s+/)[0];
|
|
3511
|
+
if (blockedCommands.length > 0 && blockedCommands.includes(executable)) {
|
|
3512
|
+
throw new Error(`Command "${executable}" is blocked`);
|
|
3513
|
+
}
|
|
3514
|
+
if (allowedCommands.length > 0) {
|
|
3515
|
+
if (!allowedCommands.includes(executable)) {
|
|
3516
|
+
throw new Error(`Command "${executable}" is not in the allowed commands list`);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
// src/agent/capabilities/shell.ts
|
|
3522
|
+
function createShellCapability(config) {
|
|
3523
|
+
const methods = /* @__PURE__ */ new Map();
|
|
3524
|
+
methods.set("shell.exec", async (params) => {
|
|
3525
|
+
const command2 = params.command;
|
|
3526
|
+
const args = params.args || [];
|
|
3527
|
+
const cwd = params.cwd || config.workingDir;
|
|
3528
|
+
const timeout = Math.min(
|
|
3529
|
+
params.timeout || config.shellTimeout,
|
|
3530
|
+
config.shellMaxTimeout
|
|
3531
|
+
);
|
|
3532
|
+
validateCommand(command2, config.allowedCommands, config.blockedCommands);
|
|
3533
|
+
if (cwd) {
|
|
3534
|
+
validatePath(cwd, config.allowedPaths, config.blockedPaths);
|
|
3535
|
+
}
|
|
3536
|
+
const safeEnv = { TERM: "dumb" };
|
|
3537
|
+
for (const key of config.shellEnvPassthrough) {
|
|
3538
|
+
if (process.env[key]) {
|
|
3539
|
+
safeEnv[key] = process.env[key];
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
return new Promise((resolve2, reject) => {
|
|
3543
|
+
const proc = spawn(command2, args, {
|
|
3544
|
+
cwd,
|
|
3545
|
+
shell: false,
|
|
3546
|
+
timeout,
|
|
3547
|
+
env: safeEnv
|
|
3548
|
+
});
|
|
3549
|
+
let stdout = "";
|
|
3550
|
+
let stderr = "";
|
|
3551
|
+
let stdoutTruncated = false;
|
|
3552
|
+
let stderrTruncated = false;
|
|
3553
|
+
proc.stdout?.on("data", (data) => {
|
|
3554
|
+
if (stdout.length >= config.shellMaxOutputSize) {
|
|
3555
|
+
stdoutTruncated = true;
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
const chunk = data.toString();
|
|
3559
|
+
const remaining = config.shellMaxOutputSize - stdout.length;
|
|
3560
|
+
if (chunk.length > remaining) {
|
|
3561
|
+
stdout += chunk.slice(0, remaining);
|
|
3562
|
+
stdoutTruncated = true;
|
|
3563
|
+
} else {
|
|
3564
|
+
stdout += chunk;
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
proc.stderr?.on("data", (data) => {
|
|
3568
|
+
if (stderr.length >= config.shellMaxOutputSize) {
|
|
3569
|
+
stderrTruncated = true;
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
const chunk = data.toString();
|
|
3573
|
+
const remaining = config.shellMaxOutputSize - stderr.length;
|
|
3574
|
+
if (chunk.length > remaining) {
|
|
3575
|
+
stderr += chunk.slice(0, remaining);
|
|
3576
|
+
stderrTruncated = true;
|
|
3577
|
+
} else {
|
|
3578
|
+
stderr += chunk;
|
|
3579
|
+
}
|
|
3580
|
+
});
|
|
3581
|
+
proc.on("error", (err) => {
|
|
3582
|
+
reject(new Error(`Command failed to start: ${err.message}`));
|
|
3583
|
+
});
|
|
3584
|
+
proc.on("close", (code, signal) => {
|
|
3585
|
+
resolve2({
|
|
3586
|
+
exitCode: code,
|
|
3587
|
+
signal,
|
|
3588
|
+
stdout,
|
|
3589
|
+
stderr,
|
|
3590
|
+
stdoutTruncated,
|
|
3591
|
+
stderrTruncated
|
|
3592
|
+
});
|
|
3593
|
+
});
|
|
3594
|
+
});
|
|
3595
|
+
});
|
|
3596
|
+
return {
|
|
3597
|
+
name: "shell",
|
|
3598
|
+
methods
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
// src/agent/capabilities/desktop.ts
|
|
3603
|
+
import { platform as platform2 } from "os";
|
|
3604
|
+
function createDriver() {
|
|
3605
|
+
switch (platform2()) {
|
|
3606
|
+
case "darwin": {
|
|
3607
|
+
const { MacOSDriver: MacOSDriver2 } = (init_macos_driver(), __toCommonJS(macos_driver_exports));
|
|
3608
|
+
return new MacOSDriver2();
|
|
3609
|
+
}
|
|
3610
|
+
case "win32": {
|
|
3611
|
+
const { WindowsDriver: WindowsDriver2 } = (init_windows_driver(), __toCommonJS(windows_driver_exports));
|
|
3612
|
+
return new WindowsDriver2();
|
|
3613
|
+
}
|
|
3614
|
+
default: {
|
|
3615
|
+
const { LinuxDriver: LinuxDriver2 } = (init_linux_driver(), __toCommonJS(linux_driver_exports));
|
|
3616
|
+
return new LinuxDriver2();
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
function createDesktopCapability() {
|
|
3621
|
+
const driver = createDriver();
|
|
3622
|
+
const methods = /* @__PURE__ */ new Map();
|
|
3623
|
+
methods.set("desktop.screenshot", async (params) => {
|
|
3624
|
+
return driver.screenshot({
|
|
3625
|
+
region: params.region,
|
|
3626
|
+
windowId: params.windowId
|
|
3627
|
+
});
|
|
3628
|
+
});
|
|
3629
|
+
methods.set("desktop.mouse.click", async (params) => {
|
|
3630
|
+
await driver.mouseClick({
|
|
3631
|
+
x: params.x,
|
|
3632
|
+
y: params.y,
|
|
3633
|
+
button: params.button,
|
|
3634
|
+
clicks: params.clicks,
|
|
3635
|
+
modifiers: params.modifiers
|
|
3636
|
+
});
|
|
3637
|
+
return { ok: true };
|
|
3638
|
+
});
|
|
3639
|
+
methods.set("desktop.mouse.move", async (params) => {
|
|
3640
|
+
await driver.mouseMove({
|
|
3641
|
+
x: params.x,
|
|
3642
|
+
y: params.y
|
|
3643
|
+
});
|
|
3644
|
+
return { ok: true };
|
|
3645
|
+
});
|
|
3646
|
+
methods.set("desktop.mouse.drag", async (params) => {
|
|
3647
|
+
await driver.mouseDrag({
|
|
3648
|
+
fromX: params.fromX,
|
|
3649
|
+
fromY: params.fromY,
|
|
3650
|
+
toX: params.toX,
|
|
3651
|
+
toY: params.toY,
|
|
3652
|
+
button: params.button
|
|
3653
|
+
});
|
|
3654
|
+
return { ok: true };
|
|
3655
|
+
});
|
|
3656
|
+
methods.set("desktop.mouse.scroll", async (params) => {
|
|
3657
|
+
await driver.mouseScroll({
|
|
3658
|
+
x: params.x,
|
|
3659
|
+
y: params.y,
|
|
3660
|
+
deltaX: params.deltaX,
|
|
3661
|
+
deltaY: params.deltaY
|
|
3662
|
+
});
|
|
3663
|
+
return { ok: true };
|
|
3664
|
+
});
|
|
3665
|
+
methods.set("desktop.mouse.position", async () => {
|
|
3666
|
+
return driver.mousePosition();
|
|
3667
|
+
});
|
|
3668
|
+
methods.set("desktop.keyboard.type", async (params) => {
|
|
3669
|
+
await driver.keyboardType({
|
|
3670
|
+
text: params.text,
|
|
3671
|
+
delay: params.delay
|
|
3672
|
+
});
|
|
3673
|
+
return { ok: true };
|
|
3674
|
+
});
|
|
3675
|
+
methods.set("desktop.keyboard.key", async (params) => {
|
|
3676
|
+
await driver.keyboardKey({
|
|
3677
|
+
keys: params.keys
|
|
3678
|
+
});
|
|
3679
|
+
return { ok: true };
|
|
3680
|
+
});
|
|
3681
|
+
methods.set("desktop.window.list", async () => {
|
|
3682
|
+
return { windows: await driver.windowList() };
|
|
3683
|
+
});
|
|
3684
|
+
methods.set("desktop.window.focus", async (params) => {
|
|
3685
|
+
await driver.windowFocus(params.windowId);
|
|
3686
|
+
return { ok: true };
|
|
3687
|
+
});
|
|
3688
|
+
methods.set("desktop.window.resize", async (params) => {
|
|
3689
|
+
await driver.windowResize(params.windowId, {
|
|
3690
|
+
x: params.x,
|
|
3691
|
+
y: params.y,
|
|
3692
|
+
width: params.width,
|
|
3693
|
+
height: params.height
|
|
3694
|
+
});
|
|
3695
|
+
return { ok: true };
|
|
3696
|
+
});
|
|
3697
|
+
methods.set("desktop.window.close", async (params) => {
|
|
3698
|
+
await driver.windowClose(params.windowId);
|
|
3699
|
+
return { ok: true };
|
|
3700
|
+
});
|
|
3701
|
+
methods.set("desktop.window.minimize", async (params) => {
|
|
3702
|
+
await driver.windowMinimize(params.windowId);
|
|
3703
|
+
return { ok: true };
|
|
3704
|
+
});
|
|
3705
|
+
methods.set("desktop.app.launch", async (params) => {
|
|
3706
|
+
await driver.appLaunch(params.app);
|
|
3707
|
+
return { ok: true };
|
|
3708
|
+
});
|
|
3709
|
+
methods.set("desktop.app.quit", async (params) => {
|
|
3710
|
+
await driver.appQuit(params.app);
|
|
3711
|
+
return { ok: true };
|
|
3712
|
+
});
|
|
3713
|
+
methods.set("desktop.app.list", async () => {
|
|
3714
|
+
return { apps: await driver.appList() };
|
|
3715
|
+
});
|
|
3716
|
+
methods.set("desktop.clipboard.read", async () => {
|
|
3717
|
+
return { text: await driver.clipboardRead() };
|
|
3718
|
+
});
|
|
3719
|
+
methods.set("desktop.clipboard.write", async (params) => {
|
|
3720
|
+
await driver.clipboardWrite(params.text);
|
|
3721
|
+
return { ok: true };
|
|
3722
|
+
});
|
|
3723
|
+
methods.set("desktop.screen.info", async () => {
|
|
3724
|
+
return driver.screenInfo();
|
|
3725
|
+
});
|
|
3726
|
+
methods.set("desktop.cursor.image", async (params) => {
|
|
3727
|
+
return driver.cursorImage(params.radius);
|
|
3728
|
+
});
|
|
3729
|
+
methods.set("desktop.ax.tree", async (params) => {
|
|
3730
|
+
return driver.axTree({
|
|
3731
|
+
pid: params.pid,
|
|
3732
|
+
maxDepth: params.maxDepth,
|
|
3733
|
+
roles: params.roles
|
|
3734
|
+
});
|
|
3735
|
+
});
|
|
3736
|
+
methods.set("desktop.ax.action", async (params) => {
|
|
3737
|
+
return driver.axAction({
|
|
3738
|
+
elementId: params.elementId,
|
|
3739
|
+
action: params.action,
|
|
3740
|
+
pid: params.pid
|
|
3741
|
+
});
|
|
3742
|
+
});
|
|
3743
|
+
methods.set("desktop.ax.set_value", async (params) => {
|
|
3744
|
+
return driver.axSetValue({
|
|
3745
|
+
elementId: params.elementId,
|
|
3746
|
+
value: params.value,
|
|
3747
|
+
pid: params.pid
|
|
3748
|
+
});
|
|
3749
|
+
});
|
|
3750
|
+
methods.set("desktop.ax.focus", async (params) => {
|
|
3751
|
+
return driver.axFocus({
|
|
3752
|
+
elementId: params.elementId,
|
|
3753
|
+
pid: params.pid
|
|
3754
|
+
});
|
|
3755
|
+
});
|
|
3756
|
+
methods.set("desktop.ax.search", async (params) => {
|
|
3757
|
+
return driver.axSearch({
|
|
3758
|
+
query: params.query,
|
|
3759
|
+
role: params.role,
|
|
3760
|
+
pid: params.pid,
|
|
3761
|
+
maxResults: params.maxResults
|
|
3762
|
+
});
|
|
3763
|
+
});
|
|
3764
|
+
return {
|
|
3765
|
+
name: "desktop",
|
|
3766
|
+
methods
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
// src/agent/cli.ts
|
|
3771
|
+
import { hostname as hostname2, platform as platform3, arch as arch2 } from "os";
|
|
3772
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync2 } from "fs";
|
|
3773
|
+
import { join as join8 } from "path";
|
|
3774
|
+
import { homedir as homedir5 } from "os";
|
|
3775
|
+
import { execSync } from "child_process";
|
|
3776
|
+
var c2 = {
|
|
3777
|
+
reset: "\x1B[0m",
|
|
3778
|
+
bold: "\x1B[1m",
|
|
3779
|
+
dim: "\x1B[2m",
|
|
3780
|
+
italic: "\x1B[3m",
|
|
3781
|
+
cyan: "\x1B[36m",
|
|
3782
|
+
blue: "\x1B[34m",
|
|
3783
|
+
green: "\x1B[32m",
|
|
3784
|
+
yellow: "\x1B[33m",
|
|
3785
|
+
red: "\x1B[31m",
|
|
3786
|
+
magenta: "\x1B[35m",
|
|
3787
|
+
white: "\x1B[97m",
|
|
3788
|
+
gray: "\x1B[90m",
|
|
3789
|
+
bgCyan: "\x1B[46m",
|
|
3790
|
+
bgBlue: "\x1B[44m"
|
|
3791
|
+
};
|
|
3792
|
+
function parseArgs(argv) {
|
|
3793
|
+
const command2 = argv[2] || "help";
|
|
3794
|
+
const flags2 = {};
|
|
3795
|
+
for (let i = 3; i < argv.length; i++) {
|
|
3796
|
+
const arg = argv[i];
|
|
3797
|
+
if (arg.startsWith("--")) {
|
|
3798
|
+
const key = arg.slice(2);
|
|
3799
|
+
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
3800
|
+
flags2[key] = value;
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
return { command: command2, flags: flags2 };
|
|
3804
|
+
}
|
|
3805
|
+
function clearScreen() {
|
|
3806
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
3807
|
+
}
|
|
3808
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3809
|
+
async function printStartup(config, capabilities, version) {
|
|
3810
|
+
const machine = hostname2();
|
|
3811
|
+
const plat = `${platform3()} ${arch2()}`;
|
|
3812
|
+
const truncate = (s, max) => s.length > max ? s.slice(0, max) + "\u2026" : s;
|
|
3813
|
+
const tunnelDisplay = truncate(config.tunnelId, 40);
|
|
3814
|
+
const apiDisplay = truncate(config.apiUrl, 40);
|
|
3815
|
+
const machineDisplay = truncate(machine, 28);
|
|
3816
|
+
console.log("");
|
|
3817
|
+
console.log(` ${c2.cyan}\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2584 \u2588 \u2580\u2588\u2580${c2.reset} ${c2.cyan}\u2580\u2588\u2580 \u2588 \u2588 \u2588\u2584 \u2588 \u2588\u2584 \u2588 \u2588\u2580\u2580 \u2588 ${c2.reset}`);
|
|
3818
|
+
console.log(` ${c2.cyan}\u2588\u2580\u2588 \u2588\u2584\u2588 \u2588\u2588\u2584 \u2588 \u2580\u2588 \u2588${c2.reset} ${c2.cyan} \u2588 \u2588\u2584\u2588 \u2588 \u2580\u2588 \u2588 \u2580\u2588 \u2588\u2588\u2584 \u2588\u2584\u2584${c2.reset}`);
|
|
3819
|
+
console.log("");
|
|
3820
|
+
const barW = 50;
|
|
3821
|
+
const frames = 14;
|
|
3822
|
+
for (let i = 0; i <= frames; i++) {
|
|
3823
|
+
const filled = Math.round(i / frames * barW);
|
|
3824
|
+
const empty = barW - filled;
|
|
3825
|
+
process.stdout.write(
|
|
3826
|
+
`\r ${c2.cyan}\u25C7${c2.reset} ${c2.cyan}${"\u2550".repeat(filled)}${c2.reset}${c2.gray}${"\u2500".repeat(empty)}${c2.reset} `
|
|
3827
|
+
);
|
|
3828
|
+
await sleep(20);
|
|
3829
|
+
}
|
|
3830
|
+
process.stdout.write(`\r ${c2.cyan}\u25C7 ${"\u2550".repeat(barW)} \u25C6${c2.reset}
|
|
3831
|
+
`);
|
|
3832
|
+
await sleep(120);
|
|
3833
|
+
const W = 60;
|
|
3834
|
+
const vLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
3835
|
+
const row = (content) => {
|
|
3836
|
+
const pad = Math.max(0, W - vLen(content));
|
|
3837
|
+
console.log(` ${c2.gray}\u2502${c2.reset}${content}${" ".repeat(pad)}${c2.gray}\u2502${c2.reset}`);
|
|
3838
|
+
};
|
|
3839
|
+
const blank = () => console.log(` ${c2.gray}\u2502${c2.reset}${" ".repeat(W)}${c2.gray}\u2502${c2.reset}`);
|
|
3840
|
+
const titleL = ` ${c2.cyan}\u25C6${c2.reset} ${c2.bold}${c2.white}Agent Tunnel${c2.reset}`;
|
|
3841
|
+
const titleR = `${c2.dim}v${version}${c2.reset} `;
|
|
3842
|
+
const titleLLen = 18;
|
|
3843
|
+
const titleRLen = 1 + version.length + 3;
|
|
3844
|
+
const titlePad = Math.max(1, W - titleLLen - titleRLen);
|
|
3845
|
+
const capStr = capabilities.map((name) => `${c2.green}\u25CF${c2.reset} ${c2.white}${name}${c2.reset}`).join(" ");
|
|
3846
|
+
const brand = "created by kortix";
|
|
3847
|
+
const brandFill = W - brand.length - 3;
|
|
3848
|
+
console.log("");
|
|
3849
|
+
console.log(` ${c2.gray}\u256D${"\u2500".repeat(W)}\u256E${c2.reset}`);
|
|
3850
|
+
blank();
|
|
3851
|
+
row(`${titleL}${" ".repeat(titlePad)}${titleR}`);
|
|
3852
|
+
row(` ${c2.dim}Bridge between AI agents & local machines${c2.reset}`);
|
|
3853
|
+
blank();
|
|
3854
|
+
row(` ${c2.dim}tunnel${c2.reset} ${c2.white}${tunnelDisplay}${c2.reset}`);
|
|
3855
|
+
row(` ${c2.dim}relay${c2.reset} ${c2.white}${apiDisplay}${c2.reset}`);
|
|
3856
|
+
row(` ${c2.dim}machine${c2.reset} ${c2.white}${machineDisplay}${c2.reset} ${c2.dim}(${plat})${c2.reset}`);
|
|
3857
|
+
blank();
|
|
3858
|
+
console.log(` ${c2.gray}\u2570${"\u2500".repeat(brandFill)} ${c2.dim}created by ${c2.cyan}kortix${c2.reset} ${c2.gray}\u2500\u256F${c2.reset}`);
|
|
3859
|
+
console.log("");
|
|
3860
|
+
}
|
|
3861
|
+
function startAgent(config) {
|
|
3862
|
+
const registry = new CapabilityRegistry();
|
|
3863
|
+
registry.register(createFilesystemCapability(config));
|
|
3864
|
+
registry.register(createShellCapability(config));
|
|
3865
|
+
registry.register(createDesktopCapability());
|
|
3866
|
+
clearScreen();
|
|
3867
|
+
printStartup(config, registry.getCapabilityNames(), "0.1.2");
|
|
3868
|
+
const agent = new TunnelAgent(config, registry);
|
|
3869
|
+
agent.connect();
|
|
3870
|
+
const shutdown = () => {
|
|
3871
|
+
console.log(`
|
|
3872
|
+
${c2.dim} Shutting down\u2026${c2.reset}`);
|
|
3873
|
+
agent.disconnect();
|
|
3874
|
+
process.exit(0);
|
|
3875
|
+
};
|
|
3876
|
+
process.on("SIGTERM", shutdown);
|
|
3877
|
+
process.on("SIGINT", shutdown);
|
|
3878
|
+
}
|
|
3879
|
+
function openBrowser(url) {
|
|
3880
|
+
try {
|
|
3881
|
+
const plat = platform3();
|
|
3882
|
+
if (plat === "darwin") execSync(`open "${url}"`);
|
|
3883
|
+
else if (plat === "win32") execSync(`start "" "${url}"`);
|
|
3884
|
+
else execSync(`xdg-open "${url}"`);
|
|
3885
|
+
} catch {
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
var CONFIG_DIR2 = join8(homedir5(), ".agent-tunnel");
|
|
3889
|
+
var CONFIG_FILE2 = join8(CONFIG_DIR2, "config.json");
|
|
3890
|
+
function saveCredentials(tunnelId, token, apiUrl) {
|
|
3891
|
+
mkdirSync4(CONFIG_DIR2, { recursive: true });
|
|
3892
|
+
let existing = {};
|
|
3893
|
+
if (existsSync5(CONFIG_FILE2)) {
|
|
3894
|
+
try {
|
|
3895
|
+
existing = JSON.parse(readFileSync2(CONFIG_FILE2, "utf-8"));
|
|
3896
|
+
} catch {
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
writeFileSync4(CONFIG_FILE2, JSON.stringify({ ...existing, tunnelId, token, apiUrl }, null, 2));
|
|
3900
|
+
}
|
|
3901
|
+
async function commandConnectDeviceAuth(config) {
|
|
3902
|
+
console.log("");
|
|
3903
|
+
console.log(` ${c2.cyan}\u25C6${c2.reset} ${c2.bold}Device Authorization${c2.reset}`);
|
|
3904
|
+
console.log("");
|
|
3905
|
+
let deviceCode;
|
|
3906
|
+
let deviceSecret;
|
|
3907
|
+
let verificationUrl;
|
|
3908
|
+
let expiresAt;
|
|
3909
|
+
let pollIntervalMs;
|
|
3910
|
+
try {
|
|
3911
|
+
const res = await fetch(`${config.apiUrl}/device-auth`, {
|
|
3912
|
+
method: "POST",
|
|
3913
|
+
headers: { "Content-Type": "application/json" },
|
|
3914
|
+
body: JSON.stringify({ machineHostname: hostname2() })
|
|
3915
|
+
});
|
|
3916
|
+
if (!res.ok) {
|
|
3917
|
+
const text = await res.text().catch(() => "");
|
|
3918
|
+
console.error(` ${c2.red}\u2717${c2.reset} Failed to create device auth request: ${res.status} ${text.slice(0, 200)}`);
|
|
3919
|
+
process.exit(1);
|
|
3920
|
+
}
|
|
3921
|
+
const data = await res.json();
|
|
3922
|
+
deviceCode = data.deviceCode;
|
|
3923
|
+
deviceSecret = data.deviceSecret;
|
|
3924
|
+
verificationUrl = data.verificationUrl;
|
|
3925
|
+
expiresAt = data.expiresAt;
|
|
3926
|
+
pollIntervalMs = data.pollIntervalMs || 2e3;
|
|
3927
|
+
} catch (err) {
|
|
3928
|
+
console.error(` ${c2.red}\u2717${c2.reset} Failed to reach API at ${config.apiUrl}`);
|
|
3929
|
+
process.exit(1);
|
|
3930
|
+
return;
|
|
3931
|
+
}
|
|
3932
|
+
console.log(` ${c2.dim}Code:${c2.reset} ${c2.bold}${c2.white}${deviceCode}${c2.reset}`);
|
|
3933
|
+
console.log("");
|
|
3934
|
+
console.log(` ${c2.dim}Open this URL on any device to approve:${c2.reset}`);
|
|
3935
|
+
console.log(` ${c2.cyan}${verificationUrl}${c2.reset}`);
|
|
3936
|
+
console.log("");
|
|
3937
|
+
openBrowser(verificationUrl);
|
|
3938
|
+
const expiresAtMs = new Date(expiresAt).getTime();
|
|
3939
|
+
while (true) {
|
|
3940
|
+
const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1e3));
|
|
3941
|
+
if (remaining <= 0) {
|
|
3942
|
+
console.log(`
|
|
3943
|
+
${c2.red}\u2717${c2.reset} Authorization expired. Please try again.`);
|
|
3944
|
+
process.exit(1);
|
|
3945
|
+
}
|
|
3946
|
+
const min = Math.floor(remaining / 60);
|
|
3947
|
+
const sec = remaining % 60;
|
|
3948
|
+
process.stdout.write(`\r ${c2.dim}Waiting for approval... ${c2.white}${min}:${sec.toString().padStart(2, "0")}${c2.reset} `);
|
|
3949
|
+
try {
|
|
3950
|
+
const res = await fetch(`${config.apiUrl}/device-auth/${deviceCode}/status?secret=${deviceSecret}`);
|
|
3951
|
+
if (res.ok) {
|
|
3952
|
+
const data = await res.json();
|
|
3953
|
+
if (data.status === "approved" && data.tunnelId && data.token) {
|
|
3954
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
3955
|
+
console.log(` ${c2.green}\u25CF${c2.reset} ${c2.bold}Authorized!${c2.reset}`);
|
|
3956
|
+
console.log("");
|
|
3957
|
+
saveCredentials(data.tunnelId, data.token, config.apiUrl);
|
|
3958
|
+
console.log(` ${c2.dim}Credentials saved to ${CONFIG_FILE2}${c2.reset}`);
|
|
3959
|
+
console.log("");
|
|
3960
|
+
const fullConfig = loadConfig({
|
|
3961
|
+
token: data.token,
|
|
3962
|
+
tunnelId: data.tunnelId,
|
|
3963
|
+
apiUrl: config.apiUrl
|
|
3964
|
+
});
|
|
3965
|
+
startAgent(fullConfig);
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
if (data.status === "denied") {
|
|
3969
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
3970
|
+
console.log(` ${c2.red}\u2717${c2.reset} Authorization denied.`);
|
|
3971
|
+
process.exit(1);
|
|
3972
|
+
}
|
|
3973
|
+
if (data.status === "expired") {
|
|
3974
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
3975
|
+
console.log(` ${c2.red}\u2717${c2.reset} Authorization expired. Please try again.`);
|
|
3976
|
+
process.exit(1);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
} catch {
|
|
3980
|
+
}
|
|
3981
|
+
await sleep(pollIntervalMs);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
async function commandConnect(flags2) {
|
|
3985
|
+
const config = loadConfig({
|
|
3986
|
+
token: flags2.token,
|
|
3987
|
+
tunnelId: flags2["tunnel-id"],
|
|
3988
|
+
apiUrl: flags2["api-url"]
|
|
3989
|
+
});
|
|
3990
|
+
if (config.token && config.tunnelId) {
|
|
3991
|
+
startAgent(config);
|
|
3992
|
+
return;
|
|
3993
|
+
}
|
|
3994
|
+
if (!config.token && !config.tunnelId) {
|
|
3995
|
+
await commandConnectDeviceAuth(config);
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
console.error(`${c2.red}${c2.bold} error${c2.reset} Provide both --token and --tunnel-id, or neither (for device auth)`);
|
|
3999
|
+
process.exit(1);
|
|
4000
|
+
}
|
|
4001
|
+
async function commandStatus(flags2) {
|
|
4002
|
+
const config = loadConfig({
|
|
4003
|
+
token: flags2.token,
|
|
4004
|
+
tunnelId: flags2["tunnel-id"],
|
|
4005
|
+
apiUrl: flags2["api-url"]
|
|
4006
|
+
});
|
|
4007
|
+
if (!config.token || !config.tunnelId) {
|
|
4008
|
+
console.error("Error: --token and --tunnel-id are required");
|
|
4009
|
+
process.exit(1);
|
|
4010
|
+
}
|
|
4011
|
+
try {
|
|
4012
|
+
const res = await fetch(`${config.apiUrl}/connections/${config.tunnelId}`, {
|
|
4013
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
4014
|
+
});
|
|
4015
|
+
if (!res.ok) {
|
|
4016
|
+
console.error(`Error: ${res.status} ${await res.text()}`);
|
|
4017
|
+
process.exit(1);
|
|
4018
|
+
}
|
|
4019
|
+
const data = await res.json();
|
|
4020
|
+
console.log(JSON.stringify(data, null, 2));
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
console.error("Error:", err);
|
|
4023
|
+
process.exit(1);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
function showHelp() {
|
|
4027
|
+
console.log("");
|
|
4028
|
+
console.log(` ${c2.cyan}\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2584 \u2588 \u2580\u2588\u2580${c2.reset} ${c2.cyan}\u2580\u2588\u2580 \u2588 \u2588 \u2588\u2584 \u2588 \u2588\u2584 \u2588 \u2588\u2580\u2580 \u2588 ${c2.reset}`);
|
|
4029
|
+
console.log(` ${c2.cyan}\u2588\u2580\u2588 \u2588\u2584\u2588 \u2588\u2588\u2584 \u2588 \u2580\u2588 \u2588${c2.reset} ${c2.cyan} \u2588 \u2588\u2584\u2588 \u2588 \u2580\u2588 \u2588 \u2580\u2588 \u2588\u2588\u2584 \u2588\u2584\u2584${c2.reset}`);
|
|
4030
|
+
console.log("");
|
|
4031
|
+
console.log(` ${c2.dim}Secure bridge between AI agents & local machines${c2.reset}`);
|
|
4032
|
+
console.log("");
|
|
4033
|
+
console.log(` ${c2.bold}Usage${c2.reset} ${c2.dim}npx @kortix/agent-tunnel <command> [options]${c2.reset}`);
|
|
4034
|
+
console.log("");
|
|
4035
|
+
console.log(`${c2.gray} \u2500\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c2.reset}`);
|
|
4036
|
+
console.log(` ${c2.cyan}connect${c2.reset} Connect via device auth (opens browser)`);
|
|
4037
|
+
console.log(` ${c2.cyan}status${c2.reset} Check tunnel connection status`);
|
|
4038
|
+
console.log(` ${c2.cyan}help${c2.reset} Show this help message`);
|
|
4039
|
+
console.log("");
|
|
4040
|
+
console.log(`${c2.gray} \u2500\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c2.reset}`);
|
|
4041
|
+
console.log(` ${c2.white}--token${c2.reset} ${c2.dim}<token>${c2.reset} Skip device auth, connect directly`);
|
|
4042
|
+
console.log(` ${c2.white}--tunnel-id${c2.reset} ${c2.dim}<id>${c2.reset} Tunnel ID ${c2.dim}(required with --token)${c2.reset}`);
|
|
4043
|
+
console.log(` ${c2.white}--api-url${c2.reset} ${c2.dim}<url>${c2.reset} API URL ${c2.dim}(default: http://localhost:8080)${c2.reset}`);
|
|
4044
|
+
console.log("");
|
|
4045
|
+
console.log(` ${c2.dim}Config: ~/.agent-tunnel/config.json${c2.reset}`);
|
|
4046
|
+
console.log(` ${c2.dim}powered by ${c2.cyan}kortix${c2.reset}`);
|
|
4047
|
+
console.log("");
|
|
4048
|
+
}
|
|
4049
|
+
var { command, flags } = parseArgs(process.argv);
|
|
4050
|
+
switch (command) {
|
|
4051
|
+
case "connect":
|
|
4052
|
+
commandConnect(flags);
|
|
4053
|
+
break;
|
|
4054
|
+
case "status":
|
|
4055
|
+
commandStatus(flags);
|
|
4056
|
+
break;
|
|
4057
|
+
case "help":
|
|
4058
|
+
default:
|
|
4059
|
+
showHelp();
|
|
4060
|
+
break;
|
|
4061
|
+
}
|