@mseep/clawdcursor 1.5.5

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 (354) hide show
  1. package/CHANGELOG.md +2264 -0
  2. package/LICENSE +21 -0
  3. package/README.md +385 -0
  4. package/SECURITY.md +44 -0
  5. package/SKILL.md +503 -0
  6. package/dist/core/agent-loop/agent.d.ts +42 -0
  7. package/dist/core/agent-loop/agent.js +1023 -0
  8. package/dist/core/agent-loop/agent.js.map +1 -0
  9. package/dist/core/agent-loop/batch-tool.d.ts +25 -0
  10. package/dist/core/agent-loop/batch-tool.js +218 -0
  11. package/dist/core/agent-loop/batch-tool.js.map +1 -0
  12. package/dist/core/agent-loop/coord-scale.d.ts +72 -0
  13. package/dist/core/agent-loop/coord-scale.js +89 -0
  14. package/dist/core/agent-loop/coord-scale.js.map +1 -0
  15. package/dist/core/agent-loop/focus-guard.d.ts +24 -0
  16. package/dist/core/agent-loop/focus-guard.js +29 -0
  17. package/dist/core/agent-loop/focus-guard.js.map +1 -0
  18. package/dist/core/agent-loop/project-mcp.d.ts +97 -0
  19. package/dist/core/agent-loop/project-mcp.js +253 -0
  20. package/dist/core/agent-loop/project-mcp.js.map +1 -0
  21. package/dist/core/agent-loop/prompt.d.ts +45 -0
  22. package/dist/core/agent-loop/prompt.js +426 -0
  23. package/dist/core/agent-loop/prompt.js.map +1 -0
  24. package/dist/core/agent-loop/tool-meta.d.ts +93 -0
  25. package/dist/core/agent-loop/tool-meta.js +651 -0
  26. package/dist/core/agent-loop/tool-meta.js.map +1 -0
  27. package/dist/core/agent-loop/tools.d.ts +38 -0
  28. package/dist/core/agent-loop/tools.js +2134 -0
  29. package/dist/core/agent-loop/tools.js.map +1 -0
  30. package/dist/core/agent-loop/types.d.ts +170 -0
  31. package/dist/core/agent-loop/types.js +12 -0
  32. package/dist/core/agent-loop/types.js.map +1 -0
  33. package/dist/core/agent.d.ts +51 -0
  34. package/dist/core/agent.js +245 -0
  35. package/dist/core/agent.js.map +1 -0
  36. package/dist/core/app-categories.d.ts +67 -0
  37. package/dist/core/app-categories.js +108 -0
  38. package/dist/core/app-categories.js.map +1 -0
  39. package/dist/core/banner.d.ts +70 -0
  40. package/dist/core/banner.js +245 -0
  41. package/dist/core/banner.js.map +1 -0
  42. package/dist/core/classify/capability.d.ts +45 -0
  43. package/dist/core/classify/capability.js +78 -0
  44. package/dist/core/classify/capability.js.map +1 -0
  45. package/dist/core/decompose/llm-decomposer.d.ts +35 -0
  46. package/dist/core/decompose/llm-decomposer.js +156 -0
  47. package/dist/core/decompose/llm-decomposer.js.map +1 -0
  48. package/dist/core/decompose/parser.d.ts +27 -0
  49. package/dist/core/decompose/parser.js +101 -0
  50. package/dist/core/decompose/parser.js.map +1 -0
  51. package/dist/core/observability/correlation.d.ts +19 -0
  52. package/dist/core/observability/correlation.js +36 -0
  53. package/dist/core/observability/correlation.js.map +1 -0
  54. package/dist/core/observability/cost-meter.d.ts +51 -0
  55. package/dist/core/observability/cost-meter.js +134 -0
  56. package/dist/core/observability/cost-meter.js.map +1 -0
  57. package/dist/core/observability/logger.d.ts +61 -0
  58. package/dist/core/observability/logger.js +550 -0
  59. package/dist/core/observability/logger.js.map +1 -0
  60. package/dist/core/router/aliases.d.ts +50 -0
  61. package/dist/core/router/aliases.js +104 -0
  62. package/dist/core/router/aliases.js.map +1 -0
  63. package/dist/core/router/normalize.d.ts +41 -0
  64. package/dist/core/router/normalize.js +80 -0
  65. package/dist/core/router/normalize.js.map +1 -0
  66. package/dist/core/safety.d.ts +126 -0
  67. package/dist/core/safety.js +568 -0
  68. package/dist/core/safety.js.map +1 -0
  69. package/dist/core/sense/a11y-resolver.d.ts +73 -0
  70. package/dist/core/sense/a11y-resolver.js +76 -0
  71. package/dist/core/sense/a11y-resolver.js.map +1 -0
  72. package/dist/core/sense/fingerprint.d.ts +41 -0
  73. package/dist/core/sense/fingerprint.js +123 -0
  74. package/dist/core/sense/fingerprint.js.map +1 -0
  75. package/dist/core/sense/rank.d.ts +70 -0
  76. package/dist/core/sense/rank.js +192 -0
  77. package/dist/core/sense/rank.js.map +1 -0
  78. package/dist/core/sense/reactive-check.d.ts +40 -0
  79. package/dist/core/sense/reactive-check.js +48 -0
  80. package/dist/core/sense/reactive-check.js.map +1 -0
  81. package/dist/core/sense/snapshot.d.ts +19 -0
  82. package/dist/core/sense/snapshot.js +100 -0
  83. package/dist/core/sense/snapshot.js.map +1 -0
  84. package/dist/core/sense/types.d.ts +66 -0
  85. package/dist/core/sense/types.js +9 -0
  86. package/dist/core/sense/types.js.map +1 -0
  87. package/dist/core/sense/ui-map-anchors.d.ts +7 -0
  88. package/dist/core/sense/ui-map-anchors.js +24 -0
  89. package/dist/core/sense/ui-map-anchors.js.map +1 -0
  90. package/dist/core/sense/ui-map-elements.d.ts +5 -0
  91. package/dist/core/sense/ui-map-elements.js +33 -0
  92. package/dist/core/sense/ui-map-elements.js.map +1 -0
  93. package/dist/core/sense/ui-map-find.d.ts +56 -0
  94. package/dist/core/sense/ui-map-find.js +153 -0
  95. package/dist/core/sense/ui-map-find.js.map +1 -0
  96. package/dist/core/sense/ui-map-fuse.d.ts +4 -0
  97. package/dist/core/sense/ui-map-fuse.js +44 -0
  98. package/dist/core/sense/ui-map-fuse.js.map +1 -0
  99. package/dist/core/sense/ui-map-geom.d.ts +3 -0
  100. package/dist/core/sense/ui-map-geom.js +16 -0
  101. package/dist/core/sense/ui-map-geom.js.map +1 -0
  102. package/dist/core/sense/ui-map-holder.d.ts +58 -0
  103. package/dist/core/sense/ui-map-holder.js +87 -0
  104. package/dist/core/sense/ui-map-holder.js.map +1 -0
  105. package/dist/core/sense/ui-map-normalize.d.ts +19 -0
  106. package/dist/core/sense/ui-map-normalize.js +65 -0
  107. package/dist/core/sense/ui-map-normalize.js.map +1 -0
  108. package/dist/core/sense/ui-map-render.d.ts +4 -0
  109. package/dist/core/sense/ui-map-render.js +34 -0
  110. package/dist/core/sense/ui-map-render.js.map +1 -0
  111. package/dist/core/sense/ui-map-resolve.d.ts +41 -0
  112. package/dist/core/sense/ui-map-resolve.js +59 -0
  113. package/dist/core/sense/ui-map-resolve.js.map +1 -0
  114. package/dist/core/sense/ui-map-types.d.ts +66 -0
  115. package/dist/core/sense/ui-map-types.js +11 -0
  116. package/dist/core/sense/ui-map-types.js.map +1 -0
  117. package/dist/core/sense/ui-map.d.ts +29 -0
  118. package/dist/core/sense/ui-map.js +113 -0
  119. package/dist/core/sense/ui-map.js.map +1 -0
  120. package/dist/core/verify/assertions.d.ts +132 -0
  121. package/dist/core/verify/assertions.js +284 -0
  122. package/dist/core/verify/assertions.js.map +1 -0
  123. package/dist/index.d.ts +21 -0
  124. package/dist/index.js +24 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/llm/browser-config.d.ts +36 -0
  127. package/dist/llm/browser-config.js +83 -0
  128. package/dist/llm/browser-config.js.map +1 -0
  129. package/dist/llm/client.d.ts +268 -0
  130. package/dist/llm/client.js +1094 -0
  131. package/dist/llm/client.js.map +1 -0
  132. package/dist/llm/config.d.ts +79 -0
  133. package/dist/llm/config.js +375 -0
  134. package/dist/llm/config.js.map +1 -0
  135. package/dist/llm/credentials.d.ts +35 -0
  136. package/dist/llm/credentials.js +491 -0
  137. package/dist/llm/credentials.js.map +1 -0
  138. package/dist/llm/external-creds.d.ts +42 -0
  139. package/dist/llm/external-creds.js +169 -0
  140. package/dist/llm/external-creds.js.map +1 -0
  141. package/dist/llm/providers.d.ts +123 -0
  142. package/dist/llm/providers.js +717 -0
  143. package/dist/llm/providers.js.map +1 -0
  144. package/dist/paths.d.ts +31 -0
  145. package/dist/paths.js +147 -0
  146. package/dist/paths.js.map +1 -0
  147. package/dist/platform/accessibility.d.ts +139 -0
  148. package/dist/platform/accessibility.js +670 -0
  149. package/dist/platform/accessibility.js.map +1 -0
  150. package/dist/platform/cdp-driver.d.ts +318 -0
  151. package/dist/platform/cdp-driver.js +1179 -0
  152. package/dist/platform/cdp-driver.js.map +1 -0
  153. package/dist/platform/index.d.ts +11 -0
  154. package/dist/platform/index.js +69 -0
  155. package/dist/platform/index.js.map +1 -0
  156. package/dist/platform/keys.d.ts +17 -0
  157. package/dist/platform/keys.js +129 -0
  158. package/dist/platform/keys.js.map +1 -0
  159. package/dist/platform/launch-poll.d.ts +101 -0
  160. package/dist/platform/launch-poll.js +177 -0
  161. package/dist/platform/launch-poll.js.map +1 -0
  162. package/dist/platform/linux.d.ts +173 -0
  163. package/dist/platform/linux.js +1253 -0
  164. package/dist/platform/linux.js.map +1 -0
  165. package/dist/platform/macos.d.ts +136 -0
  166. package/dist/platform/macos.js +976 -0
  167. package/dist/platform/macos.js.map +1 -0
  168. package/dist/platform/native-desktop.d.ts +145 -0
  169. package/dist/platform/native-desktop.js +936 -0
  170. package/dist/platform/native-desktop.js.map +1 -0
  171. package/dist/platform/native-helper.d.ts +130 -0
  172. package/dist/platform/native-helper.js +592 -0
  173. package/dist/platform/native-helper.js.map +1 -0
  174. package/dist/platform/ocr-engine.d.ts +78 -0
  175. package/dist/platform/ocr-engine.js +363 -0
  176. package/dist/platform/ocr-engine.js.map +1 -0
  177. package/dist/platform/ps-runner.d.ts +28 -0
  178. package/dist/platform/ps-runner.js +228 -0
  179. package/dist/platform/ps-runner.js.map +1 -0
  180. package/dist/platform/types.d.ts +397 -0
  181. package/dist/platform/types.js +15 -0
  182. package/dist/platform/types.js.map +1 -0
  183. package/dist/platform/uri-handler.d.ts +75 -0
  184. package/dist/platform/uri-handler.js +273 -0
  185. package/dist/platform/uri-handler.js.map +1 -0
  186. package/dist/platform/wayland-backend.d.ts +53 -0
  187. package/dist/platform/wayland-backend.js +348 -0
  188. package/dist/platform/wayland-backend.js.map +1 -0
  189. package/dist/platform/windows.d.ts +232 -0
  190. package/dist/platform/windows.js +1210 -0
  191. package/dist/platform/windows.js.map +1 -0
  192. package/dist/postbuild.d.ts +10 -0
  193. package/dist/postbuild.js +98 -0
  194. package/dist/postbuild.js.map +1 -0
  195. package/dist/schema/snapshot.d.ts +33 -0
  196. package/dist/schema/snapshot.js +90 -0
  197. package/dist/schema/snapshot.js.map +1 -0
  198. package/dist/shortcuts.d.ts +30 -0
  199. package/dist/shortcuts.js +261 -0
  200. package/dist/shortcuts.js.map +1 -0
  201. package/dist/surface/cli.d.ts +7 -0
  202. package/dist/surface/cli.js +1556 -0
  203. package/dist/surface/cli.js.map +1 -0
  204. package/dist/surface/dashboard.d.ts +8 -0
  205. package/dist/surface/dashboard.js +1193 -0
  206. package/dist/surface/dashboard.js.map +1 -0
  207. package/dist/surface/doctor.d.ts +29 -0
  208. package/dist/surface/doctor.js +1514 -0
  209. package/dist/surface/doctor.js.map +1 -0
  210. package/dist/surface/format.d.ts +10 -0
  211. package/dist/surface/format.js +37 -0
  212. package/dist/surface/format.js.map +1 -0
  213. package/dist/surface/http-utility.d.ts +65 -0
  214. package/dist/surface/http-utility.js +336 -0
  215. package/dist/surface/http-utility.js.map +1 -0
  216. package/dist/surface/mcp-server.d.ts +91 -0
  217. package/dist/surface/mcp-server.js +280 -0
  218. package/dist/surface/mcp-server.js.map +1 -0
  219. package/dist/surface/onboarding.d.ts +15 -0
  220. package/dist/surface/onboarding.js +184 -0
  221. package/dist/surface/onboarding.js.map +1 -0
  222. package/dist/surface/pidfile.d.ts +79 -0
  223. package/dist/surface/pidfile.js +263 -0
  224. package/dist/surface/pidfile.js.map +1 -0
  225. package/dist/surface/readiness.d.ts +45 -0
  226. package/dist/surface/readiness.js +230 -0
  227. package/dist/surface/readiness.js.map +1 -0
  228. package/dist/surface/report.d.ts +68 -0
  229. package/dist/surface/report.js +341 -0
  230. package/dist/surface/report.js.map +1 -0
  231. package/dist/surface/skill-register.d.ts +14 -0
  232. package/dist/surface/skill-register.js +150 -0
  233. package/dist/surface/skill-register.js.map +1 -0
  234. package/dist/surface/version.d.ts +6 -0
  235. package/dist/surface/version.js +27 -0
  236. package/dist/surface/version.js.map +1 -0
  237. package/dist/tools/a11y.d.ts +8 -0
  238. package/dist/tools/a11y.js +545 -0
  239. package/dist/tools/a11y.js.map +1 -0
  240. package/dist/tools/a11y_depth.d.ts +19 -0
  241. package/dist/tools/a11y_depth.js +455 -0
  242. package/dist/tools/a11y_depth.js.map +1 -0
  243. package/dist/tools/agent.d.ts +15 -0
  244. package/dist/tools/agent.js +248 -0
  245. package/dist/tools/agent.js.map +1 -0
  246. package/dist/tools/batch.d.ts +46 -0
  247. package/dist/tools/batch.js +230 -0
  248. package/dist/tools/batch.js.map +1 -0
  249. package/dist/tools/cdp.d.ts +8 -0
  250. package/dist/tools/cdp.js +233 -0
  251. package/dist/tools/cdp.js.map +1 -0
  252. package/dist/tools/compact.d.ts +63 -0
  253. package/dist/tools/compact.js +418 -0
  254. package/dist/tools/compact.js.map +1 -0
  255. package/dist/tools/cost-class.d.ts +38 -0
  256. package/dist/tools/cost-class.js +117 -0
  257. package/dist/tools/cost-class.js.map +1 -0
  258. package/dist/tools/desktop.d.ts +9 -0
  259. package/dist/tools/desktop.js +346 -0
  260. package/dist/tools/desktop.js.map +1 -0
  261. package/dist/tools/electron_bridge.d.ts +41 -0
  262. package/dist/tools/electron_bridge.js +261 -0
  263. package/dist/tools/electron_bridge.js.map +1 -0
  264. package/dist/tools/extras.d.ts +22 -0
  265. package/dist/tools/extras.js +942 -0
  266. package/dist/tools/extras.js.map +1 -0
  267. package/dist/tools/favorites.d.ts +13 -0
  268. package/dist/tools/favorites.js +137 -0
  269. package/dist/tools/favorites.js.map +1 -0
  270. package/dist/tools/introspection.d.ts +13 -0
  271. package/dist/tools/introspection.js +55 -0
  272. package/dist/tools/introspection.js.map +1 -0
  273. package/dist/tools/ocr.d.ts +8 -0
  274. package/dist/tools/ocr.js +66 -0
  275. package/dist/tools/ocr.js.map +1 -0
  276. package/dist/tools/orchestration.d.ts +7 -0
  277. package/dist/tools/orchestration.js +377 -0
  278. package/dist/tools/orchestration.js.map +1 -0
  279. package/dist/tools/playbooks/extract-compose.d.ts +22 -0
  280. package/dist/tools/playbooks/extract-compose.js +85 -0
  281. package/dist/tools/playbooks/extract-compose.js.map +1 -0
  282. package/dist/tools/playbooks/find-replace.d.ts +11 -0
  283. package/dist/tools/playbooks/find-replace.js +56 -0
  284. package/dist/tools/playbooks/find-replace.js.map +1 -0
  285. package/dist/tools/playbooks/index.d.ts +63 -0
  286. package/dist/tools/playbooks/index.js +70 -0
  287. package/dist/tools/playbooks/index.js.map +1 -0
  288. package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
  289. package/dist/tools/playbooks/keys-blocklist.js +89 -0
  290. package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
  291. package/dist/tools/registry.d.ts +40 -0
  292. package/dist/tools/registry.js +560 -0
  293. package/dist/tools/registry.js.map +1 -0
  294. package/dist/tools/safety-gate.d.ts +16 -0
  295. package/dist/tools/safety-gate.js +70 -0
  296. package/dist/tools/safety-gate.js.map +1 -0
  297. package/dist/tools/scheduler.d.ts +76 -0
  298. package/dist/tools/scheduler.js +413 -0
  299. package/dist/tools/scheduler.js.map +1 -0
  300. package/dist/tools/shortcuts.d.ts +13 -0
  301. package/dist/tools/shortcuts.js +205 -0
  302. package/dist/tools/shortcuts.js.map +1 -0
  303. package/dist/tools/smart.d.ts +15 -0
  304. package/dist/tools/smart.js +785 -0
  305. package/dist/tools/smart.js.map +1 -0
  306. package/dist/tools/types.d.ts +174 -0
  307. package/dist/tools/types.js +67 -0
  308. package/dist/tools/types.js.map +1 -0
  309. package/dist/tools/window-text.d.ts +15 -0
  310. package/dist/tools/window-text.js +39 -0
  311. package/dist/tools/window-text.js.map +1 -0
  312. package/dist/types.d.ts +122 -0
  313. package/dist/types.js +41 -0
  314. package/dist/types.js.map +1 -0
  315. package/native/Package.swift +38 -0
  316. package/native/README.md +113 -0
  317. package/native/Sources/ClawdCursorHelper/main.swift +602 -0
  318. package/native/Sources/ClawdCursorHost/main.swift +182 -0
  319. package/native/Sources/PermissionCheck/main.swift +53 -0
  320. package/native/Sources/ScreenshotHelper/main.swift +219 -0
  321. package/native/build.sh +139 -0
  322. package/native/entitlements.plist +12 -0
  323. package/package.json +115 -0
  324. package/scripts/banner.ps1 +112 -0
  325. package/scripts/coord-accuracy.ps1 +140 -0
  326. package/scripts/coord-uwp.ps1 +80 -0
  327. package/scripts/edge-glow.ps1 +180 -0
  328. package/scripts/find-element.ps1 +198 -0
  329. package/scripts/get-foreground-window.ps1 +71 -0
  330. package/scripts/get-screen-context.ps1 +183 -0
  331. package/scripts/get-windows.ps1 +66 -0
  332. package/scripts/install-panic-hotkey.ps1 +46 -0
  333. package/scripts/interact-element.ps1 +431 -0
  334. package/scripts/invoke-element.ps1 +314 -0
  335. package/scripts/linux/atspi-bridge.py +356 -0
  336. package/scripts/linux/ocr-recognize.py +154 -0
  337. package/scripts/mac/_window-picker.jxa +163 -0
  338. package/scripts/mac/find-element.jxa +0 -0
  339. package/scripts/mac/find-element.sh +161 -0
  340. package/scripts/mac/focus-window.jxa +284 -0
  341. package/scripts/mac/get-focused-element.jxa +102 -0
  342. package/scripts/mac/get-foreground-window.jxa +173 -0
  343. package/scripts/mac/get-screen-context.jxa +197 -0
  344. package/scripts/mac/get-ui-tree.sh +141 -0
  345. package/scripts/mac/get-windows.jxa +117 -0
  346. package/scripts/mac/interact-element.sh +235 -0
  347. package/scripts/mac/invoke-element.jxa +408 -0
  348. package/scripts/mac/ocr-recognize.swift +124 -0
  349. package/scripts/ocr-recognize.ps1 +102 -0
  350. package/scripts/postinstall-native.js +48 -0
  351. package/scripts/ps-bridge.ps1 +830 -0
  352. package/scripts/smoke-mcp.ps1 +119 -0
  353. package/scripts/sync-version.ts +178 -0
  354. package/scripts/verify-install.js +81 -0
