screenhand 0.1.1 → 0.2.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.
Files changed (177) hide show
  1. package/README.md +458 -93
  2. package/dist/.audit-log.jsonl +55 -0
  3. package/dist/.screenhand/memory/.lock +1 -0
  4. package/dist/.screenhand/memory/actions.jsonl +85 -0
  5. package/dist/.screenhand/memory/errors.jsonl +5 -0
  6. package/dist/.screenhand/memory/errors.jsonl.bak +4 -0
  7. package/dist/.screenhand/memory/state.json +35 -0
  8. package/dist/.screenhand/memory/state.json.bak +35 -0
  9. package/dist/.screenhand/memory/strategies.jsonl +12 -0
  10. package/dist/agent/cli.js +73 -0
  11. package/dist/agent/loop.js +258 -0
  12. package/dist/config.js +9 -0
  13. package/dist/index.js +56 -0
  14. package/dist/logging/timeline-logger.js +29 -0
  15. package/dist/mcp/mcp-stdio-server.js +448 -0
  16. package/dist/mcp/server.js +347 -0
  17. package/dist/mcp-desktop.js +2731 -0
  18. package/dist/mcp-entry.js +59 -0
  19. package/dist/memory/recall.js +160 -0
  20. package/dist/memory/research.js +98 -0
  21. package/dist/memory/seeds.js +89 -0
  22. package/dist/memory/session.js +161 -0
  23. package/dist/memory/store.js +391 -0
  24. package/dist/memory/types.js +4 -0
  25. package/dist/monitor/codex-monitor.js +377 -0
  26. package/dist/monitor/task-queue.js +84 -0
  27. package/dist/monitor/types.js +49 -0
  28. package/dist/native/bridge-client.js +174 -0
  29. package/dist/native/macos-bridge-client.js +5 -0
  30. package/dist/npm-publish-helper.js +117 -0
  31. package/dist/npm-token-cdp.js +113 -0
  32. package/dist/npm-token-create.js +135 -0
  33. package/dist/npm-token-finish.js +126 -0
  34. package/dist/playbook/engine.js +193 -0
  35. package/dist/playbook/index.js +4 -0
  36. package/dist/playbook/recorder.js +519 -0
  37. package/dist/playbook/runner.js +392 -0
  38. package/dist/playbook/store.js +166 -0
  39. package/dist/playbook/types.js +4 -0
  40. package/dist/runtime/accessibility-adapter.js +377 -0
  41. package/dist/runtime/app-adapter.js +48 -0
  42. package/dist/runtime/applescript-adapter.js +283 -0
  43. package/dist/runtime/ax-role-map.js +80 -0
  44. package/dist/runtime/browser-adapter.js +36 -0
  45. package/dist/runtime/cdp-chrome-adapter.js +505 -0
  46. package/dist/runtime/composite-adapter.js +205 -0
  47. package/dist/runtime/executor.js +250 -0
  48. package/dist/runtime/locator-cache.js +12 -0
  49. package/dist/runtime/planning-loop.js +47 -0
  50. package/dist/runtime/service.js +372 -0
  51. package/dist/runtime/session-manager.js +28 -0
  52. package/dist/runtime/state-observer.js +105 -0
  53. package/dist/runtime/vision-adapter.js +208 -0
  54. package/dist/scripts/codex-monitor-daemon.js +335 -0
  55. package/dist/scripts/supervisor-daemon.js +272 -0
  56. package/dist/scripts/worker-daemon.js +228 -0
  57. package/dist/src/agent/cli.js +82 -0
  58. package/dist/src/agent/loop.js +274 -0
  59. package/{src/config.ts → dist/src/config.js} +5 -10
  60. package/{src/index.ts → dist/src/index.js} +32 -52
  61. package/dist/src/jobs/manager.js +237 -0
  62. package/dist/src/jobs/runner.js +683 -0
  63. package/dist/src/jobs/store.js +102 -0
  64. package/dist/src/jobs/types.js +30 -0
  65. package/dist/src/jobs/worker.js +97 -0
  66. package/dist/src/logging/timeline-logger.js +45 -0
  67. package/dist/src/mcp/mcp-stdio-server.js +464 -0
  68. package/dist/src/mcp/server.js +363 -0
  69. package/dist/src/mcp-entry.js +60 -0
  70. package/dist/src/memory/recall.js +170 -0
  71. package/dist/src/memory/research.js +104 -0
  72. package/dist/src/memory/seeds.js +101 -0
  73. package/dist/src/memory/service.js +421 -0
  74. package/dist/src/memory/session.js +169 -0
  75. package/dist/src/memory/store.js +422 -0
  76. package/dist/src/memory/types.js +17 -0
  77. package/dist/src/monitor/codex-monitor.js +382 -0
  78. package/dist/src/monitor/task-queue.js +97 -0
  79. package/dist/src/monitor/types.js +62 -0
  80. package/dist/src/native/bridge-client.js +190 -0
  81. package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
  82. package/dist/src/playbook/engine.js +201 -0
  83. package/dist/src/playbook/index.js +20 -0
  84. package/dist/src/playbook/recorder.js +535 -0
  85. package/dist/src/playbook/runner.js +408 -0
  86. package/dist/src/playbook/store.js +183 -0
  87. package/dist/src/playbook/types.js +17 -0
  88. package/dist/src/runtime/accessibility-adapter.js +393 -0
  89. package/dist/src/runtime/app-adapter.js +64 -0
  90. package/dist/src/runtime/applescript-adapter.js +299 -0
  91. package/dist/src/runtime/ax-role-map.js +96 -0
  92. package/dist/src/runtime/browser-adapter.js +52 -0
  93. package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
  94. package/dist/src/runtime/composite-adapter.js +221 -0
  95. package/dist/src/runtime/execution-contract.js +159 -0
  96. package/dist/src/runtime/executor.js +266 -0
  97. package/{src/runtime/locator-cache.ts → dist/src/runtime/locator-cache.js} +10 -15
  98. package/dist/src/runtime/planning-loop.js +63 -0
  99. package/dist/src/runtime/service.js +388 -0
  100. package/dist/src/runtime/session-manager.js +60 -0
  101. package/dist/src/runtime/state-observer.js +121 -0
  102. package/dist/src/runtime/vision-adapter.js +224 -0
  103. package/dist/src/supervisor/locks.js +186 -0
  104. package/dist/src/supervisor/supervisor.js +403 -0
  105. package/dist/src/supervisor/types.js +30 -0
  106. package/dist/src/test-mcp-protocol.js +154 -0
  107. package/dist/src/types.js +17 -0
  108. package/dist/src/util/atomic-write.js +118 -0
  109. package/dist/test-mcp-protocol.js +138 -0
  110. package/dist/types.js +1 -0
  111. package/package.json +18 -4
  112. package/.claude/commands/automate.md +0 -28
  113. package/.claude/commands/debug-ui.md +0 -19
  114. package/.claude/commands/screenshot.md +0 -15
  115. package/.github/FUNDING.yml +0 -1
  116. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  117. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  118. package/.mcp.json +0 -8
  119. package/DESKTOP_MCP_GUIDE.md +0 -92
  120. package/SECURITY.md +0 -44
  121. package/docs/architecture.md +0 -47
  122. package/install-skills.sh +0 -19
  123. package/mcp-bridge.ts +0 -271
  124. package/mcp-desktop.ts +0 -1221
  125. package/native/macos-bridge/Package.swift +0 -21
  126. package/native/macos-bridge/Sources/AccessibilityBridge.swift +0 -261
  127. package/native/macos-bridge/Sources/AppManagement.swift +0 -129
  128. package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +0 -242
  129. package/native/macos-bridge/Sources/ObserverBridge.swift +0 -120
  130. package/native/macos-bridge/Sources/VisionBridge.swift +0 -80
  131. package/native/macos-bridge/Sources/main.swift +0 -345
  132. package/native/windows-bridge/AppManagement.cs +0 -234
  133. package/native/windows-bridge/InputBridge.cs +0 -436
  134. package/native/windows-bridge/Program.cs +0 -265
  135. package/native/windows-bridge/ScreenCapture.cs +0 -329
  136. package/native/windows-bridge/UIAutomationBridge.cs +0 -571
  137. package/native/windows-bridge/WindowsBridge.csproj +0 -17
  138. package/playbooks/devpost.json +0 -186
  139. package/playbooks/instagram.json +0 -41
  140. package/playbooks/instagram_v2.json +0 -201
  141. package/playbooks/x_v1.json +0 -211
  142. package/scripts/devpost-live-loop.mjs +0 -421
  143. package/src/logging/timeline-logger.ts +0 -55
  144. package/src/mcp/server.ts +0 -449
  145. package/src/memory/recall.ts +0 -191
  146. package/src/memory/research.ts +0 -146
  147. package/src/memory/seeds.ts +0 -123
  148. package/src/memory/session.ts +0 -201
  149. package/src/memory/store.ts +0 -434
  150. package/src/memory/types.ts +0 -69
  151. package/src/native/bridge-client.ts +0 -239
  152. package/src/runtime/accessibility-adapter.ts +0 -487
  153. package/src/runtime/app-adapter.ts +0 -169
  154. package/src/runtime/applescript-adapter.ts +0 -376
  155. package/src/runtime/ax-role-map.ts +0 -102
  156. package/src/runtime/browser-adapter.ts +0 -129
  157. package/src/runtime/cdp-chrome-adapter.ts +0 -676
  158. package/src/runtime/composite-adapter.ts +0 -274
  159. package/src/runtime/executor.ts +0 -396
  160. package/src/runtime/planning-loop.ts +0 -81
  161. package/src/runtime/service.ts +0 -448
  162. package/src/runtime/session-manager.ts +0 -50
  163. package/src/runtime/state-observer.ts +0 -136
  164. package/src/runtime/vision-adapter.ts +0 -297
  165. package/src/types.ts +0 -297
  166. package/tests/bridge-client.test.ts +0 -176
  167. package/tests/browser-stealth.test.ts +0 -210
  168. package/tests/composite-adapter.test.ts +0 -64
  169. package/tests/mcp-server.test.ts +0 -151
  170. package/tests/memory-recall.test.ts +0 -339
  171. package/tests/memory-research.test.ts +0 -159
  172. package/tests/memory-seeds.test.ts +0 -120
  173. package/tests/memory-store.test.ts +0 -392
  174. package/tests/types.test.ts +0 -92
  175. package/tsconfig.check.json +0 -17
  176. package/tsconfig.json +0 -19
  177. package/vitest.config.ts +0 -8
