@kortix/agent-tunnel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,914 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const HELPER_VERSION = 'v1';
7
+ const BIN_DIR = join(homedir(), '.kortix-tunnel', 'bin');
8
+ const HELPER_PATH = join(BIN_DIR, `desktop-helper-win-${HELPER_VERSION}.exe`);
9
+
10
+ const CSHARP_SOURCE = `
11
+ using System;
12
+ using System.Collections.Generic;
13
+ using System.Diagnostics;
14
+ using System.Drawing;
15
+ using System.Drawing.Imaging;
16
+ using System.IO;
17
+ using System.Linq;
18
+ using System.Runtime.InteropServices;
19
+ using System.Text;
20
+ using System.Threading;
21
+ using System.Windows.Automation;
22
+ using System.Windows.Forms;
23
+
24
+ class Helper
25
+ {
26
+ // ─── P/Invoke ────────────────────────────────────────────────
27
+ [DllImport("user32.dll")] static extern bool SetCursorPos(int X, int Y);
28
+ [DllImport("user32.dll")] static extern bool GetCursorPos(out POINT lpPoint);
29
+ [DllImport("user32.dll")] static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
30
+ [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
31
+ [DllImport("user32.dll")] static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
32
+ [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
33
+ [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
34
+ [DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd);
35
+ [DllImport("user32.dll")] static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
36
+ [DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
37
+ [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
38
+ [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
39
+ [DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr hWnd);
40
+
41
+ delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
42
+
43
+ const int SW_MINIMIZE = 6;
44
+ const uint WM_CLOSE = 0x0010;
45
+ const int INPUT_MOUSE = 0;
46
+ const int INPUT_KEYBOARD = 1;
47
+ const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
48
+ const uint MOUSEEVENTF_LEFTUP = 0x0004;
49
+ const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
50
+ const uint MOUSEEVENTF_RIGHTUP = 0x0010;
51
+ const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
52
+ const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
53
+ const uint MOUSEEVENTF_WHEEL = 0x0800;
54
+ const uint MOUSEEVENTF_HWHEEL = 0x1000;
55
+ const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
56
+ const uint MOUSEEVENTF_MOVE = 0x0001;
57
+ const uint KEYEVENTF_KEYUP = 0x0002;
58
+ const uint KEYEVENTF_UNICODE = 0x0004;
59
+
60
+ [StructLayout(LayoutKind.Sequential)] struct POINT { public int X; public int Y; }
61
+ [StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; }
62
+
63
+ [StructLayout(LayoutKind.Sequential)]
64
+ struct INPUT { public int type; public INPUTUNION u; }
65
+
66
+ [StructLayout(LayoutKind.Explicit)]
67
+ struct INPUTUNION
68
+ {
69
+ [FieldOffset(0)] public MOUSEINPUT mi;
70
+ [FieldOffset(0)] public KEYBDINPUT ki;
71
+ }
72
+
73
+ [StructLayout(LayoutKind.Sequential)]
74
+ struct MOUSEINPUT { public int dx, dy; public uint mouseData; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
75
+
76
+ [StructLayout(LayoutKind.Sequential)]
77
+ struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
78
+
79
+ // ─── JSON helpers (minimal, no dependencies) ─────────────────
80
+ static string JsonStr(string s) => "\\"" + s.Replace("\\\\", "\\\\\\\\").Replace("\\"", "\\\\\\"").Replace("\\n", "\\\\n").Replace("\\r", "\\\\r").Replace("\\t", "\\\\t") + "\\"";
81
+
82
+ static Dictionary<string, object> ParseJson(string json)
83
+ {
84
+ var d = new Dictionary<string, object>();
85
+ json = json.Trim();
86
+ if (json.StartsWith("{")) json = json.Substring(1, json.Length - 2).Trim();
87
+
88
+ int i = 0;
89
+ while (i < json.Length)
90
+ {
91
+ while (i < json.Length && (json[i] == ',' || json[i] == ' ' || json[i] == '\\n' || json[i] == '\\r' || json[i] == '\\t')) i++;
92
+ if (i >= json.Length) break;
93
+
94
+ var key = ParseJsonString(json, ref i);
95
+ while (i < json.Length && (json[i] == ' ' || json[i] == ':')) i++;
96
+ var val = ParseJsonValue(json, ref i);
97
+ d[key] = val;
98
+ }
99
+ return d;
100
+ }
101
+
102
+ static string ParseJsonString(string json, ref int i)
103
+ {
104
+ if (json[i] != '\\"') throw new Exception("Expected string at " + i);
105
+ i++;
106
+ var sb = new StringBuilder();
107
+ while (i < json.Length && json[i] != '\\"')
108
+ {
109
+ if (json[i] == '\\\\') { i++; sb.Append(json[i]); }
110
+ else sb.Append(json[i]);
111
+ i++;
112
+ }
113
+ i++; // skip closing quote
114
+ return sb.ToString();
115
+ }
116
+
117
+ static object ParseJsonValue(string json, ref int i)
118
+ {
119
+ while (i < json.Length && json[i] == ' ') i++;
120
+ if (i >= json.Length) return null;
121
+
122
+ if (json[i] == '\\"') return ParseJsonString(json, ref i);
123
+ if (json[i] == '[')
124
+ {
125
+ i++;
126
+ var list = new List<object>();
127
+ while (i < json.Length && json[i] != ']')
128
+ {
129
+ while (i < json.Length && (json[i] == ',' || json[i] == ' ' || json[i] == '\\n' || json[i] == '\\r' || json[i] == '\\t')) i++;
130
+ if (i < json.Length && json[i] != ']')
131
+ list.Add(ParseJsonValue(json, ref i));
132
+ }
133
+ if (i < json.Length) i++;
134
+ return list;
135
+ }
136
+ if (json[i] == '{')
137
+ {
138
+ var start = i;
139
+ int depth = 1; i++;
140
+ while (i < json.Length && depth > 0) { if (json[i] == '{') depth++; if (json[i] == '}') depth--; i++; }
141
+ return json.Substring(start, i - start);
142
+ }
143
+ if (json[i] == 'n' && json.Substring(i, 4) == "null") { i += 4; return null; }
144
+ if (json[i] == 't' && json.Substring(i, 4) == "true") { i += 4; return true; }
145
+ if (json[i] == 'f' && json.Substring(i, 5) == "false") { i += 5; return false; }
146
+
147
+ // number
148
+ var numStart = i;
149
+ while (i < json.Length && (char.IsDigit(json[i]) || json[i] == '.' || json[i] == '-' || json[i] == 'e' || json[i] == 'E' || json[i] == '+')) i++;
150
+ var numStr = json.Substring(numStart, i - numStart);
151
+ if (numStr.Contains(".")) return double.Parse(numStr, System.Globalization.CultureInfo.InvariantCulture);
152
+ return int.Parse(numStr);
153
+ }
154
+
155
+ static int GetInt(Dictionary<string, object> d, string k, int def = 0) { return d.ContainsKey(k) && d[k] != null ? Convert.ToInt32(d[k]) : def; }
156
+ static double GetDbl(Dictionary<string, object> d, string k, double def = 0) { return d.ContainsKey(k) && d[k] != null ? Convert.ToDouble(d[k]) : def; }
157
+ static string GetStr(Dictionary<string, object> d, string k, string def = "") { return d.ContainsKey(k) && d[k] is string ? (string)d[k] : def; }
158
+ 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>(); }
159
+
160
+ // ─── Virtual key codes ───────────────────────────────────────
161
+ static Dictionary<string, ushort> VKMap = new Dictionary<string, ushort>(StringComparer.OrdinalIgnoreCase)
162
+ {
163
+ {"return", 0x0D}, {"enter", 0x0D}, {"tab", 0x09}, {"space", 0x20},
164
+ {"backspace", 0x08}, {"delete", 0x2E}, {"escape", 0x1B}, {"esc", 0x1B},
165
+ {"up", 0x26}, {"down", 0x28}, {"left", 0x25}, {"right", 0x27},
166
+ {"home", 0x24}, {"end", 0x23}, {"pageup", 0x21}, {"pagedown", 0x22},
167
+ {"f1", 0x70}, {"f2", 0x71}, {"f3", 0x72}, {"f4", 0x73},
168
+ {"f5", 0x74}, {"f6", 0x75}, {"f7", 0x76}, {"f8", 0x77},
169
+ {"f9", 0x78}, {"f10", 0x79}, {"f11", 0x7A}, {"f12", 0x7B},
170
+ {"shift", 0x10}, {"ctrl", 0x11}, {"control", 0x11},
171
+ {"alt", 0x12}, {"option", 0x12}, {"cmd", 0x5B}, {"command", 0x5B},
172
+ {"a", 0x41}, {"b", 0x42}, {"c", 0x43}, {"d", 0x44}, {"e", 0x45}, {"f", 0x46},
173
+ {"g", 0x47}, {"h", 0x48}, {"i", 0x49}, {"j", 0x4A}, {"k", 0x4B}, {"l", 0x4C},
174
+ {"m", 0x4D}, {"n", 0x4E}, {"o", 0x4F}, {"p", 0x50}, {"q", 0x51}, {"r", 0x52},
175
+ {"s", 0x53}, {"t", 0x54}, {"u", 0x55}, {"v", 0x56}, {"w", 0x57}, {"x", 0x58},
176
+ {"y", 0x59}, {"z", 0x5A},
177
+ {"0", 0x30}, {"1", 0x31}, {"2", 0x32}, {"3", 0x33}, {"4", 0x34},
178
+ {"5", 0x35}, {"6", 0x36}, {"7", 0x37}, {"8", 0x38}, {"9", 0x39},
179
+ };
180
+
181
+ static ushort GetVK(string key)
182
+ {
183
+ ushort vk;
184
+ if (VKMap.TryGetValue(key, out vk)) return vk;
185
+ if (key.Length == 1) return (ushort)char.ToUpper(key[0]);
186
+ return 0;
187
+ }
188
+
189
+ static bool IsModifier(string key)
190
+ {
191
+ var k = key.ToLower();
192
+ return k == "shift" || k == "ctrl" || k == "control" || k == "alt" || k == "option" || k == "cmd" || k == "command";
193
+ }
194
+
195
+ // ─── Mouse helpers ───────────────────────────────────────────
196
+ static void SendMouseClick(int x, int y, string button, int clicks)
197
+ {
198
+ SetCursorPos(x, y);
199
+ Thread.Sleep(10);
200
+
201
+ uint downFlag, upFlag;
202
+ switch (button)
203
+ {
204
+ case "right": downFlag = MOUSEEVENTF_RIGHTDOWN; upFlag = MOUSEEVENTF_RIGHTUP; break;
205
+ case "middle": downFlag = MOUSEEVENTF_MIDDLEDOWN; upFlag = MOUSEEVENTF_MIDDLEUP; break;
206
+ default: downFlag = MOUSEEVENTF_LEFTDOWN; upFlag = MOUSEEVENTF_LEFTUP; break;
207
+ }
208
+
209
+ for (int c = 0; c < clicks; c++)
210
+ {
211
+ var inputs = new INPUT[]
212
+ {
213
+ new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = downFlag } } },
214
+ new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = upFlag } } },
215
+ };
216
+ SendInput(2, inputs, Marshal.SizeOf(typeof(INPUT)));
217
+ if (c < clicks - 1) Thread.Sleep(50);
218
+ }
219
+ }
220
+
221
+ // ─── AX helpers ──────────────────────────────────────────────
222
+ static int axElementCount;
223
+
224
+ static string WalkAXTree(AutomationElement el, int depth, int maxDepth, List<string> roles, string pathPrefix)
225
+ {
226
+ if (el == null || depth > maxDepth) return "null";
227
+ axElementCount++;
228
+
229
+ string role = "";
230
+ string name = "";
231
+ string val = "";
232
+ string desc = "";
233
+ var bounds = System.Windows.Rect.Empty;
234
+ bool enabled = true;
235
+ bool focused = false;
236
+ var actionList = new List<string>();
237
+
238
+ try { role = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); } catch {}
239
+ try { name = el.Current.Name ?? ""; } catch {}
240
+ try { val = el.Current.AutomationId ?? ""; } catch {}
241
+ try { desc = el.Current.HelpText ?? ""; } catch {}
242
+ try { bounds = el.Current.BoundingRectangle; } catch {}
243
+ try { enabled = el.Current.IsEnabled; } catch {}
244
+ try { focused = el.Current.HasKeyboardFocus; } catch {}
245
+
246
+ // Check supported patterns for actions
247
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) actionList.Add("invoke"); } catch {}
248
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) actionList.Add("toggle"); } catch {}
249
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) actionList.Add("expandcollapse"); } catch {}
250
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) actionList.Add("setvalue"); } catch {}
251
+
252
+ if (roles != null && roles.Count > 0 && !roles.Contains(role.ToLower()))
253
+ {
254
+ // Skip this element but still walk children
255
+ var sb2 = new StringBuilder();
256
+ bool first2 = true;
257
+ int childIdx = 0;
258
+ try
259
+ {
260
+ var walker = TreeWalker.ControlViewWalker;
261
+ var child = walker.GetFirstChild(el);
262
+ while (child != null)
263
+ {
264
+ var childPath = pathPrefix.Length > 0 ? pathPrefix + "." + childIdx : childIdx.ToString();
265
+ var childJson = WalkAXTree(child, depth, maxDepth, roles, childPath);
266
+ if (childJson != "null")
267
+ {
268
+ if (!first2) sb2.Append(",");
269
+ sb2.Append(childJson);
270
+ first2 = false;
271
+ }
272
+ child = walker.GetNextSibling(child);
273
+ childIdx++;
274
+ }
275
+ } catch {}
276
+ if (sb2.Length == 0) return "null";
277
+ return sb2.ToString();
278
+ }
279
+
280
+ var sb = new StringBuilder();
281
+ sb.Append("{");
282
+ sb.AppendFormat("\\"id\\":{0}", JsonStr(pathPrefix));
283
+ sb.AppendFormat(",\\"role\\":{0}", JsonStr(role));
284
+ sb.AppendFormat(",\\"title\\":{0}", JsonStr(name));
285
+ sb.AppendFormat(",\\"value\\":{0}", JsonStr(val));
286
+ sb.AppendFormat(",\\"description\\":{0}", JsonStr(desc));
287
+ sb.AppendFormat(",\\"bounds\\":{{\\"x\\":{0},\\"y\\":{1},\\"width\\":{2},\\"height\\":{3}}}",
288
+ bounds.IsEmpty ? 0 : (int)bounds.X,
289
+ bounds.IsEmpty ? 0 : (int)bounds.Y,
290
+ bounds.IsEmpty ? 0 : (int)bounds.Width,
291
+ bounds.IsEmpty ? 0 : (int)bounds.Height);
292
+ sb.AppendFormat(",\\"enabled\\":{0}", enabled ? "true" : "false");
293
+ sb.AppendFormat(",\\"focused\\":{0}", focused ? "true" : "false");
294
+ sb.Append(",\\"actions\\":[");
295
+ for (int a = 0; a < actionList.Count; a++) { if (a > 0) sb.Append(","); sb.Append(JsonStr(actionList[a])); }
296
+ sb.Append("]");
297
+
298
+ // Children
299
+ sb.Append(",\\"children\\":[");
300
+ if (depth < maxDepth)
301
+ {
302
+ bool first = true;
303
+ int childIdx = 0;
304
+ try
305
+ {
306
+ var walker = TreeWalker.ControlViewWalker;
307
+ var child = walker.GetFirstChild(el);
308
+ while (child != null)
309
+ {
310
+ var childPath = pathPrefix.Length > 0 ? pathPrefix + "." + childIdx : childIdx.ToString();
311
+ var childJson = WalkAXTree(child, depth + 1, maxDepth, roles, childPath);
312
+ if (childJson != "null")
313
+ {
314
+ if (!first) sb.Append(",");
315
+ sb.Append(childJson);
316
+ first = false;
317
+ }
318
+ child = walker.GetNextSibling(child);
319
+ childIdx++;
320
+ }
321
+ } catch {}
322
+ }
323
+ sb.Append("]");
324
+ sb.Append("}");
325
+ return sb.ToString();
326
+ }
327
+
328
+ static AutomationElement NavigateToElement(AutomationElement root, string elementId)
329
+ {
330
+ var parts = elementId.Split('.');
331
+ var current = root;
332
+
333
+ foreach (var part in parts)
334
+ {
335
+ int idx = int.Parse(part);
336
+ var walker = TreeWalker.ControlViewWalker;
337
+ var child = walker.GetFirstChild(current);
338
+ for (int i = 0; i < idx && child != null; i++)
339
+ child = walker.GetNextSibling(child);
340
+ if (child == null) throw new Exception("Element not found at path: " + elementId);
341
+ current = child;
342
+ }
343
+ return current;
344
+ }
345
+
346
+ static void SearchAXTree(AutomationElement el, string query, string roleFilter, int maxResults, List<string> results, string pathPrefix, int depth, int maxDepth)
347
+ {
348
+ if (el == null || results.Count >= maxResults || depth > maxDepth) return;
349
+
350
+ string role = "";
351
+ string name = "";
352
+ string val = "";
353
+ string desc = "";
354
+ var bounds = System.Windows.Rect.Empty;
355
+ bool enabled = true;
356
+ bool focused = false;
357
+ var actionList = new List<string>();
358
+
359
+ try { role = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); } catch {}
360
+ try { name = el.Current.Name ?? ""; } catch {}
361
+ try { val = el.Current.AutomationId ?? ""; } catch {}
362
+ try { desc = el.Current.HelpText ?? ""; } catch {}
363
+ try { bounds = el.Current.BoundingRectangle; } catch {}
364
+ try { enabled = el.Current.IsEnabled; } catch {}
365
+ try { focused = el.Current.HasKeyboardFocus; } catch {}
366
+
367
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) actionList.Add("invoke"); } catch {}
368
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) actionList.Add("toggle"); } catch {}
369
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) actionList.Add("expandcollapse"); } catch {}
370
+ try { if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) actionList.Add("setvalue"); } catch {}
371
+
372
+ var queryLower = query.ToLower();
373
+ bool match = name.ToLower().Contains(queryLower) || val.ToLower().Contains(queryLower) || desc.ToLower().Contains(queryLower);
374
+
375
+ if (roleFilter != null && roleFilter.Length > 0 && role.ToLower() != roleFilter.ToLower())
376
+ match = false;
377
+
378
+ if (match)
379
+ {
380
+ var sb = new StringBuilder();
381
+ sb.Append("{");
382
+ sb.AppendFormat("\\"id\\":{0}", JsonStr(pathPrefix));
383
+ sb.AppendFormat(",\\"role\\":{0}", JsonStr(role));
384
+ sb.AppendFormat(",\\"title\\":{0}", JsonStr(name));
385
+ sb.AppendFormat(",\\"value\\":{0}", JsonStr(val));
386
+ sb.AppendFormat(",\\"description\\":{0}", JsonStr(desc));
387
+ sb.AppendFormat(",\\"bounds\\":{{\\"x\\":{0},\\"y\\":{1},\\"width\\":{2},\\"height\\":{3}}}",
388
+ bounds.IsEmpty ? 0 : (int)bounds.X, bounds.IsEmpty ? 0 : (int)bounds.Y,
389
+ bounds.IsEmpty ? 0 : (int)bounds.Width, bounds.IsEmpty ? 0 : (int)bounds.Height);
390
+ sb.AppendFormat(",\\"enabled\\":{0}", enabled ? "true" : "false");
391
+ sb.AppendFormat(",\\"focused\\":{0}", focused ? "true" : "false");
392
+ sb.Append(",\\"actions\\":[");
393
+ for (int a = 0; a < actionList.Count; a++) { if (a > 0) sb.Append(","); sb.Append(JsonStr(actionList[a])); }
394
+ sb.Append("],\\"children\\":[]}");
395
+ results.Add(sb.ToString());
396
+ }
397
+
398
+ int childIdx = 0;
399
+ try
400
+ {
401
+ var walker = TreeWalker.ControlViewWalker;
402
+ var child = walker.GetFirstChild(el);
403
+ while (child != null && results.Count < maxResults)
404
+ {
405
+ var childPath = pathPrefix.Length > 0 ? pathPrefix + "." + childIdx : childIdx.ToString();
406
+ SearchAXTree(child, query, roleFilter, maxResults, results, childPath, depth + 1, maxDepth);
407
+ child = walker.GetNextSibling(child);
408
+ childIdx++;
409
+ }
410
+ } catch {}
411
+ }
412
+
413
+ static AutomationElement FindAppRoot(int pid)
414
+ {
415
+ if (pid <= 0) return AutomationElement.RootElement;
416
+
417
+ var cond = new PropertyCondition(AutomationElement.ProcessIdProperty, pid);
418
+ var el = AutomationElement.RootElement.FindFirst(TreeScope.Children, cond);
419
+ if (el == null) throw new Exception("No UI Automation element found for PID " + pid);
420
+ return el;
421
+ }
422
+
423
+ // ─── Main ────────────────────────────────────────────────────
424
+ [STAThread]
425
+ static void Main()
426
+ {
427
+ var input = Console.In.ReadToEnd().Trim();
428
+ Dictionary<string, object> req;
429
+ try { req = ParseJson(input); }
430
+ catch (Exception ex) { Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr("Invalid JSON: " + ex.Message) + "}"); return; }
431
+
432
+ var action = GetStr(req, "action");
433
+
434
+ try
435
+ {
436
+ switch (action)
437
+ {
438
+ case "click":
439
+ {
440
+ var x = (int)GetDbl(req, "x");
441
+ var y = (int)GetDbl(req, "y");
442
+ var button = GetStr(req, "button", "left");
443
+ var clicks = GetInt(req, "clicks", 1);
444
+ SendMouseClick(x, y, button, clicks);
445
+ Console.WriteLine("{\\"ok\\":true}");
446
+ break;
447
+ }
448
+ case "move":
449
+ {
450
+ SetCursorPos((int)GetDbl(req, "x"), (int)GetDbl(req, "y"));
451
+ Console.WriteLine("{\\"ok\\":true}");
452
+ break;
453
+ }
454
+ case "drag":
455
+ {
456
+ int fx = (int)GetDbl(req, "x"), fy = (int)GetDbl(req, "y");
457
+ int tx = (int)GetDbl(req, "toX"), ty = (int)GetDbl(req, "toY");
458
+ SetCursorPos(fx, fy);
459
+ Thread.Sleep(50);
460
+ var down = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTDOWN } } } };
461
+ SendInput(1, down, Marshal.SizeOf(typeof(INPUT)));
462
+
463
+ for (int i = 1; i <= 10; i++)
464
+ {
465
+ double t = i / 10.0;
466
+ int mx = fx + (int)((tx - fx) * t);
467
+ int my = fy + (int)((ty - fy) * t);
468
+ SetCursorPos(mx, my);
469
+ Thread.Sleep(10);
470
+ }
471
+
472
+ var up = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTUP } } } };
473
+ SendInput(1, up, Marshal.SizeOf(typeof(INPUT)));
474
+ Console.WriteLine("{\\"ok\\":true}");
475
+ break;
476
+ }
477
+ case "scroll":
478
+ {
479
+ SetCursorPos((int)GetDbl(req, "x"), (int)GetDbl(req, "y"));
480
+ Thread.Sleep(10);
481
+ int dy = GetInt(req, "deltaY");
482
+ int dx = GetInt(req, "deltaX");
483
+ if (dy != 0)
484
+ {
485
+ var inputs = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { mouseData = (uint)(dy * 120), dwFlags = MOUSEEVENTF_WHEEL } } } };
486
+ SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
487
+ }
488
+ if (dx != 0)
489
+ {
490
+ var inputs = new INPUT[] { new INPUT { type = INPUT_MOUSE, u = new INPUTUNION { mi = new MOUSEINPUT { mouseData = (uint)(dx * 120), dwFlags = MOUSEEVENTF_HWHEEL } } } };
491
+ SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
492
+ }
493
+ Console.WriteLine("{\\"ok\\":true}");
494
+ break;
495
+ }
496
+ case "position":
497
+ {
498
+ POINT p;
499
+ GetCursorPos(out p);
500
+ Console.WriteLine("{\\"ok\\":true,\\"x\\":" + p.X + ",\\"y\\":" + p.Y + "}");
501
+ break;
502
+ }
503
+ case "key":
504
+ {
505
+ var keys = GetList(req, "keys");
506
+ var mods = new List<ushort>();
507
+ var mainKeys = new List<ushort>();
508
+
509
+ foreach (var k in keys)
510
+ {
511
+ var keyStr = k.ToString();
512
+ var vk = GetVK(keyStr);
513
+ if (vk == 0) { Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr("Unknown key: " + keyStr) + "}"); return; }
514
+ if (IsModifier(keyStr)) mods.Add(vk); else mainKeys.Add(vk);
515
+ }
516
+
517
+ var inputList = new List<INPUT>();
518
+ foreach (var m in mods) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = m } } });
519
+ foreach (var k in mainKeys) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = k } } });
520
+ foreach (var k in mainKeys) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = k, dwFlags = KEYEVENTF_KEYUP } } });
521
+ foreach (var m in mods) inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wVk = m, dwFlags = KEYEVENTF_KEYUP } } });
522
+
523
+ SendInput((uint)inputList.Count, inputList.ToArray(), Marshal.SizeOf(typeof(INPUT)));
524
+ Console.WriteLine("{\\"ok\\":true}");
525
+ break;
526
+ }
527
+ case "type":
528
+ {
529
+ var text = GetStr(req, "text");
530
+ var inputList = new List<INPUT>();
531
+ foreach (char c in text)
532
+ {
533
+ inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wScan = (ushort)c, dwFlags = KEYEVENTF_UNICODE } } });
534
+ inputList.Add(new INPUT { type = INPUT_KEYBOARD, u = new INPUTUNION { ki = new KEYBDINPUT { wScan = (ushort)c, dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP } } });
535
+ }
536
+ SendInput((uint)inputList.Count, inputList.ToArray(), Marshal.SizeOf(typeof(INPUT)));
537
+ Console.WriteLine("{\\"ok\\":true}");
538
+ break;
539
+ }
540
+ case "screenshot":
541
+ {
542
+ var bounds = Screen.PrimaryScreen.Bounds;
543
+ using (var bmp = new Bitmap(bounds.Width, bounds.Height))
544
+ using (var g = Graphics.FromImage(bmp))
545
+ {
546
+ g.CopyFromScreen(bounds.Location, System.Drawing.Point.Empty, bounds.Size);
547
+
548
+ // Downscale if > 1920
549
+ Bitmap output = bmp;
550
+ bool scaled = false;
551
+ if (bmp.Width > 1920 || bmp.Height > 1920)
552
+ {
553
+ double scale = Math.Min(1920.0 / bmp.Width, 1920.0 / bmp.Height);
554
+ int nw = (int)(bmp.Width * scale);
555
+ int nh = (int)(bmp.Height * scale);
556
+ output = new Bitmap(nw, nh);
557
+ using (var g2 = Graphics.FromImage(output))
558
+ {
559
+ g2.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
560
+ g2.DrawImage(bmp, 0, 0, nw, nh);
561
+ }
562
+ scaled = true;
563
+ }
564
+
565
+ using (var ms = new MemoryStream())
566
+ {
567
+ var jpegEncoder = ImageCodecInfo.GetImageEncoders().First(e => e.FormatID == ImageFormat.Jpeg.Guid);
568
+ var encoderParams = new EncoderParameters(1);
569
+ encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 60L);
570
+ output.Save(ms, jpegEncoder, encoderParams);
571
+
572
+ var b64 = Convert.ToBase64String(ms.ToArray());
573
+ Console.WriteLine("{\\"ok\\":true,\\"image\\":" + JsonStr(b64) + ",\\"width\\":" + output.Width + ",\\"height\\":" + output.Height + ",\\"format\\":\\"jpeg\\"}");
574
+ }
575
+ if (scaled) output.Dispose();
576
+ }
577
+ break;
578
+ }
579
+ case "window_list":
580
+ {
581
+ var windows = new List<string>();
582
+ EnumWindows((hWnd, _) =>
583
+ {
584
+ if (!IsWindowVisible(hWnd)) return true;
585
+ int len = GetWindowTextLength(hWnd);
586
+ if (len == 0) return true;
587
+ var sb = new StringBuilder(len + 1);
588
+ GetWindowText(hWnd, sb, sb.Capacity);
589
+ var title = sb.ToString();
590
+
591
+ RECT r;
592
+ GetWindowRect(hWnd, out r);
593
+
594
+ uint pid;
595
+ GetWindowThreadProcessId(hWnd, out pid);
596
+ string appName = "";
597
+ try { appName = Process.GetProcessById((int)pid).ProcessName; } catch {}
598
+
599
+ windows.Add(String.Format("{{\\"id\\":{0},\\"app\\":{1},\\"title\\":{2},\\"bounds\\":{{\\"x\\":{3},\\"y\\":{4},\\"width\\":{5},\\"height\\":{6}}},\\"minimized\\":false}}",
600
+ hWnd.ToInt64(), JsonStr(appName), JsonStr(title),
601
+ r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top));
602
+ return true;
603
+ }, IntPtr.Zero);
604
+
605
+ Console.WriteLine("{\\"ok\\":true,\\"windows\\":[" + string.Join(",", windows) + "]}");
606
+ break;
607
+ }
608
+ case "window_focus":
609
+ {
610
+ var wid = (IntPtr)(long)GetDbl(req, "windowId");
611
+ SetForegroundWindow(wid);
612
+ Console.WriteLine("{\\"ok\\":true}");
613
+ break;
614
+ }
615
+ case "window_resize":
616
+ {
617
+ var wid = (IntPtr)(long)GetDbl(req, "windowId");
618
+ int x = (int)GetDbl(req, "x"), y = (int)GetDbl(req, "y");
619
+ int w = (int)GetDbl(req, "width"), h = (int)GetDbl(req, "height");
620
+ MoveWindow(wid, x, y, w, h, true);
621
+ Console.WriteLine("{\\"ok\\":true}");
622
+ break;
623
+ }
624
+ case "window_close":
625
+ {
626
+ var wid = (IntPtr)(long)GetDbl(req, "windowId");
627
+ SendMessage(wid, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
628
+ Console.WriteLine("{\\"ok\\":true}");
629
+ break;
630
+ }
631
+ case "window_minimize":
632
+ {
633
+ var wid = (IntPtr)(long)GetDbl(req, "windowId");
634
+ ShowWindow(wid, SW_MINIMIZE);
635
+ Console.WriteLine("{\\"ok\\":true}");
636
+ break;
637
+ }
638
+ case "app_launch":
639
+ {
640
+ var app = GetStr(req, "name");
641
+ Process.Start(app);
642
+ Console.WriteLine("{\\"ok\\":true}");
643
+ break;
644
+ }
645
+ case "app_quit":
646
+ {
647
+ var app = GetStr(req, "name").ToLower();
648
+ foreach (var p in Process.GetProcesses())
649
+ {
650
+ try { if (p.ProcessName.ToLower() == app) p.Kill(); } catch {}
651
+ }
652
+ Console.WriteLine("{\\"ok\\":true}");
653
+ break;
654
+ }
655
+ case "app_list":
656
+ {
657
+ var apps = new List<string>();
658
+ var seen = new HashSet<int>();
659
+ foreach (var p in Process.GetProcesses())
660
+ {
661
+ try
662
+ {
663
+ if (p.MainWindowHandle != IntPtr.Zero && !seen.Contains(p.Id))
664
+ {
665
+ seen.Add(p.Id);
666
+ apps.Add(String.Format("{{\\"name\\":{0},\\"pid\\":{1}}}", JsonStr(p.ProcessName), p.Id));
667
+ }
668
+ } catch {}
669
+ }
670
+ Console.WriteLine("{\\"ok\\":true,\\"apps\\":[" + string.Join(",", apps) + "]}");
671
+ break;
672
+ }
673
+ case "clipboard_read":
674
+ {
675
+ string text = Clipboard.GetText() ?? "";
676
+ Console.WriteLine("{\\"ok\\":true,\\"text\\":" + JsonStr(text) + "}");
677
+ break;
678
+ }
679
+ case "clipboard_write":
680
+ {
681
+ var text = GetStr(req, "text");
682
+ if (string.IsNullOrEmpty(text)) Clipboard.Clear();
683
+ else Clipboard.SetText(text);
684
+ Console.WriteLine("{\\"ok\\":true}");
685
+ break;
686
+ }
687
+ case "screen_info":
688
+ {
689
+ var screen = Screen.PrimaryScreen;
690
+ float dpi;
691
+ using (var g = Graphics.FromHwnd(IntPtr.Zero)) { dpi = g.DpiX; }
692
+ double scale = Math.Round(dpi / 96.0, 2);
693
+ Console.WriteLine("{\\"ok\\":true,\\"width\\":" + screen.Bounds.Width + ",\\"height\\":" + screen.Bounds.Height + ",\\"scaleFactor\\":" + scale + "}");
694
+ break;
695
+ }
696
+ case "ax_tree":
697
+ {
698
+ int pid = GetInt(req, "pid");
699
+ int maxDepth = GetInt(req, "maxDepth", 8);
700
+ var rolesObj = GetList(req, "roles");
701
+ var roles = rolesObj.Count > 0 ? rolesObj.Select(r => r.ToString().ToLower()).ToList() : null;
702
+
703
+ var root = FindAppRoot(pid);
704
+ axElementCount = 0;
705
+ var treeJson = WalkAXTree(root, 0, maxDepth, roles, "0");
706
+ Console.WriteLine("{\\"ok\\":true,\\"root\\":" + treeJson + ",\\"elementCount\\":" + axElementCount + "}");
707
+ break;
708
+ }
709
+ case "ax_action":
710
+ {
711
+ var elementId = GetStr(req, "elementId");
712
+ var act = GetStr(req, "action_name");
713
+ int pid = GetInt(req, "pid");
714
+
715
+ var root = FindAppRoot(pid);
716
+ var el = NavigateToElement(root, elementId);
717
+
718
+ switch (act.ToLower())
719
+ {
720
+ case "invoke":
721
+ case "press":
722
+ case "click":
723
+ ((InvokePattern)el.GetCurrentPattern(InvokePattern.Pattern)).Invoke();
724
+ break;
725
+ case "toggle":
726
+ ((TogglePattern)el.GetCurrentPattern(TogglePattern.Pattern)).Toggle();
727
+ break;
728
+ case "expand":
729
+ ((ExpandCollapsePattern)el.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Expand();
730
+ break;
731
+ case "collapse":
732
+ ((ExpandCollapsePattern)el.GetCurrentPattern(ExpandCollapsePattern.Pattern)).Collapse();
733
+ break;
734
+ default:
735
+ if (act.StartsWith("setvalue:"))
736
+ {
737
+ var value = act.Substring(9);
738
+ ((ValuePattern)el.GetCurrentPattern(ValuePattern.Pattern)).SetValue(value);
739
+ }
740
+ else
741
+ {
742
+ throw new Exception("Unsupported action: " + act);
743
+ }
744
+ break;
745
+ }
746
+ Console.WriteLine("{\\"ok\\":true}");
747
+ break;
748
+ }
749
+ case "ax_search":
750
+ {
751
+ var query = GetStr(req, "query");
752
+ var roleFilter = GetStr(req, "role", null);
753
+ int pid = GetInt(req, "pid");
754
+ int maxResults = GetInt(req, "maxResults", 20);
755
+
756
+ var root = FindAppRoot(pid);
757
+ var results = new List<string>();
758
+ SearchAXTree(root, query, roleFilter, maxResults, results, "0", 0, 20);
759
+ Console.WriteLine("{\\"ok\\":true,\\"elements\\":[" + string.Join(",", results) + "]}");
760
+ break;
761
+ }
762
+ default:
763
+ Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr("Unknown action: " + action) + "}");
764
+ break;
765
+ }
766
+ }
767
+ catch (Exception ex)
768
+ {
769
+ Console.WriteLine("{\\"ok\\":false,\\"error\\":" + JsonStr(ex.Message) + "}");
770
+ }
771
+ }
772
+ }
773
+ `;
774
+
775
+ let compiled = false;
776
+
777
+ export async function ensureHelper(): Promise<string> {
778
+ if (compiled && existsSync(HELPER_PATH)) return HELPER_PATH;
779
+
780
+ if (existsSync(HELPER_PATH)) {
781
+ compiled = true;
782
+ return HELPER_PATH;
783
+ }
784
+
785
+ mkdirSync(BIN_DIR, { recursive: true });
786
+
787
+ const srcPath = join(BIN_DIR, `desktop-helper-win-${HELPER_VERSION}.cs`);
788
+ writeFileSync(srcPath, CSHARP_SOURCE);
789
+
790
+ const cscPath = join(
791
+ process.env.WINDIR || 'C:\\Windows',
792
+ 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe',
793
+ );
794
+
795
+ await new Promise<void>((resolve, reject) => {
796
+ const proc = spawn(cscPath, [
797
+ '/nologo',
798
+ '/optimize+',
799
+ `/out:${HELPER_PATH}`,
800
+ '/r:System.Windows.Forms.dll',
801
+ '/r:System.Drawing.dll',
802
+ '/r:UIAutomationClient.dll',
803
+ '/r:UIAutomationTypes.dll',
804
+ '/r:WindowsBase.dll',
805
+ '/r:PresentationCore.dll',
806
+ srcPath,
807
+ ], {
808
+ stdio: ['ignore', 'pipe', 'pipe'],
809
+ });
810
+
811
+ let stderr = '';
812
+ proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); });
813
+
814
+ proc.on('close', (code) => {
815
+ if (code === 0) {
816
+ compiled = true;
817
+ resolve();
818
+ } else {
819
+ reject(new Error(`csc.exe failed (exit ${code}): ${stderr}`));
820
+ }
821
+ });
822
+
823
+ proc.on('error', (err) => {
824
+ reject(new Error(`csc.exe not found: ${err.message}. Ensure .NET Framework 4.x is installed.`));
825
+ });
826
+ });
827
+
828
+ return HELPER_PATH;
829
+ }
830
+
831
+ export interface CSharpHelperRequest {
832
+ action: string;
833
+ x?: number;
834
+ y?: number;
835
+ toX?: number;
836
+ toY?: number;
837
+ button?: string;
838
+ clicks?: number;
839
+ modifiers?: string[];
840
+ deltaX?: number;
841
+ deltaY?: number;
842
+ keys?: string[];
843
+ text?: string;
844
+ name?: string;
845
+ windowId?: number;
846
+ width?: number;
847
+ height?: number;
848
+ pid?: number;
849
+ maxDepth?: number;
850
+ roles?: string[];
851
+ elementId?: string;
852
+ action_name?: string;
853
+ query?: string;
854
+ role?: string;
855
+ maxResults?: number;
856
+ value?: string;
857
+ }
858
+
859
+ export interface CSharpHelperResponse {
860
+ ok: boolean;
861
+ x?: number;
862
+ y?: number;
863
+ error?: string;
864
+ image?: string;
865
+ width?: number;
866
+ height?: number;
867
+ format?: string;
868
+ windows?: any[];
869
+ apps?: any[];
870
+ text?: string;
871
+ scaleFactor?: number;
872
+ root?: any;
873
+ elementCount?: number;
874
+ elements?: any[];
875
+ }
876
+
877
+ export async function execHelper(request: CSharpHelperRequest): Promise<CSharpHelperResponse> {
878
+ const helperPath = await ensureHelper();
879
+
880
+ return new Promise((resolve, reject) => {
881
+ const proc = spawn(helperPath, [], {
882
+ stdio: ['pipe', 'pipe', 'pipe'],
883
+ });
884
+
885
+ let stdout = '';
886
+ let stderr = '';
887
+
888
+ proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
889
+ proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
890
+
891
+ proc.on('close', (code) => {
892
+ if (code !== 0) {
893
+ reject(new Error(`Helper failed (exit ${code}): ${stderr}`));
894
+ return;
895
+ }
896
+
897
+ try {
898
+ const response = JSON.parse(stdout.trim()) as CSharpHelperResponse;
899
+ if (!response.ok && response.error) {
900
+ reject(new Error(response.error));
901
+ return;
902
+ }
903
+ resolve(response);
904
+ } catch {
905
+ reject(new Error(`Invalid helper output: ${stdout}`));
906
+ }
907
+ });
908
+
909
+ proc.on('error', reject);
910
+
911
+ proc.stdin.write(JSON.stringify(request));
912
+ proc.stdin.end();
913
+ });
914
+ }