@@ -0,0 +1,1210 @@
1
+ "use strict";
2
+ /**
3
+ * Windows PlatformAdapter — all Windows-specific code lives here.
4
+ *
5
+ * Strategy:
6
+ * - Mouse + keyboard: nut-js directly (no TCC blocking as on macOS)
7
+ * - Screenshot: nut-js screen.grab() — no special helper binary
8
+ * - Screen size + DPI: System.Windows.Forms.Screen via PowerShell for logical px,
9
+ * compared with nut-js physical px to derive dpiRatio
10
+ * - Windows + A11y: persistent PSRunner (../../ps-runner.ts) driving UI Automation
11
+ * - Clipboard: Get-Clipboard / Set-Clipboard via PowerShell
12
+ * - App launch: Start-Process via PowerShell
13
+ *
14
+ * Permissions: Windows has no TCC-style gate — returns all-true.
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.WindowsAdapter = void 0;
21
+ const child_process_1 = require("child_process");
22
+ const util_1 = require("util");
23
+ const sharp_1 = __importDefault(require("sharp"));
24
+ const nut_js_1 = require("@nut-tree-fork/nut-js");
25
+ const ps_runner_1 = require("./ps-runner");
26
+ const launch_poll_1 = require("./launch-poll");
27
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
28
+ // Tunables
29
+ const PS_TIMEOUT_MS = 8_000;
30
+ const CLIPBOARD_TIMEOUT_MS = 3_000;
31
+ class WindowsAdapter {
32
+ platform = 'win32';
33
+ screenSize = null;
34
+ async init() {
35
+ // Configure nut-js for snappy input; same tuning as native-desktop.ts.
36
+ nut_js_1.mouse.config.mouseSpeed = 2000;
37
+ nut_js_1.mouse.config.autoDelayMs = 0;
38
+ nut_js_1.keyboard.config.autoDelayMs = 0;
39
+ // Kick off the PowerShell bridge so the ~800ms UIA assembly load happens
40
+ // in the background. Errors surface on first real a11y call.
41
+ ps_runner_1.psRunner.start().catch(() => { });
42
+ // Pre-warm screen size so the first capture / first click isn't paying for it.
43
+ await this.getScreenSize().catch(() => null);
44
+ }
45
+ async shutdown() {
46
+ try {
47
+ ps_runner_1.psRunner.stop();
48
+ }
49
+ catch { /* */ }
50
+ }
51
+ // ─── PERMISSIONS ──────────────────────────────────────────────────
52
+ async checkPermissions() {
53
+ // Windows doesn't gate any of these behind TCC-style prompts. If the
54
+ // user can run the binary at all, they can do input / capture / a11y.
55
+ return { input: true, accessibility: true, screenRecording: true };
56
+ }
57
+ async requestPermissions() {
58
+ return this.checkPermissions();
59
+ }
60
+ // ─── DISPLAY ──────────────────────────────────────────────────────
61
+ async getScreenSize() {
62
+ if (this.screenSize)
63
+ return this.screenSize;
64
+ // nut-js screen.grab() returns PHYSICAL pixels on Windows.
65
+ let physicalWidth = 0, physicalHeight = 0;
66
+ try {
67
+ const img = await nut_js_1.screen.grab();
68
+ physicalWidth = img.width;
69
+ physicalHeight = img.height;
70
+ img.data = null;
71
+ }
72
+ catch { /* fall through with zeros */ }
73
+ // System.Windows.Forms.Screen returns LOGICAL (DPI-scaled) pixels on Win —
74
+ // that's the coordinate space nut-js mouse API expects.
75
+ let logicalWidth = physicalWidth;
76
+ let logicalHeight = physicalHeight;
77
+ try {
78
+ const { stdout } = await execFileAsync('powershell.exe', [
79
+ '-NoProfile',
80
+ '-Command',
81
+ 'Add-Type -AssemblyName System.Windows.Forms; ' +
82
+ '$s=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; ' +
83
+ '"$($s.Width),$($s.Height)"',
84
+ ], { timeout: PS_TIMEOUT_MS });
85
+ const [w, h] = stdout.trim().split(',').map(s => parseInt(s, 10));
86
+ if (w > 0 && h > 0) {
87
+ logicalWidth = w;
88
+ logicalHeight = h;
89
+ }
90
+ }
91
+ catch { /* non-fatal — fall back to physical */ }
92
+ if (!physicalWidth)
93
+ physicalWidth = logicalWidth;
94
+ if (!physicalHeight)
95
+ physicalHeight = logicalHeight;
96
+ const dpiRatio = physicalWidth > logicalWidth ? physicalWidth / logicalWidth : 1;
97
+ this.screenSize = {
98
+ physicalWidth,
99
+ physicalHeight,
100
+ logicalWidth,
101
+ logicalHeight,
102
+ dpiRatio,
103
+ };
104
+ return this.screenSize;
105
+ }
106
+ async listDisplays() {
107
+ // System.Windows.Forms.Screen.AllScreens enumerates every connected
108
+ // display with bounds + primary flag. We call it via the PS UIA path
109
+ // we already have warmed up.
110
+ try {
111
+ const { stdout } = await execFileAsync('powershell.exe', [
112
+ '-NoProfile',
113
+ '-Command',
114
+ 'Add-Type -AssemblyName System.Windows.Forms; ' +
115
+ '[System.Windows.Forms.Screen]::AllScreens | ForEach-Object { ' +
116
+ ' $b = $_.Bounds; ' +
117
+ ' [pscustomobject]@{ ' +
118
+ ' name = $_.DeviceName; ' +
119
+ ' primary = $_.Primary; ' +
120
+ ' x = $b.X; y = $b.Y; w = $b.Width; h = $b.Height ' +
121
+ ' } ' +
122
+ '} | ConvertTo-Json -Compress',
123
+ ], { timeout: PS_TIMEOUT_MS });
124
+ const raw = JSON.parse(stdout.trim() || '[]');
125
+ const arr = Array.isArray(raw) ? raw : [raw];
126
+ // Physical pixel dimensions: we can only confidently compute these for
127
+ // the primary display (via our cached ScreenSize). For secondaries we
128
+ // assume the same dpiRatio — accurate on homogeneous setups, a safe
129
+ // approximation on mixed-DPI (caller can override per-monitor later).
130
+ const size = await this.getScreenSize();
131
+ return arr.map((s, i) => {
132
+ const w = Number(s.w) || 0;
133
+ const h = Number(s.h) || 0;
134
+ return {
135
+ index: i,
136
+ label: String(s.name || `Display ${i + 1}`),
137
+ primary: !!s.primary,
138
+ bounds: { x: Number(s.x) || 0, y: Number(s.y) || 0, width: w, height: h },
139
+ physicalSize: {
140
+ width: Math.round(w * size.dpiRatio),
141
+ height: Math.round(h * size.dpiRatio),
142
+ },
143
+ dpiRatio: size.dpiRatio,
144
+ };
145
+ });
146
+ }
147
+ catch {
148
+ // Fallback to single display so callers don't have to special-case.
149
+ const size = await this.getScreenSize();
150
+ return [{
151
+ index: 0,
152
+ label: 'Display 1',
153
+ primary: true,
154
+ bounds: { x: 0, y: 0, width: size.logicalWidth, height: size.logicalHeight },
155
+ physicalSize: { width: size.physicalWidth, height: size.physicalHeight },
156
+ dpiRatio: size.dpiRatio,
157
+ }];
158
+ }
159
+ }
160
+ async screenshot(opts) {
161
+ // displayIndex is plumbed through but nut-js's screen.grab() always
162
+ // captures ALL displays combined. For index selection on Windows,
163
+ // we crop to the target display's bounds after the grab.
164
+ const img = await nut_js_1.screen.grab();
165
+ let srcWidth = img.width;
166
+ let srcHeight = img.height;
167
+ let rgba = img.data;
168
+ let pipeline;
169
+ if (opts?.displayIndex !== undefined && opts.displayIndex > 0) {
170
+ const displays = await this.listDisplays();
171
+ const target = displays[opts.displayIndex];
172
+ if (target) {
173
+ // Translate logical bounds into the physical image (nut-js returns
174
+ // hardware pixels; multiply by dpiRatio).
175
+ const r = target.dpiRatio || 1;
176
+ const left = Math.max(0, Math.round(target.bounds.x * r));
177
+ const top = Math.max(0, Math.round(target.bounds.y * r));
178
+ const width = Math.max(1, Math.min(Math.round(target.bounds.width * r), img.width - left));
179
+ const height = Math.max(1, Math.min(Math.round(target.bounds.height * r), img.height - top));
180
+ pipeline = (0, sharp_1.default)(rgba, { raw: { width: img.width, height: img.height, channels: 4 } })
181
+ .extract({ left, top, width, height });
182
+ srcWidth = width;
183
+ srcHeight = height;
184
+ }
185
+ else {
186
+ pipeline = (0, sharp_1.default)(rgba, { raw: { width: srcWidth, height: srcHeight, channels: 4 } });
187
+ }
188
+ }
189
+ else {
190
+ pipeline = (0, sharp_1.default)(rgba, { raw: { width: srcWidth, height: srcHeight, channels: 4 } });
191
+ }
192
+ let width = srcWidth;
193
+ let height = srcHeight;
194
+ let scaleFactor = 1;
195
+ if (opts?.maxWidth && srcWidth > opts.maxWidth) {
196
+ scaleFactor = srcWidth / opts.maxWidth;
197
+ const newH = Math.round(srcHeight / scaleFactor);
198
+ pipeline = pipeline.resize(opts.maxWidth, newH, { fit: 'fill', kernel: 'lanczos3' });
199
+ width = opts.maxWidth;
200
+ height = newH;
201
+ }
202
+ const buffer = await pipeline.png().toBuffer();
203
+ img.data = null;
204
+ return { buffer, width, height, scaleFactor };
205
+ }
206
+ async screenshotRegion(x, y, w, h) {
207
+ const img = await nut_js_1.screen.grab();
208
+ const rx = Math.max(0, Math.min(x, img.width - 1));
209
+ const ry = Math.max(0, Math.min(y, img.height - 1));
210
+ const rw = Math.min(w, img.width - rx);
211
+ const rh = Math.min(h, img.height - ry);
212
+ const buffer = await (0, sharp_1.default)(img.data, {
213
+ raw: { width: img.width, height: img.height, channels: 4 },
214
+ })
215
+ .extract({ left: rx, top: ry, width: rw, height: rh })
216
+ .png()
217
+ .toBuffer();
218
+ img.data = null;
219
+ return { buffer, width: rw, height: rh, scaleFactor: 1 };
220
+ }
221
+ // ─── WINDOWS ──────────────────────────────────────────────────────
222
+ async listWindows() {
223
+ try {
224
+ const result = await ps_runner_1.psRunner.run({ cmd: 'get-screen-context', maxDepth: 0 });
225
+ const raw = Array.isArray(result?.windows) ? result.windows : [];
226
+ return raw.map(this.normalizeWindow);
227
+ }
228
+ catch {
229
+ return [];
230
+ }
231
+ }
232
+ async getActiveWindow() {
233
+ try {
234
+ const fg = await ps_runner_1.psRunner.run({ cmd: 'get-foreground-window' });
235
+ if (!fg || fg.success === false)
236
+ return null;
237
+ // Try to find the same window in the full list so we get bounds/minimized.
238
+ const all = await this.listWindows();
239
+ const match = all.find(w => w.processId === fg.processId);
240
+ if (match)
241
+ return match;
242
+ return this.normalizeWindow({
243
+ title: fg.title ?? '',
244
+ processName: fg.processName ?? '',
245
+ processId: fg.processId ?? 0,
246
+ handle: fg.handle,
247
+ bounds: { x: 0, y: 0, width: 0, height: 0 },
248
+ isMinimized: false,
249
+ });
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ }
255
+ async focusWindow(query) {
256
+ // The PSRunner focus-window command takes title and/or processId. Look up by
257
+ // processName first so callers can pass just that.
258
+ let processId = query.processId;
259
+ let title = query.title;
260
+ if (processId === undefined && query.processName) {
261
+ const target = query.processName.toLowerCase();
262
+ const windows = await this.listWindows();
263
+ const hit = windows.find(w => w.processName.toLowerCase() === target)
264
+ ?? windows.find(w => w.processName.toLowerCase().includes(target));
265
+ if (hit)
266
+ processId = hit.processId;
267
+ }
268
+ try {
269
+ const result = await ps_runner_1.psRunner.run({
270
+ cmd: 'focus-window',
271
+ restore: true,
272
+ ...(title !== undefined ? { title } : {}),
273
+ ...(processId !== undefined ? { processId } : {}),
274
+ });
275
+ // The PS script reports `success` (target window was found and SetFocus
276
+ // was attempted) and `foreground` (Win32 SetForegroundWindow actually
277
+ // promoted the window). We need foreground=true for subsequent keystroke
278
+ // tools to land on the right app, so treat foreground=false as a focus
279
+ // failure even if SetFocus succeeded. This is the difference between
280
+ // "a11y-focused" and "will receive global SendInput keystrokes".
281
+ if (result?.success !== true)
282
+ return false;
283
+ if (result?.foreground === false)
284
+ return false;
285
+ return true;
286
+ }
287
+ catch {
288
+ return false;
289
+ }
290
+ }
291
+ async maximizeWindow() {
292
+ // Win+Up is the portable Windows maximize shortcut.
293
+ await this.keyPress('super+up').catch(() => { });
294
+ }
295
+ async setWindowState(state, query) {
296
+ // Resolve the target: either the caller-supplied window or the
297
+ // foreground one. We drive the transition through a single PowerShell
298
+ // call that wraps Win32 ShowWindow / PostMessage so we don't depend
299
+ // on focus timing of a key-press chord.
300
+ let pid;
301
+ let hwnd;
302
+ if (query) {
303
+ // Prefer pid resolution when we can — cheaper than listWindows.
304
+ pid = query.processId;
305
+ if (pid === undefined) {
306
+ const match = await this.resolveWindow(query);
307
+ if (match) {
308
+ pid = match.processId;
309
+ const handle = match.handle;
310
+ if (typeof handle === 'number')
311
+ hwnd = handle;
312
+ }
313
+ }
314
+ }
315
+ const showCmd = state === 'maximize' ? 3 // SW_MAXIMIZE
316
+ : state === 'minimize' ? 6 // SW_MINIMIZE
317
+ : state === 'normal' ? 9 // SW_RESTORE
318
+ : null;
319
+ const target = hwnd !== undefined
320
+ ? `[IntPtr]${hwnd}`
321
+ : pid !== undefined
322
+ ? `(Get-Process -Id ${pid}).MainWindowHandle`
323
+ : '[NativeMethods]::GetForegroundWindow()';
324
+ try {
325
+ if (state === 'close') {
326
+ // WM_CLOSE — polite close request. App may prompt, we return true
327
+ // when the message was posted, not when the window actually closed.
328
+ const ps =
329
+ // Single-quoted -MemberDefinition (not a here-string) — a here-string header
330
+ // is illegal in a single-line `-Command` and fails to parse (see #153).
331
+ "Add-Type -Name NativeMethods -Namespace Win32 -MemberDefinition '" +
332
+ '[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();' +
333
+ '[DllImport("user32.dll")] public static extern bool PostMessage(System.IntPtr hWnd, uint Msg, System.IntPtr wParam, System.IntPtr lParam);' +
334
+ "' -PassThru | Out-Null;" +
335
+ `$h = ${target};` +
336
+ 'if ($h -ne [System.IntPtr]::Zero) { [Win32.NativeMethods]::PostMessage($h, 0x0010, [System.IntPtr]::Zero, [System.IntPtr]::Zero) | Out-Null; "ok" } else { "no-window" }';
337
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', ps], { timeout: PS_TIMEOUT_MS });
338
+ return stdout.trim() === 'ok';
339
+ }
340
+ if (showCmd !== null) {
341
+ // UWP windows hosted by ApplicationFrameHost ignore a cross-process
342
+ // Win32 ShowWindow(SW_MINIMIZE) — it silently no-ops (#153: minimize
343
+ // failed for Calculator/Settings while maximize/restore worked). Drive
344
+ // the transition through the UIA WindowPattern (the supported
345
+ // cross-process way, and what we already use for restore), which works
346
+ // for UWP *and* Win32. Fall back to ShowWindowAsync (plus SW_FORCEMINIMIZE
347
+ // for the minimize case) only if the pattern isn't available.
348
+ const visualState = state === 'maximize' ? 'Maximized'
349
+ : state === 'minimize' ? 'Minimized'
350
+ : 'Normal';
351
+ const titleQ = this.psQuote(query?.title ?? '');
352
+ const forceMin = state === 'minimize'
353
+ ? ' [Win32.NativeMethods]::ShowWindowAsync($nwh, 11) | Out-Null;'
354
+ : '';
355
+ const ps =
356
+ // NB: a here-string header (@"...) is illegal in a single-line `-Command`
357
+ // ("No characters are allowed after a here-string header before the end of
358
+ // the line") — it fails to PARSE, so the whole script silently produced no
359
+ // output and minimize returned false (#153). Use a PS single-quoted
360
+ // -MemberDefinition instead: the C# double-quotes are literal inside it, and
361
+ // Node handles the wire-escaping of those quotes for us.
362
+ 'Add-Type -AssemblyName UIAutomationClient,UIAutomationTypes | Out-Null;' +
363
+ "Add-Type -Name NativeMethods -Namespace Win32 -MemberDefinition '" +
364
+ '[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();' +
365
+ '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(System.IntPtr hWnd, int nCmdShow);' +
366
+ "' -PassThru | Out-Null;" +
367
+ `$title = ${titleQ};` +
368
+ `$h = ${target};` +
369
+ '$el = $null;' +
370
+ // Strategy A — find the top-level window by title via UIA. This is the
371
+ // ONLY reliable handle for UWP / ApplicationFrameHost apps, whose
372
+ // visible window is owned by ApplicationFrameHost (so pid→MainWindowHandle
373
+ // is 0/wrong) and whose cross-process ShowWindow(SW_MINIMIZE) no-ops (#153).
374
+ 'if ($title -ne "") {' +
375
+ ' $root = [System.Windows.Automation.AutomationElement]::RootElement;' +
376
+ ' $cond = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, [System.Windows.Automation.ControlType]::Window);' +
377
+ ' foreach ($w in $root.FindAll([System.Windows.Automation.TreeScope]::Children, $cond)) {' +
378
+ ' $n = $w.Current.Name; if ($n -and $n.ToLower().Contains($title.ToLower())) { $el = $w; break }' +
379
+ ' }' +
380
+ '}' +
381
+ // Strategy B — the caller-resolved handle. Strategy C — foreground.
382
+ 'if ($el -eq $null -and $h -ne [System.IntPtr]::Zero) { try { $el = [System.Windows.Automation.AutomationElement]::FromHandle($h) } catch {} }' +
383
+ 'if ($el -eq $null) { $fg = [Win32.NativeMethods]::GetForegroundWindow(); if ($fg -ne [System.IntPtr]::Zero) { try { $el = [System.Windows.Automation.AutomationElement]::FromHandle($fg) } catch {} } }' +
384
+ 'if ($el -eq $null) { "no-window" } else {' +
385
+ ' $ok = $false;' +
386
+ ` try { $wp = $el.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern); $wp.SetWindowVisualState([System.Windows.Automation.WindowVisualState]::${visualState}); $ok = $true } catch { $ok = $false }` +
387
+ ' if (-not $ok) { $nwh = [System.IntPtr]$el.Current.NativeWindowHandle; if ($nwh -ne [System.IntPtr]::Zero) {' +
388
+ ` [Win32.NativeMethods]::ShowWindowAsync($nwh, ${showCmd}) | Out-Null;${forceMin}` +
389
+ ' $ok = $true } }' +
390
+ ' if ($ok) { "ok" } else { "no-window" } }';
391
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', ps], { timeout: PS_TIMEOUT_MS });
392
+ return stdout.trim() === 'ok';
393
+ }
394
+ return false;
395
+ }
396
+ catch {
397
+ return false;
398
+ }
399
+ }
400
+ async setWindowBounds(bounds, query) {
401
+ // SetWindowPos takes hwnd + x/y/w/h. Use SWP_NOZORDER to keep z-order.
402
+ let hwnd;
403
+ if (query) {
404
+ const match = await this.resolveWindow(query);
405
+ if (match && typeof match.handle === 'number')
406
+ hwnd = match.handle;
407
+ }
408
+ const handleExpr = hwnd !== undefined
409
+ ? `[IntPtr]${hwnd}`
410
+ : '[Win32.NativeMethods]::GetForegroundWindow()';
411
+ try {
412
+ const x = bounds.x ?? -1;
413
+ const y = bounds.y ?? -1;
414
+ const w = bounds.width ?? -1;
415
+ const h = bounds.height ?? -1;
416
+ // When a dim is -1, we read the current rect and preserve it.
417
+ const ps =
418
+ // Single-quoted -MemberDefinition (not a here-string) — a here-string header
419
+ // is illegal in a single-line `-Command` and fails to parse (see #153).
420
+ "Add-Type -Name NativeMethods -Namespace Win32 -MemberDefinition '" +
421
+ '[DllImport("user32.dll")] public static extern System.IntPtr GetForegroundWindow();' +
422
+ '[DllImport("user32.dll")] public static extern bool GetWindowRect(System.IntPtr hWnd, out System.Drawing.Rectangle rect);' +
423
+ '[DllImport("user32.dll")] public static extern bool SetWindowPos(System.IntPtr hWnd, System.IntPtr hWndAfter, int X, int Y, int cx, int cy, uint uFlags);' +
424
+ "' -ReferencedAssemblies System.Drawing -PassThru | Out-Null;" +
425
+ `$h = ${handleExpr};` +
426
+ 'if ($h -eq [System.IntPtr]::Zero) { "no-window"; exit }' +
427
+ '$r = New-Object System.Drawing.Rectangle;' +
428
+ '[Win32.NativeMethods]::GetWindowRect($h, [ref] $r) | Out-Null;' +
429
+ `$nx = ${x}; $ny = ${y}; $nw = ${w}; $nh = ${h};` +
430
+ 'if ($nx -lt 0) { $nx = $r.X }' +
431
+ 'if ($ny -lt 0) { $ny = $r.Y }' +
432
+ 'if ($nw -lt 0) { $nw = $r.Width - $r.X }' +
433
+ 'if ($nh -lt 0) { $nh = $r.Height - $r.Y }' +
434
+ '[Win32.NativeMethods]::SetWindowPos($h, [System.IntPtr]::Zero, $nx, $ny, $nw, $nh, 0x0004) | Out-Null;' +
435
+ '"ok"';
436
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', ps], { timeout: PS_TIMEOUT_MS });
437
+ return stdout.trim() === 'ok';
438
+ }
439
+ catch {
440
+ return false;
441
+ }
442
+ }
443
+ /**
444
+ * Internal helper — resolve a focusWindow-style query to a single
445
+ * WindowInfo. Same precedence the public `focusWindow` uses.
446
+ */
447
+ async resolveWindow(query) {
448
+ const windows = await this.listWindows();
449
+ return windows.find(w => {
450
+ if (query.processId !== undefined && w.processId === query.processId)
451
+ return true;
452
+ if (query.processName && w.processName.toLowerCase() === query.processName.toLowerCase())
453
+ return true;
454
+ if (query.title && w.title.toLowerCase().includes(query.title.toLowerCase()))
455
+ return true;
456
+ return false;
457
+ }) ?? null;
458
+ }
459
+ // ─── ACCESSIBILITY ────────────────────────────────────────────────
460
+ async getUiTree(processId) {
461
+ // Default to the foreground window's pid when the caller omits it — exactly
462
+ // as findElements does below. Without this, get-screen-context is called
463
+ // with focusedPid=0 and the bridge returns NO tree (Cmd-GetScreenContext
464
+ // only walks a window when focusedPid>0), so read_screen over MCP came back
465
+ // "(empty a11y tree)" for EVERY app — a regression once the pid-resolving
466
+ // System-A read_screen was projected away in favor of this path.
467
+ let pid = processId;
468
+ if (pid === undefined) {
469
+ const fg = await this.getActiveWindow().catch(() => null);
470
+ if (fg?.processId)
471
+ pid = fg.processId;
472
+ }
473
+ try {
474
+ const result = await ps_runner_1.psRunner.run({
475
+ cmd: 'get-screen-context',
476
+ maxDepth: 8,
477
+ ...(pid !== undefined ? { focusedProcessId: pid } : {}),
478
+ });
479
+ const tree = result?.uiTree;
480
+ if (!tree)
481
+ return [];
482
+ const nodes = Array.isArray(tree) ? tree : [tree];
483
+ const flat = [];
484
+ for (const n of nodes)
485
+ this.flattenTree(n, flat);
486
+ return flat;
487
+ }
488
+ catch {
489
+ return [];
490
+ }
491
+ }
492
+ async findElements(query) {
493
+ // Default to the foreground window's pid when caller omits processId.
494
+ // Without this, the PSBridge searches from the desktop root across ALL
495
+ // windows and hits its 20-element cap before finding deep targets. The
496
+ // foreground window is almost always the right scope for an unscoped
497
+ // "find me X" query coming from the agent.
498
+ let processId = query.processId;
499
+ if (processId === undefined) {
500
+ const fg = await this.getActiveWindow();
501
+ if (fg?.processId)
502
+ processId = fg.processId;
503
+ }
504
+ try {
505
+ const result = await ps_runner_1.psRunner.run({
506
+ cmd: 'find-element',
507
+ ...(query.name !== undefined ? { name: query.name } : {}),
508
+ ...(query.controlType !== undefined ? { controlType: query.controlType } : {}),
509
+ ...(processId !== undefined ? { processId } : {}),
510
+ });
511
+ const raw = Array.isArray(result) ? result : [];
512
+ return raw.map(this.normalizeElement);
513
+ }
514
+ catch {
515
+ return [];
516
+ }
517
+ }
518
+ async getFocusedElement() {
519
+ try {
520
+ const result = await ps_runner_1.psRunner.run({ cmd: 'get-focused-element' });
521
+ if (!result || result.success === false)
522
+ return null;
523
+ return this.normalizeElement(result);
524
+ }
525
+ catch {
526
+ return null;
527
+ }
528
+ }
529
+ async invokeElement(query) {
530
+ // The underlying PS bridge requires a processId for invoke-element.
531
+ // Resolution order when caller omits processId:
532
+ // 1. Foreground window (the agent's usual implicit scope).
533
+ // 2. Fall back to find-element scan if the foreground window has no match.
534
+ // Without this, find-element ran from the desktop root and could miss
535
+ // deeply-nested targets due to the PSBridge 20-result cap.
536
+ let processId = query.processId;
537
+ if (processId === undefined && query.name) {
538
+ const fg = await this.getActiveWindow();
539
+ if (fg?.processId) {
540
+ processId = fg.processId;
541
+ }
542
+ else {
543
+ const candidates = await this.findElements({
544
+ name: query.name,
545
+ controlType: query.controlType,
546
+ });
547
+ if (candidates.length === 0)
548
+ return { success: false };
549
+ processId = candidates[0].processId
550
+ ?? candidates[0].pid;
551
+ // If still no pid but we have bounds, caller can fall back to a coord click.
552
+ if (processId === undefined) {
553
+ return {
554
+ success: false,
555
+ bounds: candidates[0].bounds,
556
+ };
557
+ }
558
+ }
559
+ }
560
+ if (processId === undefined)
561
+ return { success: false };
562
+ try {
563
+ const result = await ps_runner_1.psRunner.run({
564
+ cmd: 'invoke-element',
565
+ processId,
566
+ action: query.action ?? 'click',
567
+ ...(query.name !== undefined ? { name: query.name } : {}),
568
+ ...(query.controlType !== undefined ? { controlType: query.controlType } : {}),
569
+ ...(query.value !== undefined ? { value: query.value } : {}),
570
+ });
571
+ return {
572
+ success: result?.success === true,
573
+ bounds: result?.bounds,
574
+ // The bridge returns get-value's payload at the TOP level
575
+ // ({success, action, value, method}), not nested under .data — but
576
+ // every consumer reads res.data?.value. Surface it so a11y_get_value /
577
+ // element_value_contains actually see the value (review 2026-06-11).
578
+ data: result?.data ?? (result?.value !== undefined ? { value: result.value } : undefined),
579
+ };
580
+ }
581
+ catch {
582
+ return { success: false };
583
+ }
584
+ }
585
+ async waitForElement(query, timeoutMs) {
586
+ const interval = query.intervalMs ?? 250;
587
+ const deadline = Date.now() + timeoutMs;
588
+ while (Date.now() < deadline) {
589
+ const hits = await this.findElements({
590
+ name: query.name,
591
+ controlType: query.controlType,
592
+ processId: query.processId,
593
+ });
594
+ if (hits.length > 0)
595
+ return hits[0];
596
+ await this.delay(interval);
597
+ }
598
+ return null;
599
+ }
600
+ // ─── INPUT (mouse) ────────────────────────────────────────────────
601
+ // All coords are LOGICAL pixels — nut-js mouse API lives in that space on Win.
602
+ /** Cursor cache for mouseMoveRelative — last known target. */
603
+ lastCursor = null;
604
+ /**
605
+ * Ensure the window at (x, y) is the foreground window before clicking.
606
+ *
607
+ * Problem: On Windows, nut-js sends mouse input via SendInput which
608
+ * delivers to whatever window is topmost at those coordinates — not
609
+ * necessarily the foreground window. When a Save As dialog sits over a
610
+ * File Explorer window (or any background window), a click intended for
611
+ * the dialog's filename field can land on the Explorer window if the
612
+ * dialog's owning process lost foreground between the screenshot and the
613
+ * click (race) or if the click coords are slightly outside the dialog rect
614
+ * due to DPI-related rounding.
615
+ *
616
+ * Fix: use Win32 WindowFromPoint (via the warm psRunner bridge) to
617
+ * identify the window at the target coords. If it is not the current
618
+ * foreground window, call SetForegroundWindow to bring it forward before
619
+ * the click lands. Non-fatal — if the PS call fails we proceed anyway.
620
+ */
621
+ async ensureForegroundAtPoint(x, y) {
622
+ try {
623
+ const r = await ps_runner_1.psRunner.run({ cmd: 'activate-at-point', x, y });
624
+ // 'noop' reasons mean nothing to promote (no window, or already foreground)
625
+ // — both are fine, treat as activated.
626
+ const activated = r?.activated !== false;
627
+ return { activated, title: r?.title, processName: r?.processName, reason: r?.reason };
628
+ }
629
+ catch {
630
+ // Non-fatal — click proceeds regardless. We do not want to block
631
+ // mouse input if the foreground check fails. Undefined = unknown.
632
+ return undefined;
633
+ }
634
+ }
635
+ toNutButton(button) {
636
+ if (button === 'right')
637
+ return nut_js_1.Button.RIGHT;
638
+ if (button === 'middle')
639
+ return nut_js_1.Button.MIDDLE;
640
+ return nut_js_1.Button.LEFT;
641
+ }
642
+ async mouseClick(x, y, opts) {
643
+ // Bring the window at (x, y) to the foreground before sending any
644
+ // button events. Without this, a click intended for a Save As dialog
645
+ // can land on a background Explorer window when the dialog lost focus
646
+ // between the screenshot and the click (z-order / activation race).
647
+ // The activation verdict flows back to the caller so a FAILED raise
648
+ // (foreground-lock) is visible instead of a silent wrong-window click.
649
+ const activation = await this.ensureForegroundAtPoint(x, y);
650
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
651
+ this.lastCursor = { x, y };
652
+ await this.delay(40);
653
+ const count = opts?.count ?? 1;
654
+ const btn = this.toNutButton(opts?.button);
655
+ for (let i = 0; i < count; i++) {
656
+ if (btn === nut_js_1.Button.RIGHT)
657
+ await nut_js_1.mouse.rightClick();
658
+ else if (btn === nut_js_1.Button.MIDDLE) {
659
+ // nut-js has no direct middleClick helper; press+release.
660
+ await nut_js_1.mouse.pressButton(nut_js_1.Button.MIDDLE);
661
+ await this.delay(30);
662
+ await nut_js_1.mouse.releaseButton(nut_js_1.Button.MIDDLE);
663
+ }
664
+ else {
665
+ await nut_js_1.mouse.click(nut_js_1.Button.LEFT);
666
+ }
667
+ if (i < count - 1)
668
+ await this.delay(60);
669
+ }
670
+ return activation;
671
+ }
672
+ async mouseMove(x, y) {
673
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
674
+ this.lastCursor = { x, y };
675
+ }
676
+ async mouseMoveRelative(dx, dy) {
677
+ // nut-js `getPosition()` works reliably on Windows — prefer that over
678
+ // the cache. Fall back to the cache if the query fails.
679
+ try {
680
+ const pos = await nut_js_1.mouse.getPosition();
681
+ const nx = Math.round(pos.x + dx);
682
+ const ny = Math.round(pos.y + dy);
683
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
684
+ this.lastCursor = { x: nx, y: ny };
685
+ }
686
+ catch {
687
+ if (this.lastCursor) {
688
+ const nx = this.lastCursor.x + dx;
689
+ const ny = this.lastCursor.y + dy;
690
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
691
+ this.lastCursor = { x: nx, y: ny };
692
+ }
693
+ }
694
+ }
695
+ async mouseDrag(x1, y1, x2, y2) {
696
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x1, y1));
697
+ this.lastCursor = { x: x1, y: y1 };
698
+ await this.delay(50);
699
+ await nut_js_1.mouse.pressButton(nut_js_1.Button.LEFT);
700
+ await this.delay(80);
701
+ const steps = Math.max(8, Math.floor(Math.hypot(x2 - x1, y2 - y1) / 18));
702
+ for (let i = 1; i <= steps; i++) {
703
+ const t = i / steps;
704
+ const nx = Math.round(x1 + (x2 - x1) * t);
705
+ const ny = Math.round(y1 + (y2 - y1) * t);
706
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
707
+ this.lastCursor = { x: nx, y: ny };
708
+ await this.delay(10);
709
+ }
710
+ await nut_js_1.mouse.releaseButton(nut_js_1.Button.LEFT);
711
+ }
712
+ async mouseScroll(x, y, direction, amount = 3) {
713
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
714
+ this.lastCursor = { x, y };
715
+ await this.delay(30);
716
+ // nut-js only exposes scrollUp/scrollDown natively. For horizontal,
717
+ // fall back to Shift+scroll which most apps interpret as horizontal.
718
+ if (direction === 'down')
719
+ await nut_js_1.mouse.scrollDown(amount);
720
+ else if (direction === 'up')
721
+ await nut_js_1.mouse.scrollUp(amount);
722
+ else {
723
+ // Horizontal: hold Shift, scroll vertically.
724
+ await nut_js_1.keyboard.pressKey(nut_js_1.Key.LeftShift);
725
+ try {
726
+ if (direction === 'left')
727
+ await nut_js_1.mouse.scrollUp(amount);
728
+ else
729
+ await nut_js_1.mouse.scrollDown(amount);
730
+ }
731
+ finally {
732
+ await nut_js_1.keyboard.releaseKey(nut_js_1.Key.LeftShift);
733
+ }
734
+ }
735
+ }
736
+ async mouseDown(button) {
737
+ await nut_js_1.mouse.pressButton(this.toNutButton(button));
738
+ }
739
+ async mouseUp(button) {
740
+ await nut_js_1.mouse.releaseButton(this.toNutButton(button));
741
+ }
742
+ // ─── INPUT (keyboard) ─────────────────────────────────────────────
743
+ async typeText(text) {
744
+ if (!text)
745
+ return;
746
+ await nut_js_1.keyboard.type(text);
747
+ }
748
+ async keyPress(combo) {
749
+ if (!combo)
750
+ return;
751
+ // Literal "+" — can't split on "+" since it IS the separator.
752
+ if (combo === '+') {
753
+ await nut_js_1.keyboard.type('+');
754
+ return;
755
+ }
756
+ const parts = combo.split('+').map(s => s.trim()).filter(Boolean);
757
+ if (parts.length === 0)
758
+ return;
759
+ // Convert "mod" → "ctrl" on Windows, leave the rest of the combo alone.
760
+ const normalized = parts.map(p => {
761
+ const l = p.toLowerCase();
762
+ if (l === 'mod' || l === 'cmd' || l === 'command' || l === 'meta')
763
+ return 'ctrl';
764
+ return p;
765
+ });
766
+ // Map every part to a nut-js Key enum value, or 'TYPE_CHAR' for printable chars
767
+ // like '*', '+', '.' that have no direct enum entry.
768
+ const mapped = normalized.map(p => this.mapKey(p));
769
+ // Single-key: either type it as a character or press+release the mapped key.
770
+ if (mapped.length === 1) {
771
+ if (mapped[0] === 'TYPE_CHAR') {
772
+ await nut_js_1.keyboard.type(normalized[0]);
773
+ }
774
+ else {
775
+ await nut_js_1.keyboard.pressKey(mapped[0]);
776
+ await this.delay(30);
777
+ await nut_js_1.keyboard.releaseKey(mapped[0]);
778
+ }
779
+ return;
780
+ }
781
+ // Combo: press each modifier (or type the printable char), then release in reverse.
782
+ for (let i = 0; i < mapped.length; i++) {
783
+ const k = mapped[i];
784
+ if (k === 'TYPE_CHAR') {
785
+ await nut_js_1.keyboard.type(normalized[i]);
786
+ }
787
+ else {
788
+ await nut_js_1.keyboard.pressKey(k);
789
+ }
790
+ await this.delay(30);
791
+ }
792
+ for (let i = mapped.length - 1; i >= 0; i--) {
793
+ const k = mapped[i];
794
+ if (k !== 'TYPE_CHAR') {
795
+ await nut_js_1.keyboard.releaseKey(k);
796
+ }
797
+ await this.delay(30);
798
+ }
799
+ }
800
+ async keyDown(key) {
801
+ const mapped = this.mapKey(key);
802
+ if (mapped === 'TYPE_CHAR') {
803
+ // Single printable char without modifier semantics — treat as type.
804
+ await nut_js_1.keyboard.type(key);
805
+ return;
806
+ }
807
+ await nut_js_1.keyboard.pressKey(mapped);
808
+ }
809
+ async keyUp(key) {
810
+ const mapped = this.mapKey(key);
811
+ if (mapped === 'TYPE_CHAR')
812
+ return; // no-op — typing isn't held
813
+ await nut_js_1.keyboard.releaseKey(mapped);
814
+ }
815
+ // ─── CLIPBOARD ────────────────────────────────────────────────────
816
+ async readClipboard() {
817
+ try {
818
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', 'Get-Clipboard'], { timeout: CLIPBOARD_TIMEOUT_MS });
819
+ // Get-Clipboard tacks on a trailing CRLF — trim for consistency with macOS.
820
+ return stdout?.replace(/\r?\n$/, '') ?? '';
821
+ }
822
+ catch {
823
+ return '';
824
+ }
825
+ }
826
+ async writeClipboard(text) {
827
+ // Pack the command as UTF-16LE base64 so arbitrary characters (quotes,
828
+ // newlines, non-ASCII) survive without any escaping dance.
829
+ const utf16 = Buffer.from(`Set-Clipboard -Value '${text.replace(/'/g, "''")}'`, 'utf16le');
830
+ try {
831
+ await execFileAsync('powershell.exe', ['-NoProfile', '-EncodedCommand', utf16.toString('base64')], { timeout: CLIPBOARD_TIMEOUT_MS });
832
+ }
833
+ catch {
834
+ // Silent — clipboard is best-effort (same contract as macOS adapter).
835
+ }
836
+ }
837
+ // ─── APPS ─────────────────────────────────────────────────────────
838
+ /**
839
+ * Thin shim — delegates straight to `launchApp` with no alias resolution.
840
+ * The platform layer is alias-data-agnostic; alias resolution lives in
841
+ * the caller (the agent's `open_app` tool, the router's `handleOpenApp`).
842
+ * Callers that want UWP / executable / searchTerm hints must pass them
843
+ * via `launchApp` directly.
844
+ */
845
+ async openApp(name, opts) {
846
+ return this.launchApp(name, opts);
847
+ }
848
+ async launchApp(name, opts) {
849
+ // Reject control chars / backticks / $() that can escape PowerShell quoting
850
+ // regardless of how we serialize.
851
+ if (/[\r\n\t\x00-\x1f]/.test(name) || /[`$]/.test(name)) {
852
+ throw new Error('launchApp: illegal characters in app name');
853
+ }
854
+ // Snapshot existing windows ONCE before any spawn so the diff-and-poll
855
+ // helper can ignore them. Reused by the idempotency check below — saves
856
+ // a redundant `listWindows()` round-trip through the PS bridge.
857
+ let windowsBefore = [];
858
+ try {
859
+ windowsBefore = await this.listWindows();
860
+ }
861
+ catch {
862
+ // Non-fatal — empty before-set means everything looks "new".
863
+ }
864
+ // v0.8.3 — idempotency: if the app is already running AND caller didn't
865
+ // ask for a fresh instance, FOCUS the existing window instead of spawning
866
+ // another. This closes the "Outlook keeps opening" bug: a retry loop that
867
+ // launches Outlook every iteration used to spawn a new instance each time
868
+ // (Start-Process -FilePath outlook with Outlook already running launches
869
+ // a fresh window).
870
+ if (!opts?.alwaysNewInstance && !opts?.url) {
871
+ const existing = this.findExistingAppWindowIn(windowsBefore, name, opts?.uwpAppId);
872
+ if (existing) {
873
+ // Focus it so it surfaces like a launch would, then return its identity.
874
+ await this.focusWindow({ processId: existing.processId }).catch(() => { });
875
+ return { pid: existing.processId, title: existing.title, handle: existing.handle };
876
+ }
877
+ }
878
+ // Route 1: UWP apps via explorer shell:AppsFolder\<id>. This is the Windows-
879
+ // sanctioned way to launch UWP / Store apps and is rock-solid — Calculator,
880
+ // Notepad-Win11, Photos, etc. all work.
881
+ if (opts?.uwpAppId) {
882
+ const id = opts.uwpAppId;
883
+ // App ID format is `<PackageFamily>_<Hash>!<AppId>`. Valid characters are
884
+ // alphanumerics, dots, underscores, hyphens, and a single `!`. Reject anything
885
+ // else to keep the shell: path from interpreting metacharacters.
886
+ if (!/^[A-Za-z0-9_.\-]+![A-Za-z0-9_.\-]+$/.test(id)) {
887
+ throw new Error(`launchApp: illegal uwpAppId "${id}"`);
888
+ }
889
+ try {
890
+ const child = (0, child_process_1.spawn)('explorer.exe', [`shell:AppsFolder\\${id}`], {
891
+ stdio: 'ignore', detached: true, windowsHide: true,
892
+ });
893
+ child.unref();
894
+ }
895
+ catch {
896
+ // Non-fatal — continue and look for the window anyway.
897
+ }
898
+ // Shorter primary budget so we have headroom for the Start-Menu
899
+ // fallback if shell:AppsFolder didn't surface a window — matches
900
+ // the router's strategy ladder.
901
+ const uwpResult = await this.findLaunchedWindow(name, windowsBefore, 4_000);
902
+ if (uwpResult.title)
903
+ return this.foregroundLaunched(uwpResult);
904
+ if (opts?.noStartMenuFallback)
905
+ return uwpResult; // {} — caller verifies
906
+ return this.foregroundLaunched(await this.launchViaStartMenuSearch(name, opts?.searchTerm, windowsBefore));
907
+ }
908
+ // Route 2: classic Start-Process via PowerShell with safely quoted args.
909
+ const args = ['-NoProfile', '-Command'];
910
+ const cmdParts = ['Start-Process'];
911
+ cmdParts.push('-FilePath', this.psQuote(name));
912
+ if (opts?.url && !/[\r\n\t\x00-\x1f"'`$]/.test(opts.url)) {
913
+ cmdParts.push('-ArgumentList', this.psQuote(opts.url));
914
+ }
915
+ if (opts?.cwd && !/[\r\n\t\x00-\x1f"'`$]/.test(opts.cwd)) {
916
+ cmdParts.push('-WorkingDirectory', this.psQuote(opts.cwd));
917
+ }
918
+ args.push(cmdParts.join(' '));
919
+ try {
920
+ const child = (0, child_process_1.spawn)('powershell.exe', args, {
921
+ stdio: 'ignore', detached: true, windowsHide: true,
922
+ });
923
+ child.unref();
924
+ }
925
+ catch {
926
+ // Fall through to the lookup — the app may already be running.
927
+ }
928
+ // Try the primary Start-Process result with a shorter budget so we have
929
+ // time for the Start-Menu fallback if it returns empty. Edge / VS Code /
930
+ // any binary not on PATH but Start-Menu-indexed will recover here.
931
+ const direct = await this.findLaunchedWindow(name, windowsBefore, 4_000);
932
+ if (direct.title)
933
+ return this.foregroundLaunched(direct);
934
+ if (opts?.noStartMenuFallback)
935
+ return direct; // {} — caller verifies (open_file)
936
+ // Route 3: Start Menu search fallback — universal for any app indexed by
937
+ // Windows. Press the Win key, type the app name, press Enter. This is
938
+ // the same pattern the router's zero-LLM fast path uses; ported here so
939
+ // every caller of launchApp (agent's open_app, MCP, REST) gets the
940
+ // reliability without duplicating router logic.
941
+ return this.foregroundLaunched(await this.launchViaStartMenuSearch(name, opts?.searchTerm, windowsBefore));
942
+ }
943
+ /**
944
+ * Bring a freshly-launched window to the foreground. A detached spawn opens
945
+ * the app BEHIND the current foreground (Windows foreground-lock), so without
946
+ * this `open_app("calc")` left Calculator in the background and every
947
+ * subsequent focused-window op (read_screen, find_element) targeted the wrong
948
+ * window. The idempotency path already focuses; this gives fresh launches the
949
+ * same contract. Best-effort — never throws, the launch already succeeded.
950
+ */
951
+ async foregroundLaunched(result) {
952
+ if (result?.pid) {
953
+ await this.focusWindow({ processId: result.pid, title: result.title }).catch(() => { });
954
+ }
955
+ return result;
956
+ }
957
+ /**
958
+ * Last-resort launch via Windows' own Start Menu search. Works for any
959
+ * app the user can find by name in the Start Menu (apps, settings panes,
960
+ * UWP without a known AppsFolder ID, third-party Win32 binaries with an
961
+ * App Paths entry). The keyboard primitives we use here go through the
962
+ * adapter directly, NOT through the safety layer — this is internal
963
+ * platform logic, not an agent action.
964
+ *
965
+ * Tuned to the same cadence as the router's startMenuSearch helper.
966
+ */
967
+ async launchViaStartMenuSearch(name, searchTermHint, windowsBefore) {
968
+ // Pick the term Windows Search will actually rank correctly. The alias's
969
+ // `searchTerm` (when provided) is the human-friendly name an end user
970
+ // would type — "Edge", "VS Code", "File Explorer". For names without an
971
+ // alias, fall back to stripping the file-system suffix off `name`:
972
+ // `msedge.exe` → `msedge`, `notepad.exe` → `notepad`, etc. Without this
973
+ // distinction, typing the binary name in Start Menu can surface the
974
+ // wrong app (e.g. "msedge" → Microsoft Store as the closest match).
975
+ const searchText = (searchTermHint && searchTermHint.trim())
976
+ ? searchTermHint.trim()
977
+ : name.replace(/\.(exe|com)$/i, '');
978
+ try {
979
+ // Close any in-progress Start Menu / search overlay so the Win key
980
+ // reliably opens a fresh one.
981
+ await this.keyPress('Escape').catch(() => { });
982
+ await this.delay(120);
983
+ await this.keyPress('Super');
984
+ await this.delay(600);
985
+ await this.typeText(searchText);
986
+ await this.delay(700);
987
+ await this.keyPress('Return');
988
+ }
989
+ catch {
990
+ // Keyboard layer flaky — caller will see empty result and decide.
991
+ }
992
+ // The post-launch predicate still uses the launched binary `name`
993
+ // because that's what the new window's processName will look like
994
+ // (msedge.exe → process "msedge"); the searchText only drives what
995
+ // Windows Search resolves to.
996
+ const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), { timeoutMs: 4_000 });
997
+ return win
998
+ ? { pid: win.processId, title: win.title, handle: win.handle }
999
+ : {};
1000
+ }
1001
+ /**
1002
+ * After a launch, wait for the new window to surface. Uses the shared
1003
+ * `waitForLaunchedWindow` diff-and-poll helper so the budget is spent
1004
+ * doing useful work (polling every 300ms) rather than a single fixed
1005
+ * settle. Returns `{}` when the deadline elapses with no match — caller
1006
+ * can interpret that as a real "this strategy didn't work" signal and
1007
+ * try the next strategy.
1008
+ *
1009
+ * On Windows, neither the UWP shell:AppsFolder spawn nor the classic
1010
+ * Start-Process spawn returns the eventual app's PID (we spawn explorer /
1011
+ * powershell, not the target binary), so we don't pass `spawnPid`.
1012
+ * The predicate matches by process name + title, same as the old
1013
+ * single-shot logic — just polled.
1014
+ */
1015
+ async findLaunchedWindow(name, windowsBefore, timeoutMs) {
1016
+ const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), timeoutMs ? { timeoutMs } : undefined);
1017
+ return win
1018
+ ? { pid: win.processId, title: win.title, handle: win.handle }
1019
+ : {};
1020
+ }
1021
+ /**
1022
+ * v0.8.3 — check whether an app matching `name` or `uwpAppId` already has
1023
+ * a visible top-level window. Used by `launchApp` to short-circuit when
1024
+ * the user / agent asks to "open Outlook" but Outlook is already running.
1025
+ *
1026
+ * Match policy: case-insensitive process-name / title substring, which
1027
+ * matches the same alias set the router uses. A `uwpAppId` like
1028
+ * `Microsoft.WindowsCalculator_8wekyb3d8bbwe!App` is reduced to its App
1029
+ * token (`App`, `Calculator`) and matched against window titles as a
1030
+ * fallback.
1031
+ *
1032
+ * Returns `null` when no matching window is found — caller proceeds with
1033
+ * a normal launch.
1034
+ */
1035
+ async findExistingAppWindow(name, uwpAppId) {
1036
+ try {
1037
+ const windows = await this.listWindows();
1038
+ return this.findExistingAppWindowIn(windows, name, uwpAppId);
1039
+ }
1040
+ catch {
1041
+ return null;
1042
+ }
1043
+ }
1044
+ /**
1045
+ * Same matching logic as `findExistingAppWindow` but takes an already-fetched
1046
+ * window list. Lets `launchApp` reuse the snapshot it captures for the
1047
+ * post-spawn diff-and-poll, avoiding a redundant PS-bridge round-trip.
1048
+ */
1049
+ findExistingAppWindowIn(windows, name, uwpAppId) {
1050
+ if (windows.length === 0)
1051
+ return null;
1052
+ const target = name.trim().toLowerCase();
1053
+ // Strip any trailing `.exe` so `outlook.exe` still matches `outlook`.
1054
+ const targetStem = target.replace(/\.(exe|com|app)$/, '');
1055
+ // Tier 1: exact processName match.
1056
+ let hit = windows.find(w => w.processName.toLowerCase() === targetStem);
1057
+ // Tier 2: processName substring (handles olk ↔ outlook etc.).
1058
+ if (!hit)
1059
+ hit = windows.find(w => w.processName.toLowerCase().includes(targetStem));
1060
+ // Tier 3: reverse — targetStem contains processName (e.g. name="msedge.exe", proc="msedge").
1061
+ if (!hit)
1062
+ hit = windows.find(w => targetStem.includes(w.processName.toLowerCase()) && w.processName.length >= 3);
1063
+ // Tier 4: title substring.
1064
+ if (!hit)
1065
+ hit = windows.find(w => w.title.toLowerCase().includes(targetStem));
1066
+ // UWP fallback — check the AppsFolder id's last segment against titles.
1067
+ if (!hit && uwpAppId) {
1068
+ const uwpTail = uwpAppId.split('!').pop()?.toLowerCase() ?? '';
1069
+ if (uwpTail)
1070
+ hit = windows.find(w => w.title.toLowerCase().includes(uwpTail));
1071
+ }
1072
+ // Skip minimized windows — if the user hid it, they probably want a
1073
+ // "fresh" focus, but we still return it so focusWindow can restore.
1074
+ return hit ?? null;
1075
+ }
1076
+ /**
1077
+ * PowerShell single-quoted string escape. Inside single quotes, the only
1078
+ * special char is the single quote itself, which doubles to escape.
1079
+ * This is the only safe way to pass a user-controlled string as a
1080
+ * PowerShell argument.
1081
+ */
1082
+ psQuote(s) {
1083
+ return `'${s.replace(/'/g, "''")}'`;
1084
+ }
1085
+ // ─── INTERNAL HELPERS ─────────────────────────────────────────────
1086
+ normalizeWindow = (raw) => ({
1087
+ title: raw?.title ?? '',
1088
+ processName: raw?.processName ?? '',
1089
+ processId: raw?.processId ?? 0,
1090
+ bounds: raw?.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
1091
+ isMinimized: raw?.isMinimized ?? false,
1092
+ handle: raw?.handle ?? raw?.processId,
1093
+ });
1094
+ normalizeElement = (raw) => {
1095
+ const enabled = raw?.isEnabled ?? raw?.enabled;
1096
+ return {
1097
+ name: raw?.name ?? '',
1098
+ controlType: (raw?.controlType ?? '').replace(/^ControlType\./, ''),
1099
+ bounds: raw?.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
1100
+ value: raw?.value,
1101
+ enabled,
1102
+ focused: raw?.focused,
1103
+ // Tranche 1A: richer state fields from ps-bridge.
1104
+ selected: raw?.selected ?? raw?.isSelected,
1105
+ disabled: enabled === false ? true : undefined,
1106
+ busy: raw?.busy ?? raw?.isBusy,
1107
+ offscreen: raw?.offscreen ?? raw?.isOffscreen,
1108
+ expandable: raw?.expandable,
1109
+ expanded: raw?.expanded,
1110
+ automationId: raw?.automationId,
1111
+ processId: raw?.processId ?? raw?.pid,
1112
+ };
1113
+ };
1114
+ /**
1115
+ * Flatten the UIA tree into a single list, matching the macOS adapter's
1116
+ * contract. Drops purely structural unnamed nodes to keep the list useful.
1117
+ */
1118
+ flattenTree(node, acc) {
1119
+ if (!node)
1120
+ return;
1121
+ // ConvertTo-UINode may return an array of children when it skipped an
1122
+ // unnamed container — just recurse through those.
1123
+ if (Array.isArray(node)) {
1124
+ for (const n of node)
1125
+ this.flattenTree(n, acc);
1126
+ return;
1127
+ }
1128
+ if (node.controlType || node.name)
1129
+ acc.push(this.normalizeElement(node));
1130
+ if (Array.isArray(node.children)) {
1131
+ for (const child of node.children)
1132
+ this.flattenTree(child, acc);
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Map a portable key token to the nut-js Key enum (or 'TYPE_CHAR' for
1137
+ * printable ASCII symbols that don't have a direct enum entry).
1138
+ */
1139
+ mapKey(name) {
1140
+ const direct = WIN_KEY_MAP[name] ?? WIN_KEY_MAP[name.toLowerCase()];
1141
+ if (direct !== undefined)
1142
+ return direct;
1143
+ if (name.length === 1) {
1144
+ const ch = name;
1145
+ const upper = ch.toUpperCase();
1146
+ // A-Z
1147
+ if (upper >= 'A' && upper <= 'Z') {
1148
+ const k = nut_js_1.Key[upper];
1149
+ if (k !== undefined)
1150
+ return k;
1151
+ }
1152
+ // 0-9 → nut-js uses Num1..Num9, Num0 for the top-row digits.
1153
+ if (upper >= '0' && upper <= '9') {
1154
+ const k = nut_js_1.Key[`Num${upper}`];
1155
+ if (k !== undefined)
1156
+ return k;
1157
+ }
1158
+ // Any other printable ASCII — ask keyboard.type() to handle it.
1159
+ if (ch.charCodeAt(0) >= 32 && ch.charCodeAt(0) <= 126)
1160
+ return 'TYPE_CHAR';
1161
+ }
1162
+ // Last resort: direct enum name match (e.g. "F13", "NumPad5").
1163
+ const enumVal = nut_js_1.Key[name];
1164
+ if (enumVal !== undefined)
1165
+ return enumVal;
1166
+ throw new Error(`Unknown key: "${name}"`);
1167
+ }
1168
+ delay(ms) {
1169
+ return new Promise(r => setTimeout(r, ms));
1170
+ }
1171
+ }
1172
+ exports.WindowsAdapter = WindowsAdapter;
1173
+ // Portable-token → nut-js Key lookup. Lowercase keys are checked as a
1174
+ // fallback so "Return"/"return", "Shift"/"shift", etc. all resolve.
1175
+ const WIN_KEY_MAP = {
1176
+ // Modifiers
1177
+ ctrl: nut_js_1.Key.LeftControl, control: nut_js_1.Key.LeftControl, Control: nut_js_1.Key.LeftControl,
1178
+ shift: nut_js_1.Key.LeftShift, Shift: nut_js_1.Key.LeftShift,
1179
+ alt: nut_js_1.Key.LeftAlt, Alt: nut_js_1.Key.LeftAlt, option: nut_js_1.Key.LeftAlt, opt: nut_js_1.Key.LeftAlt,
1180
+ super: nut_js_1.Key.LeftSuper, Super: nut_js_1.Key.LeftSuper, win: nut_js_1.Key.LeftSuper, windows: nut_js_1.Key.LeftSuper, meta: nut_js_1.Key.LeftSuper,
1181
+ // Navigation / editing
1182
+ return: nut_js_1.Key.Enter, Return: nut_js_1.Key.Enter, enter: nut_js_1.Key.Enter, Enter: nut_js_1.Key.Enter,
1183
+ tab: nut_js_1.Key.Tab, Tab: nut_js_1.Key.Tab,
1184
+ escape: nut_js_1.Key.Escape, Escape: nut_js_1.Key.Escape, esc: nut_js_1.Key.Escape, Esc: nut_js_1.Key.Escape,
1185
+ backspace: nut_js_1.Key.Backspace, Backspace: nut_js_1.Key.Backspace,
1186
+ delete: nut_js_1.Key.Delete, Delete: nut_js_1.Key.Delete, forwarddelete: nut_js_1.Key.Delete,
1187
+ space: nut_js_1.Key.Space, Space: nut_js_1.Key.Space,
1188
+ home: nut_js_1.Key.Home, Home: nut_js_1.Key.Home,
1189
+ end: nut_js_1.Key.End, End: nut_js_1.Key.End,
1190
+ pageup: nut_js_1.Key.PageUp, PageUp: nut_js_1.Key.PageUp,
1191
+ pagedown: nut_js_1.Key.PageDown, PageDown: nut_js_1.Key.PageDown,
1192
+ insert: nut_js_1.Key.Insert, Insert: nut_js_1.Key.Insert,
1193
+ // Arrows
1194
+ left: nut_js_1.Key.Left, Left: nut_js_1.Key.Left,
1195
+ right: nut_js_1.Key.Right, Right: nut_js_1.Key.Right,
1196
+ up: nut_js_1.Key.Up, Up: nut_js_1.Key.Up,
1197
+ down: nut_js_1.Key.Down, Down: nut_js_1.Key.Down,
1198
+ // F-keys
1199
+ f1: nut_js_1.Key.F1, F1: nut_js_1.Key.F1, f2: nut_js_1.Key.F2, F2: nut_js_1.Key.F2, f3: nut_js_1.Key.F3, F3: nut_js_1.Key.F3,
1200
+ f4: nut_js_1.Key.F4, F4: nut_js_1.Key.F4, f5: nut_js_1.Key.F5, F5: nut_js_1.Key.F5, f6: nut_js_1.Key.F6, F6: nut_js_1.Key.F6,
1201
+ f7: nut_js_1.Key.F7, F7: nut_js_1.Key.F7, f8: nut_js_1.Key.F8, F8: nut_js_1.Key.F8, f9: nut_js_1.Key.F9, F9: nut_js_1.Key.F9,
1202
+ f10: nut_js_1.Key.F10, F10: nut_js_1.Key.F10, f11: nut_js_1.Key.F11, F11: nut_js_1.Key.F11, f12: nut_js_1.Key.F12, F12: nut_js_1.Key.F12,
1203
+ // Symbol keys reachable as single chars in combos like "ctrl++" / "ctrl+-"
1204
+ '=': nut_js_1.Key.Equal,
1205
+ '+': nut_js_1.Key.Equal,
1206
+ '-': nut_js_1.Key.Minus,
1207
+ '_': nut_js_1.Key.Minus,
1208
+ '`': nut_js_1.Key.Grave,
1209
+ };
1210
+ //# sourceMappingURL=windows.js.map