@@ -1,234 +0,0 @@
1
- using System.Diagnostics;
2
- using System.Runtime.InteropServices;
3
- using System.Text;
4
-
5
- namespace WindowsBridge;
6
-
7
- /// <summary>
8
- /// Process and window management using Windows APIs.
9
- /// Equivalent to macOS AppManagement.swift.
10
- /// </summary>
11
- class AppManagement
12
- {
13
- // P/Invoke declarations
14
- [DllImport("user32.dll")]
15
- private static extern IntPtr GetForegroundWindow();
16
-
17
- [DllImport("user32.dll")]
18
- private static extern bool SetForegroundWindow(IntPtr hWnd);
19
-
20
- [DllImport("user32.dll")]
21
- private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
22
-
23
- [DllImport("user32.dll")]
24
- private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
25
-
26
- [DllImport("user32.dll")]
27
- private static extern bool IsWindowVisible(IntPtr hWnd);
28
-
29
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
30
- private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
31
-
32
- [DllImport("user32.dll")]
33
- private static extern int GetWindowTextLength(IntPtr hWnd);
34
-
35
- [DllImport("user32.dll")]
36
- private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
37
-
38
- [DllImport("user32.dll")]
39
- private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
40
-
41
- [DllImport("user32.dll")]
42
- private static extern bool AllowSetForegroundWindow(int dwProcessId);
43
-
44
- [StructLayout(LayoutKind.Sequential)]
45
- private struct RECT
46
- {
47
- public int Left, Top, Right, Bottom;
48
- }
49
-
50
- private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
51
-
52
- private const int SW_RESTORE = 9;
53
- private const int SW_SHOW = 5;
54
-
55
- /// <summary>
56
- /// List all running GUI applications.
57
- /// On Windows, "bundleId" is mapped to the process name (e.g., "chrome", "notepad").
58
- /// </summary>
59
- public List<Dictionary<string, object>> ListRunningApps()
60
- {
61
- var apps = new List<Dictionary<string, object>>();
62
- var seen = new HashSet<int>();
63
- var foregroundHwnd = GetForegroundWindow();
64
- GetWindowThreadProcessId(foregroundHwnd, out uint foregroundPid);
65
-
66
- var processes = Process.GetProcesses()
67
- .Where(p => !string.IsNullOrEmpty(p.MainWindowTitle) && p.MainWindowHandle != IntPtr.Zero)
68
- .OrderBy(p => p.ProcessName);
69
-
70
- foreach (var proc in processes)
71
- {
72
- try
73
- {
74
- if (seen.Contains(proc.Id)) continue;
75
- seen.Add(proc.Id);
76
-
77
- apps.Add(new Dictionary<string, object>
78
- {
79
- ["name"] = proc.ProcessName,
80
- ["bundleId"] = proc.ProcessName.ToLowerInvariant(), // Windows equivalent
81
- ["pid"] = proc.Id,
82
- ["isActive"] = proc.Id == (int)foregroundPid,
83
- });
84
- }
85
- catch
86
- {
87
- // Process may have exited
88
- }
89
- }
90
-
91
- return apps;
92
- }
93
-
94
- /// <summary>
95
- /// Launch an application by process name or path.
96
- /// </summary>
97
- public Dictionary<string, object> LaunchApp(string bundleId)
98
- {
99
- Process process;
100
- try
101
- {
102
- // Try launching as a process name first (e.g., "notepad", "calc")
103
- process = Process.Start(new ProcessStartInfo
104
- {
105
- FileName = bundleId,
106
- UseShellExecute = true,
107
- }) ?? throw new BridgeException($"Failed to launch: {bundleId}");
108
- }
109
- catch
110
- {
111
- throw new BridgeException($"Could not launch application: {bundleId}");
112
- }
113
-
114
- // Wait briefly for the process to initialize
115
- try { process.WaitForInputIdle(3000); } catch { }
116
-
117
- return new Dictionary<string, object>
118
- {
119
- ["appName"] = process.ProcessName,
120
- ["bundleId"] = process.ProcessName.ToLowerInvariant(),
121
- ["pid"] = process.Id,
122
- };
123
- }
124
-
125
- /// <summary>
126
- /// Focus/activate a window by process name.
127
- /// </summary>
128
- public Dictionary<string, object> FocusApp(string bundleId)
129
- {
130
- var name = bundleId.ToLowerInvariant().Replace(".exe", "");
131
- var processes = Process.GetProcessesByName(name);
132
-
133
- if (processes.Length == 0)
134
- {
135
- // Try exact match with original casing
136
- processes = Process.GetProcesses()
137
- .Where(p => p.ProcessName.Equals(bundleId, StringComparison.OrdinalIgnoreCase) ||
138
- p.ProcessName.Equals(name, StringComparison.OrdinalIgnoreCase))
139
- .ToArray();
140
- }
141
-
142
- if (processes.Length == 0)
143
- throw new BridgeException($"No running process found: {bundleId}");
144
-
145
- var proc = processes.First(p => p.MainWindowHandle != IntPtr.Zero);
146
- var hwnd = proc.MainWindowHandle;
147
-
148
- if (hwnd == IntPtr.Zero)
149
- throw new BridgeException($"Process {bundleId} has no visible window");
150
-
151
- ShowWindow(hwnd, SW_RESTORE);
152
- AllowSetForegroundWindow(proc.Id);
153
- SetForegroundWindow(hwnd);
154
-
155
- return new Dictionary<string, object> { ["ok"] = true };
156
- }
157
-
158
- /// <summary>
159
- /// List all visible windows with their bounds.
160
- /// </summary>
161
- public List<Dictionary<string, object>> ListWindows()
162
- {
163
- var windows = new List<Dictionary<string, object>>();
164
-
165
- EnumWindows((hWnd, _) =>
166
- {
167
- if (!IsWindowVisible(hWnd)) return true;
168
-
169
- var titleLength = GetWindowTextLength(hWnd);
170
- if (titleLength == 0) return true;
171
-
172
- var titleBuilder = new StringBuilder(titleLength + 1);
173
- GetWindowText(hWnd, titleBuilder, titleBuilder.Capacity);
174
- var title = titleBuilder.ToString();
175
-
176
- GetWindowThreadProcessId(hWnd, out uint pid);
177
- GetWindowRect(hWnd, out RECT rect);
178
-
179
- string appName;
180
- try
181
- {
182
- appName = Process.GetProcessById((int)pid).ProcessName;
183
- }
184
- catch
185
- {
186
- appName = "unknown";
187
- }
188
-
189
- windows.Add(new Dictionary<string, object>
190
- {
191
- ["windowId"] = hWnd.ToInt64(),
192
- ["appName"] = appName,
193
- ["title"] = title,
194
- ["pid"] = (int)pid,
195
- ["bounds"] = new Dictionary<string, object>
196
- {
197
- ["x"] = rect.Left,
198
- ["y"] = rect.Top,
199
- ["width"] = rect.Right - rect.Left,
200
- ["height"] = rect.Bottom - rect.Top,
201
- },
202
- });
203
-
204
- return true;
205
- }, IntPtr.Zero);
206
-
207
- return windows;
208
- }
209
-
210
- /// <summary>
211
- /// Get the frontmost (foreground) application.
212
- /// </summary>
213
- public Dictionary<string, object> FrontmostApp()
214
- {
215
- var hwnd = GetForegroundWindow();
216
- GetWindowThreadProcessId(hwnd, out uint pid);
217
-
218
- try
219
- {
220
- var proc = Process.GetProcessById((int)pid);
221
- return new Dictionary<string, object>
222
- {
223
- ["name"] = proc.ProcessName,
224
- ["bundleId"] = proc.ProcessName.ToLowerInvariant(),
225
- ["pid"] = proc.Id,
226
- ["isActive"] = true,
227
- };
228
- }
229
- catch
230
- {
231
- throw new BridgeException("Could not determine frontmost application");
232
- }
233
- }
234
- }
@@ -1,436 +0,0 @@
1
- using System.Runtime.InteropServices;
2
-
3
- namespace WindowsBridge;
4
-
5
- /// <summary>
6
- /// Mouse and keyboard input injection using SendInput().
7
- /// Equivalent to macOS CoreGraphicsBridge.swift.
8
- /// </summary>
9
- class InputBridge
10
- {
11
- // SendInput structures and constants
12
- [DllImport("user32.dll", SetLastError = true)]
13
- private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
14
-
15
- [DllImport("user32.dll")]
16
- private static extern bool SetCursorPos(int X, int Y);
17
-
18
- [DllImport("user32.dll")]
19
- private static extern bool GetCursorPos(out POINT lpPoint);
20
-
21
- [DllImport("user32.dll")]
22
- private static extern int GetSystemMetrics(int nIndex);
23
-
24
- [StructLayout(LayoutKind.Sequential)]
25
- private struct POINT
26
- {
27
- public int X, Y;
28
- }
29
-
30
- [StructLayout(LayoutKind.Sequential)]
31
- private struct INPUT
32
- {
33
- public uint Type;
34
- public INPUTUNION U;
35
- }
36
-
37
- [StructLayout(LayoutKind.Explicit)]
38
- private struct INPUTUNION
39
- {
40
- [FieldOffset(0)] public MOUSEINPUT mi;
41
- [FieldOffset(0)] public KEYBDINPUT ki;
42
- }
43
-
44
- [StructLayout(LayoutKind.Sequential)]
45
- private struct MOUSEINPUT
46
- {
47
- public int dx, dy;
48
- public uint mouseData;
49
- public uint dwFlags;
50
- public uint time;
51
- public IntPtr dwExtraInfo;
52
- }
53
-
54
- [StructLayout(LayoutKind.Sequential)]
55
- private struct KEYBDINPUT
56
- {
57
- public ushort wVk;
58
- public ushort wScan;
59
- public uint dwFlags;
60
- public uint time;
61
- public IntPtr dwExtraInfo;
62
- }
63
-
64
- // Input type constants
65
- private const uint INPUT_MOUSE = 0;
66
- private const uint INPUT_KEYBOARD = 1;
67
-
68
- // Mouse event flags
69
- private const uint MOUSEEVENTF_MOVE = 0x0001;
70
- private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
71
- private const uint MOUSEEVENTF_LEFTUP = 0x0004;
72
- private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
73
- private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
74
- private const uint MOUSEEVENTF_MIDDLEDOWN = 0x0020;
75
- private const uint MOUSEEVENTF_MIDDLEUP = 0x0040;
76
- private const uint MOUSEEVENTF_WHEEL = 0x0800;
77
- private const uint MOUSEEVENTF_HWHEEL = 0x1000;
78
- private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
79
-
80
- // Keyboard event flags
81
- private const uint KEYEVENTF_KEYUP = 0x0002;
82
- private const uint KEYEVENTF_UNICODE = 0x0004;
83
-
84
- // System metrics
85
- private const int SM_CXSCREEN = 0;
86
- private const int SM_CYSCREEN = 1;
87
-
88
- // Virtual key codes
89
- private const ushort VK_SHIFT = 0x10;
90
- private const ushort VK_CONTROL = 0x11;
91
- private const ushort VK_MENU = 0x12; // Alt
92
- private const ushort VK_LWIN = 0x5B;
93
- private const ushort VK_RETURN = 0x0D;
94
- private const ushort VK_TAB = 0x09;
95
- private const ushort VK_ESCAPE = 0x1B;
96
- private const ushort VK_SPACE = 0x20;
97
- private const ushort VK_BACK = 0x08;
98
- private const ushort VK_DELETE = 0x2E;
99
- private const ushort VK_UP = 0x26;
100
- private const ushort VK_DOWN = 0x28;
101
- private const ushort VK_LEFT = 0x25;
102
- private const ushort VK_RIGHT = 0x27;
103
- private const ushort VK_HOME = 0x24;
104
- private const ushort VK_END = 0x23;
105
- private const ushort VK_PRIOR = 0x21; // Page Up
106
- private const ushort VK_NEXT = 0x22; // Page Down
107
- private const ushort VK_F1 = 0x70;
108
-
109
- /// <summary>
110
- /// Click at screen coordinates.
111
- /// </summary>
112
- public Dictionary<string, object> MouseClick(double x, double y, string button, int clickCount)
113
- {
114
- SetCursorPos((int)x, (int)y);
115
- System.Threading.Thread.Sleep(10);
116
-
117
- uint downFlag, upFlag;
118
- switch (button.ToLowerInvariant())
119
- {
120
- case "right":
121
- downFlag = MOUSEEVENTF_RIGHTDOWN;
122
- upFlag = MOUSEEVENTF_RIGHTUP;
123
- break;
124
- case "middle":
125
- downFlag = MOUSEEVENTF_MIDDLEDOWN;
126
- upFlag = MOUSEEVENTF_MIDDLEUP;
127
- break;
128
- default: // left
129
- downFlag = MOUSEEVENTF_LEFTDOWN;
130
- upFlag = MOUSEEVENTF_LEFTUP;
131
- break;
132
- }
133
-
134
- for (int i = 0; i < clickCount; i++)
135
- {
136
- var inputs = new INPUT[]
137
- {
138
- new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = downFlag } } },
139
- new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = upFlag } } },
140
- };
141
- SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
142
-
143
- if (i < clickCount - 1)
144
- System.Threading.Thread.Sleep(30);
145
- }
146
-
147
- return new Dictionary<string, object> { ["ok"] = true };
148
- }
149
-
150
- /// <summary>
151
- /// Move mouse to screen coordinates.
152
- /// </summary>
153
- public Dictionary<string, object> MouseMove(double x, double y)
154
- {
155
- SetCursorPos((int)x, (int)y);
156
- return new Dictionary<string, object> { ["ok"] = true };
157
- }
158
-
159
- /// <summary>
160
- /// Drag from one point to another with interpolation.
161
- /// </summary>
162
- public Dictionary<string, object> MouseDrag(double fromX, double fromY, double toX, double toY)
163
- {
164
- SetCursorPos((int)fromX, (int)fromY);
165
- System.Threading.Thread.Sleep(50);
166
-
167
- // Mouse down
168
- var downInput = new INPUT[]
169
- {
170
- new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTDOWN } } },
171
- };
172
- SendInput(1, downInput, Marshal.SizeOf<INPUT>());
173
- System.Threading.Thread.Sleep(50);
174
-
175
- // Interpolate movement
176
- int steps = 20;
177
- for (int i = 1; i <= steps; i++)
178
- {
179
- double t = (double)i / steps;
180
- int cx = (int)(fromX + (toX - fromX) * t);
181
- int cy = (int)(fromY + (toY - fromY) * t);
182
- SetCursorPos(cx, cy);
183
- System.Threading.Thread.Sleep(10);
184
- }
185
-
186
- // Mouse up
187
- var upInput = new INPUT[]
188
- {
189
- new() { Type = INPUT_MOUSE, U = new INPUTUNION { mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_LEFTUP } } },
190
- };
191
- SendInput(1, upInput, Marshal.SizeOf<INPUT>());
192
-
193
- return new Dictionary<string, object> { ["ok"] = true };
194
- }
195
-
196
- /// <summary>
197
- /// Press a key combination (e.g., ["ctrl", "c"], ["alt", "f4"]).
198
- /// Maps macOS modifier names to Windows equivalents.
199
- /// </summary>
200
- public Dictionary<string, object> KeyCombo(string[] keys)
201
- {
202
- var modifiers = new List<ushort>();
203
- var regularKeys = new List<ushort>();
204
-
205
- foreach (var key in keys)
206
- {
207
- var vk = MapKeyToVK(key.Trim().ToLowerInvariant());
208
- if (IsModifier(vk))
209
- modifiers.Add(vk);
210
- else
211
- regularKeys.Add(vk);
212
- }
213
-
214
- var inputs = new List<INPUT>();
215
-
216
- // Press modifiers down
217
- foreach (var mod in modifiers)
218
- {
219
- inputs.Add(new INPUT
220
- {
221
- Type = INPUT_KEYBOARD,
222
- U = new INPUTUNION { ki = new KEYBDINPUT { wVk = mod } }
223
- });
224
- }
225
-
226
- // Press regular keys
227
- foreach (var key in regularKeys)
228
- {
229
- inputs.Add(new INPUT
230
- {
231
- Type = INPUT_KEYBOARD,
232
- U = new INPUTUNION { ki = new KEYBDINPUT { wVk = key } }
233
- });
234
- }
235
-
236
- // Release regular keys
237
- foreach (var key in regularKeys)
238
- {
239
- inputs.Add(new INPUT
240
- {
241
- Type = INPUT_KEYBOARD,
242
- U = new INPUTUNION { ki = new KEYBDINPUT { wVk = key, dwFlags = KEYEVENTF_KEYUP } }
243
- });
244
- }
245
-
246
- // Release modifiers (reverse order)
247
- for (int i = modifiers.Count - 1; i >= 0; i--)
248
- {
249
- inputs.Add(new INPUT
250
- {
251
- Type = INPUT_KEYBOARD,
252
- U = new INPUTUNION { ki = new KEYBDINPUT { wVk = modifiers[i], dwFlags = KEYEVENTF_KEYUP } }
253
- });
254
- }
255
-
256
- SendInput((uint)inputs.Count, inputs.ToArray(), Marshal.SizeOf<INPUT>());
257
-
258
- return new Dictionary<string, object> { ["ok"] = true };
259
- }
260
-
261
- /// <summary>
262
- /// Type text character by character using Unicode input.
263
- /// </summary>
264
- public Dictionary<string, object> TypeText(string text)
265
- {
266
- var inputs = new List<INPUT>();
267
-
268
- foreach (var ch in text)
269
- {
270
- // Key down
271
- inputs.Add(new INPUT
272
- {
273
- Type = INPUT_KEYBOARD,
274
- U = new INPUTUNION
275
- {
276
- ki = new KEYBDINPUT
277
- {
278
- wVk = 0,
279
- wScan = (ushort)ch,
280
- dwFlags = KEYEVENTF_UNICODE,
281
- }
282
- }
283
- });
284
- // Key up
285
- inputs.Add(new INPUT
286
- {
287
- Type = INPUT_KEYBOARD,
288
- U = new INPUTUNION
289
- {
290
- ki = new KEYBDINPUT
291
- {
292
- wVk = 0,
293
- wScan = (ushort)ch,
294
- dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP,
295
- }
296
- }
297
- });
298
- }
299
-
300
- // Send in batches to avoid overflow
301
- const int batchSize = 50;
302
- for (int i = 0; i < inputs.Count; i += batchSize)
303
- {
304
- var batch = inputs.Skip(i).Take(batchSize).ToArray();
305
- SendInput((uint)batch.Length, batch, Marshal.SizeOf<INPUT>());
306
- if (i + batchSize < inputs.Count)
307
- System.Threading.Thread.Sleep(10);
308
- }
309
-
310
- return new Dictionary<string, object> { ["ok"] = true };
311
- }
312
-
313
- /// <summary>
314
- /// Scroll at a position.
315
- /// </summary>
316
- public Dictionary<string, object> Scroll(double x, double y, int deltaX, int deltaY)
317
- {
318
- SetCursorPos((int)x, (int)y);
319
- System.Threading.Thread.Sleep(10);
320
-
321
- var inputs = new List<INPUT>();
322
-
323
- // Vertical scroll
324
- if (deltaY != 0)
325
- {
326
- inputs.Add(new INPUT
327
- {
328
- Type = INPUT_MOUSE,
329
- U = new INPUTUNION
330
- {
331
- mi = new MOUSEINPUT
332
- {
333
- dwFlags = MOUSEEVENTF_WHEEL,
334
- mouseData = (uint)(deltaY * 120), // 120 = WHEEL_DELTA
335
- }
336
- }
337
- });
338
- }
339
-
340
- // Horizontal scroll
341
- if (deltaX != 0)
342
- {
343
- inputs.Add(new INPUT
344
- {
345
- Type = INPUT_MOUSE,
346
- U = new INPUTUNION
347
- {
348
- mi = new MOUSEINPUT
349
- {
350
- dwFlags = MOUSEEVENTF_HWHEEL,
351
- mouseData = (uint)(deltaX * 120),
352
- }
353
- }
354
- });
355
- }
356
-
357
- if (inputs.Count > 0)
358
- SendInput((uint)inputs.Count, inputs.ToArray(), Marshal.SizeOf<INPUT>());
359
-
360
- return new Dictionary<string, object> { ["ok"] = true };
361
- }
362
-
363
- // ── Key mapping ──
364
-
365
- private ushort MapKeyToVK(string key)
366
- {
367
- return key switch
368
- {
369
- // Modifiers — map macOS names to Windows
370
- "cmd" or "command" or "win" or "super" => VK_LWIN,
371
- "ctrl" or "control" => VK_CONTROL,
372
- "alt" or "option" or "opt" => VK_MENU,
373
- "shift" => VK_SHIFT,
374
-
375
- // Special keys
376
- "enter" or "return" => VK_RETURN,
377
- "tab" => VK_TAB,
378
- "escape" or "esc" => VK_ESCAPE,
379
- "space" => VK_SPACE,
380
- "backspace" or "delete" => VK_BACK,
381
- "forwarddelete" or "del" => VK_DELETE,
382
- "up" or "uparrow" => VK_UP,
383
- "down" or "downarrow" => VK_DOWN,
384
- "left" or "leftarrow" => VK_LEFT,
385
- "right" or "rightarrow" => VK_RIGHT,
386
- "home" => VK_HOME,
387
- "end" => VK_END,
388
- "pageup" => VK_PRIOR,
389
- "pagedown" => VK_NEXT,
390
-
391
- // Function keys
392
- "f1" => VK_F1,
393
- "f2" => (ushort)(VK_F1 + 1),
394
- "f3" => (ushort)(VK_F1 + 2),
395
- "f4" => (ushort)(VK_F1 + 3),
396
- "f5" => (ushort)(VK_F1 + 4),
397
- "f6" => (ushort)(VK_F1 + 5),
398
- "f7" => (ushort)(VK_F1 + 6),
399
- "f8" => (ushort)(VK_F1 + 7),
400
- "f9" => (ushort)(VK_F1 + 8),
401
- "f10" => (ushort)(VK_F1 + 9),
402
- "f11" => (ushort)(VK_F1 + 10),
403
- "f12" => (ushort)(VK_F1 + 11),
404
-
405
- // Single character — use virtual key code for A-Z, 0-9
406
- _ when key.Length == 1 => CharToVK(key[0]),
407
-
408
- _ => throw new BridgeException($"Unknown key: {key}"),
409
- };
410
- }
411
-
412
- private static ushort CharToVK(char c)
413
- {
414
- c = char.ToUpperInvariant(c);
415
- if (c >= 'A' && c <= 'Z') return (ushort)c; // VK_A..VK_Z = 0x41..0x5A = 'A'..'Z'
416
- if (c >= '0' && c <= '9') return (ushort)c; // VK_0..VK_9 = 0x30..0x39 = '0'..'9'
417
- return c switch
418
- {
419
- '-' => 0xBD, // VK_OEM_MINUS
420
- '=' => 0xBB, // VK_OEM_PLUS
421
- '[' => 0xDB, // VK_OEM_4
422
- ']' => 0xDD, // VK_OEM_6
423
- '\\' => 0xDC, // VK_OEM_5
424
- ';' => 0xBA, // VK_OEM_1
425
- '\'' => 0xDE, // VK_OEM_7
426
- ',' => 0xBC, // VK_OEM_COMMA
427
- '.' => 0xBE, // VK_OEM_PERIOD
428
- '/' => 0xBF, // VK_OEM_2
429
- '`' => 0xC0, // VK_OEM_3
430
- _ => 0,
431
- };
432
- }
433
-
434
- private static bool IsModifier(ushort vk) =>
435
- vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU || vk == VK_LWIN;
436
- }