@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,314 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Finds a UI element and invokes an action on it using UIA Patterns.
4
+ .PARAMETER AutomationId
5
+ Find the element by AutomationId (exact match).
6
+ .PARAMETER Name
7
+ Find the element by Name (exact match). Used if AutomationId is not specified.
8
+ .PARAMETER ControlType
9
+ Optional. Filter by ControlType to narrow the search (e.g. "Button", "Edit").
10
+ .PARAMETER ProcessId
11
+ Required. The process ID of the target window.
12
+ .PARAMETER Action
13
+ The action to perform: "click", "set-value", "get-value", "focus", "expand", "collapse", "toggle", "select".
14
+ .PARAMETER Value
15
+ The value to set (only used with "set-value" action).
16
+ #>
17
+ param(
18
+ [string]$AutomationId = "",
19
+ [string]$Name = "",
20
+ [string]$ControlType = "",
21
+ [Parameter(Mandatory=$true)]
22
+ [int]$ProcessId,
23
+ [Parameter(Mandatory=$true)]
24
+ [ValidateSet("click", "set-value", "get-value", "focus", "expand", "collapse", "toggle", "select")]
25
+ [string]$Action,
26
+ [string]$Value = ""
27
+ )
28
+
29
+ try {
30
+ Add-Type -AssemblyName UIAutomationClient
31
+ Add-Type -AssemblyName UIAutomationTypes
32
+ } catch {
33
+ [Console]::Out.Write((@{ success = $false; error = "Failed to load UI Automation assemblies: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
34
+ exit 1
35
+ }
36
+
37
+ $ErrorActionPreference = 'Stop'
38
+
39
+ # Control type mapping
40
+ $ctMap = @{
41
+ "Button" = [System.Windows.Automation.ControlType]::Button
42
+ "CheckBox" = [System.Windows.Automation.ControlType]::CheckBox
43
+ "ComboBox" = [System.Windows.Automation.ControlType]::ComboBox
44
+ "Custom" = [System.Windows.Automation.ControlType]::Custom
45
+ "DataItem" = [System.Windows.Automation.ControlType]::DataItem
46
+ "Document" = [System.Windows.Automation.ControlType]::Document
47
+ "Edit" = [System.Windows.Automation.ControlType]::Edit
48
+ "Group" = [System.Windows.Automation.ControlType]::Group
49
+ "Hyperlink" = [System.Windows.Automation.ControlType]::Hyperlink
50
+ "List" = [System.Windows.Automation.ControlType]::List
51
+ "ListItem" = [System.Windows.Automation.ControlType]::ListItem
52
+ "Menu" = [System.Windows.Automation.ControlType]::Menu
53
+ "MenuBar" = [System.Windows.Automation.ControlType]::MenuBar
54
+ "MenuItem" = [System.Windows.Automation.ControlType]::MenuItem
55
+ "Pane" = [System.Windows.Automation.ControlType]::Pane
56
+ "RadioButton" = [System.Windows.Automation.ControlType]::RadioButton
57
+ "ScrollBar" = [System.Windows.Automation.ControlType]::ScrollBar
58
+ "Slider" = [System.Windows.Automation.ControlType]::Slider
59
+ "Spinner" = [System.Windows.Automation.ControlType]::Spinner
60
+ "SplitButton" = [System.Windows.Automation.ControlType]::SplitButton
61
+ "Tab" = [System.Windows.Automation.ControlType]::Tab
62
+ "TabItem" = [System.Windows.Automation.ControlType]::TabItem
63
+ "Text" = [System.Windows.Automation.ControlType]::Text
64
+ "ToolBar" = [System.Windows.Automation.ControlType]::ToolBar
65
+ "Tree" = [System.Windows.Automation.ControlType]::Tree
66
+ "TreeItem" = [System.Windows.Automation.ControlType]::TreeItem
67
+ "Window" = [System.Windows.Automation.ControlType]::Window
68
+ }
69
+
70
+ try {
71
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
72
+
73
+ # Find the target window
74
+ $procCondition = New-Object System.Windows.Automation.PropertyCondition(
75
+ [System.Windows.Automation.AutomationElement]::ProcessIdProperty,
76
+ $ProcessId
77
+ )
78
+ $window = $root.FindFirst(
79
+ [System.Windows.Automation.TreeScope]::Children,
80
+ $procCondition
81
+ )
82
+
83
+ if ($null -eq $window) {
84
+ [Console]::Out.Write((@{ success = $false; error = "No window found for ProcessId $ProcessId" } | ConvertTo-Json -Compress))
85
+ exit 0
86
+ }
87
+
88
+ # Build condition (without name — fuzzy name matching done below)
89
+ $conditions = @()
90
+ if ($AutomationId -ne "") {
91
+ $conditions += New-Object System.Windows.Automation.PropertyCondition(
92
+ [System.Windows.Automation.AutomationElement]::AutomationIdProperty, $AutomationId
93
+ )
94
+ }
95
+ if ($ControlType -ne "" -and $ctMap.ContainsKey($ControlType)) {
96
+ $conditions += New-Object System.Windows.Automation.PropertyCondition(
97
+ [System.Windows.Automation.AutomationElement]::ControlTypeProperty, $ctMap[$ControlType]
98
+ )
99
+ }
100
+ if ($conditions.Count -eq 0 -and $Name -eq "") {
101
+ [Console]::Out.Write((@{ success = $false; error = "Must specify at least -AutomationId or -Name" } | ConvertTo-Json -Compress))
102
+ exit 0
103
+ }
104
+
105
+ $searchCondition = if ($conditions.Count -eq 0) { [System.Windows.Automation.Condition]::TrueCondition }
106
+ elseif ($conditions.Count -eq 1) { $conditions[0] }
107
+ else { New-Object System.Windows.Automation.AndCondition([System.Windows.Automation.Condition[]]$conditions) }
108
+
109
+ $element = $null
110
+
111
+ # Fast path: exact automationId match
112
+ if ($AutomationId -ne "" -and $conditions.Count -gt 0) {
113
+ $element = $window.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $searchCondition)
114
+ }
115
+
116
+ # Fuzzy name match: strip keyboard shortcut suffix ("Save\tCtrl+S" → "save")
117
+ if ($null -eq $element -and $Name -ne "") {
118
+ $nameLower = $Name.ToLower()
119
+ $candidates = $window.FindAll([System.Windows.Automation.TreeScope]::Descendants, $searchCondition)
120
+ # First pass: exact stripped match
121
+ foreach ($el in $candidates) {
122
+ try {
123
+ $elName = ($el.Current.Name -replace '\t.*$', '').Trim().ToLower()
124
+ if ($elName -eq $nameLower -and $elName.Length -gt 0) { $element = $el; break }
125
+ } catch {}
126
+ }
127
+ # Second pass: contains match
128
+ if ($null -eq $element) {
129
+ foreach ($el in $candidates) {
130
+ try {
131
+ $elName = ($el.Current.Name -replace '\t.*$', '').Trim().ToLower()
132
+ if ($elName.Length -gt 0 -and ($elName.Contains($nameLower) -or $nameLower.Contains($elName))) {
133
+ $element = $el; break
134
+ }
135
+ } catch {}
136
+ }
137
+ }
138
+ }
139
+
140
+ if ($null -eq $element) {
141
+ $searchDesc = ""
142
+ if ($AutomationId -ne "") { $searchDesc += "AutomationId='$AutomationId' " }
143
+ if ($Name -ne "") { $searchDesc += "Name='$Name' " }
144
+ if ($ControlType -ne "") { $searchDesc += "ControlType='$ControlType' " }
145
+ [Console]::Out.Write((@{ success = $false; error = "Element not found: $($searchDesc.Trim())" } | ConvertTo-Json -Compress))
146
+ exit 0
147
+ }
148
+
149
+ # Execute the requested action
150
+ switch ($Action) {
151
+ "click" {
152
+ # Some web/Electron buttons advertise InvokePattern but block on Invoke()
153
+ # without ever throwing — caller would hang indefinitely. Wrap the pattern
154
+ # call in a Task with a 2s timeout, then fall through to the bounds-fallback
155
+ # JSON the legacy catch path already produces. See issue #71.
156
+ $rect = $element.Current.BoundingRectangle
157
+ $clickX = [int]($rect.X + $rect.Width / 2)
158
+ $clickY = [int]($rect.Y + $rect.Height / 2)
159
+
160
+ # Result is mutated by the Task action via reference (PSCustomObject).
161
+ # Closure capture (.GetNewClosure()) keeps $element and $invokeResult
162
+ # references valid inside the Task delegate.
163
+ $invokeResult = [PSCustomObject]@{ Method = $null; Error = $null }
164
+ $localElement = $element
165
+ $invokeBlock = {
166
+ try {
167
+ $p = $localElement.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)
168
+ $p.Invoke()
169
+ $invokeResult.Method = "InvokePattern"
170
+ } catch {
171
+ try {
172
+ $p = $localElement.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)
173
+ $p.Toggle()
174
+ $invokeResult.Method = "TogglePattern"
175
+ } catch {
176
+ $invokeResult.Error = $_.Exception.Message
177
+ }
178
+ }
179
+ }.GetNewClosure()
180
+
181
+ $task = [System.Threading.Tasks.Task]::Run([System.Action]$invokeBlock)
182
+ $completedInTime = $task.Wait(2000)
183
+
184
+ if ($completedInTime -and $invokeResult.Method) {
185
+ # Pattern call returned within timeout — success
186
+ [Console]::Out.Write((@{ success = $true; action = "click"; method = $invokeResult.Method } | ConvertTo-Json -Compress))
187
+ } elseif ($completedInTime) {
188
+ # Pattern call returned but threw on both Invoke and Toggle — bounds fallback (legacy behaviour)
189
+ [Console]::Out.Write((@{
190
+ success = $false
191
+ action = "click"
192
+ error = "No InvokePattern or TogglePattern supported. Use coordinate click."
193
+ clickPoint = @{ x = $clickX; y = $clickY }
194
+ } | ConvertTo-Json -Depth 5 -Compress))
195
+ } else {
196
+ # Hung past 2s — element advertises a pattern but does not honour it (typical for
197
+ # React/Chromium buttons wired to onclick). The Task is left to finish on its own;
198
+ # the PowerShell process will exit shortly and clean it up.
199
+ [Console]::Out.Write((@{
200
+ success = $false
201
+ action = "click"
202
+ error = "InvokePattern timed out after 2s (element advertises pattern but blocks on Invoke). Use coordinate click."
203
+ clickPoint = @{ x = $clickX; y = $clickY }
204
+ } | ConvertTo-Json -Depth 5 -Compress))
205
+ }
206
+ }
207
+ "set-value" {
208
+ if ($Value -eq "") {
209
+ [Console]::Out.Write((@{ success = $false; error = "Value parameter required for set-value action" } | ConvertTo-Json -Compress))
210
+ exit 0
211
+ }
212
+ # Try ValuePattern directly on the located element first.
213
+ # If it fails (common for ComboBox composites where the ValuePattern
214
+ # lives on the inner Edit child, not the ComboBox wrapper), fall back
215
+ # to the first Edit child — this covers the Win11 Save-As filename
216
+ # field which is a ComboBox containing an Edit (automation-id 1001).
217
+ $targetElement = $element
218
+ $setError = $null
219
+ try {
220
+ $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
221
+ $pattern.SetValue($Value)
222
+ } catch {
223
+ $setError = $_.Exception.Message
224
+ # Fallback: locate the first Edit child and try there.
225
+ try {
226
+ $editCond = New-Object System.Windows.Automation.PropertyCondition(
227
+ [System.Windows.Automation.AutomationElement]::ControlTypeProperty,
228
+ [System.Windows.Automation.ControlType]::Edit
229
+ )
230
+ $editChild = $targetElement.FindFirst([System.Windows.Automation.TreeScope]::Children, $editCond)
231
+ if ($null -ne $editChild) {
232
+ $childPattern = $editChild.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
233
+ $childPattern.SetValue($Value)
234
+ $setError = $null # success via child
235
+ }
236
+ } catch {
237
+ $setError = "ValuePattern not supported on element or inner Edit child: $($_.Exception.Message)"
238
+ }
239
+ }
240
+ if ($null -eq $setError) {
241
+ [Console]::Out.Write((@{ success = $true; action = "set-value"; value = $Value } | ConvertTo-Json -Compress))
242
+ } else {
243
+ [Console]::Out.Write((@{ success = $false; error = $setError } | ConvertTo-Json -Compress))
244
+ }
245
+ }
246
+ "get-value" {
247
+ try {
248
+ $pattern = $element.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
249
+ $val = $pattern.Current.Value
250
+ [Console]::Out.Write((@{ success = $true; action = "get-value"; value = $val } | ConvertTo-Json -Compress))
251
+ } catch {
252
+ # Try TextPattern as fallback
253
+ try {
254
+ $textPattern = $element.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern)
255
+ $range = $textPattern.DocumentRange
256
+ $val = $range.GetText(-1)
257
+ [Console]::Out.Write((@{ success = $true; action = "get-value"; value = $val; method = "TextPattern" } | ConvertTo-Json -Compress))
258
+ } catch {
259
+ # Last resort: return the element's Name
260
+ $val = $element.Current.Name
261
+ [Console]::Out.Write((@{ success = $true; action = "get-value"; value = $val; method = "NameProperty" } | ConvertTo-Json -Compress))
262
+ }
263
+ }
264
+ }
265
+ "focus" {
266
+ try {
267
+ $element.SetFocus()
268
+ [Console]::Out.Write((@{ success = $true; action = "focus" } | ConvertTo-Json -Compress))
269
+ } catch {
270
+ [Console]::Out.Write((@{ success = $false; error = "Failed to set focus: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
271
+ }
272
+ }
273
+ "expand" {
274
+ try {
275
+ $pattern = $element.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
276
+ $pattern.Expand()
277
+ [Console]::Out.Write((@{ success = $true; action = "expand" } | ConvertTo-Json -Compress))
278
+ } catch {
279
+ [Console]::Out.Write((@{ success = $false; error = "ExpandCollapsePattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
280
+ }
281
+ }
282
+ "collapse" {
283
+ try {
284
+ $pattern = $element.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
285
+ $pattern.Collapse()
286
+ [Console]::Out.Write((@{ success = $true; action = "collapse" } | ConvertTo-Json -Compress))
287
+ } catch {
288
+ [Console]::Out.Write((@{ success = $false; error = "ExpandCollapsePattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
289
+ }
290
+ }
291
+ "toggle" {
292
+ try {
293
+ $pattern = $element.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)
294
+ $pattern.Toggle()
295
+ $state = $pattern.Current.ToggleState.ToString()
296
+ [Console]::Out.Write((@{ success = $true; action = "toggle"; toggleState = $state } | ConvertTo-Json -Compress))
297
+ } catch {
298
+ [Console]::Out.Write((@{ success = $false; error = "TogglePattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
299
+ }
300
+ }
301
+ "select" {
302
+ try {
303
+ $pattern = $element.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)
304
+ $pattern.Select()
305
+ [Console]::Out.Write((@{ success = $true; action = "select" } | ConvertTo-Json -Compress))
306
+ } catch {
307
+ [Console]::Out.Write((@{ success = $false; error = "SelectionItemPattern not supported: $($_.Exception.Message)" } | ConvertTo-Json -Compress))
308
+ }
309
+ }
310
+ }
311
+ } catch {
312
+ [Console]::Out.Write((@{ success = $false; error = $_.Exception.Message } | ConvertTo-Json -Compress))
313
+ exit 1
314
+ }
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AT-SPI bridge — read-only first pass (Tranche 4b).
4
+
5
+ Wraps GNOME's AT-SPI D-Bus a11y API via gobject-introspection's Atspi
6
+ binding. Used by LinuxAdapter to answer getUiTree / findElements /
7
+ getFocusedElement when the host is a Linux box with at-spi2 running
8
+ (every modern GNOME / KDE session with accessibility enabled).
9
+
10
+ Contract: same JSON shape as scripts/ps-bridge.ps1 (Windows) and
11
+ scripts/mac/*.jxa (macOS) — one JSON blob to stdout, exit 0 on
12
+ success, exit 1 with {"error": "..."} on failure.
13
+
14
+ Commands:
15
+ --cmd get-tree [--process-id N]
16
+ Walk the a11y tree of the active window (or the given process
17
+ when --process-id is set). Returns a flat list of elements.
18
+
19
+ --cmd find [--name N] [--role R] [--process-id N]
20
+ Find elements matching a name substring and/or role. Returns
21
+ a flat list.
22
+
23
+ --cmd focused
24
+ Return the currently-focused a11y element (or null).
25
+
26
+ NOT IMPLEMENTED in this pass (stays at the LinuxAdapter level as a
27
+ {success:false} response):
28
+ --cmd invoke (click/focus/set-value/expand/...) — action dispatch
29
+ requires AT-SPI Action interface handling per-role. Follow-up.
30
+
31
+ Dependencies:
32
+ python3 (3.6+) with:
33
+ - python3-gi (Debian/Ubuntu) or equivalent
34
+ - gir1.2-atspi-2.0 (Debian/Ubuntu) or libatspi / atspi
35
+
36
+ Dependency probe runs on the Node side (`hasBinary('python3')` +
37
+ a `python3 -c "from gi.repository import Atspi"` check). When the
38
+ probe fails, the LinuxAdapter's a11y methods keep returning empty
39
+ gracefully — same behavior as before this bridge existed.
40
+
41
+ Safety: every AT-SPI call is wrapped in try/except so one bad
42
+ element (stale reference, permission denial, app process died)
43
+ doesn't take down the whole tree walk.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ import json
50
+ import sys
51
+ from typing import Any, Optional
52
+
53
+ try:
54
+ import gi
55
+ gi.require_version('Atspi', '2.0')
56
+ from gi.repository import Atspi # type: ignore[import-not-found]
57
+ except Exception as exc:
58
+ sys.stdout.write(json.dumps({
59
+ "error": "pyatspi/gi.repository.Atspi not available",
60
+ "detail": str(exc),
61
+ "hint": "apt-get install python3-gi gir1.2-atspi-2.0 (or distro equivalent)",
62
+ }))
63
+ sys.exit(1)
64
+
65
+
66
+ # ── Helpers ────────────────────────────────────────────────────────
67
+
68
+ MAX_TREE_DEPTH = 12
69
+ MAX_TREE_NODES = 800 # stop after this many elements to bound cost
70
+ INTERACTIVE_ROLES = {
71
+ # Roles whose state/value the agent is most likely to care about.
72
+ # Used to prefer these over structural containers when truncating.
73
+ 'push button', 'toggle button', 'check box', 'radio button',
74
+ 'menu item', 'check menu item', 'radio menu item',
75
+ 'link', 'hyperlink',
76
+ 'text', 'entry', 'password text', 'editable text', 'combo box',
77
+ 'list item', 'tree item', 'tab',
78
+ 'slider', 'spin button', 'scroll bar',
79
+ }
80
+
81
+
82
+ def safe(fn, default=None):
83
+ """Call fn(), return default if it raises (stale ref, perm denied, etc.)."""
84
+ try:
85
+ return fn()
86
+ except Exception:
87
+ return default
88
+
89
+
90
+ def node_to_dict(acc: Any) -> Optional[dict]:
91
+ """
92
+ Convert an Atspi.Accessible node into the shared UiElement JSON shape.
93
+ Returns None when the node lacks both a name AND a role — skip those.
94
+ """
95
+ if acc is None:
96
+ return None
97
+ name = safe(lambda: acc.get_name(), '') or ''
98
+ role_name = safe(lambda: acc.get_role_name(), '') or ''
99
+ if not name and not role_name:
100
+ return None
101
+
102
+ # Bounds via Component interface. Missing → zero rect.
103
+ x, y, w, h = 0, 0, 0, 0
104
+ try:
105
+ comp = acc.get_component_iface()
106
+ if comp:
107
+ extents = comp.get_extents(Atspi.CoordType.SCREEN)
108
+ x, y, w, h = extents.x, extents.y, extents.width, extents.height
109
+ except Exception:
110
+ pass
111
+
112
+ # State flags
113
+ focused = False
114
+ enabled = True
115
+ selected = False
116
+ busy = False
117
+ offscreen = False
118
+ try:
119
+ ss = acc.get_state_set()
120
+ if ss is not None:
121
+ focused = ss.contains(Atspi.StateType.FOCUSED)
122
+ enabled = ss.contains(Atspi.StateType.ENABLED) and ss.contains(Atspi.StateType.SENSITIVE)
123
+ selected = ss.contains(Atspi.StateType.SELECTED)
124
+ busy = ss.contains(Atspi.StateType.BUSY)
125
+ offscreen = not ss.contains(Atspi.StateType.VISIBLE) or not ss.contains(Atspi.StateType.SHOWING)
126
+ except Exception:
127
+ pass
128
+
129
+ # Value via Value or Text interface (whichever applies).
130
+ value = None
131
+ try:
132
+ v = acc.get_value_iface()
133
+ if v:
134
+ value = str(v.get_current_value())
135
+ except Exception:
136
+ pass
137
+ if value is None:
138
+ try:
139
+ txt = acc.get_text_iface()
140
+ if txt:
141
+ char_count = txt.get_character_count()
142
+ if char_count > 0:
143
+ value = txt.get_text(0, min(char_count, 512))
144
+ except Exception:
145
+ pass
146
+
147
+ # Process id
148
+ pid = None
149
+ try:
150
+ pid = acc.get_process_id()
151
+ except Exception:
152
+ pass
153
+
154
+ # AutomationId analogue — Atspi exposes "accessible-id" on some apps.
155
+ automation_id = safe(lambda: acc.get_accessible_id(), None)
156
+
157
+ return {
158
+ "name": name,
159
+ "controlType": role_name,
160
+ "bounds": {"x": x, "y": y, "width": w, "height": h},
161
+ "value": value,
162
+ "enabled": enabled,
163
+ "focused": focused,
164
+ "selected": selected,
165
+ "disabled": not enabled if enabled is not None else None,
166
+ "busy": busy,
167
+ "offscreen": offscreen,
168
+ "processId": pid,
169
+ "automationId": automation_id,
170
+ }
171
+
172
+
173
+ def walk(acc: Any, out: list, depth: int = 0) -> None:
174
+ """Depth-first flatten with caps on depth + total node count."""
175
+ if acc is None: return
176
+ if depth > MAX_TREE_DEPTH: return
177
+ if len(out) > MAX_TREE_NODES: return
178
+
179
+ node = node_to_dict(acc)
180
+ if node is not None:
181
+ out.append(node)
182
+
183
+ try:
184
+ child_count = acc.get_child_count()
185
+ except Exception:
186
+ return
187
+ for i in range(child_count):
188
+ try:
189
+ child = acc.get_child_at_index(i)
190
+ except Exception:
191
+ continue
192
+ walk(child, out, depth + 1)
193
+ if len(out) > MAX_TREE_NODES: return
194
+
195
+
196
+ def active_application(process_id: Optional[int] = None) -> Optional[Any]:
197
+ """Pick an Atspi.Accessible application root to walk.
198
+
199
+ Without process_id: prefer the app whose name matches the active
200
+ window title (heuristic — AT-SPI doesn't have a direct 'active app'
201
+ concept). Fall back to the first app.
202
+ """
203
+ try:
204
+ desktop = Atspi.get_desktop(0)
205
+ except Exception:
206
+ return None
207
+ try:
208
+ n = desktop.get_child_count()
209
+ except Exception:
210
+ return None
211
+
212
+ # If caller supplied a pid, match on it.
213
+ if process_id is not None:
214
+ for i in range(n):
215
+ app = safe(lambda i=i: desktop.get_child_at_index(i))
216
+ if app is None:
217
+ continue
218
+ pid = safe(lambda app=app: app.get_process_id())
219
+ if pid == process_id:
220
+ return app
221
+ return None
222
+
223
+ # Heuristic: find the app that has a FOCUSED descendant.
224
+ for i in range(n):
225
+ app = safe(lambda i=i: desktop.get_child_at_index(i))
226
+ if app is None:
227
+ continue
228
+ if has_focused_descendant(app):
229
+ return app
230
+
231
+ # Fallback: first app.
232
+ return safe(lambda: desktop.get_child_at_index(0))
233
+
234
+
235
+ def has_focused_descendant(acc: Any, depth: int = 0) -> bool:
236
+ if acc is None or depth > 6:
237
+ return False
238
+ try:
239
+ ss = acc.get_state_set()
240
+ if ss is not None and ss.contains(Atspi.StateType.FOCUSED):
241
+ return True
242
+ except Exception:
243
+ pass
244
+ try:
245
+ n = acc.get_child_count()
246
+ except Exception:
247
+ return False
248
+ for i in range(n):
249
+ child = safe(lambda i=i: acc.get_child_at_index(i))
250
+ if has_focused_descendant(child, depth + 1):
251
+ return True
252
+ return False
253
+
254
+
255
+ def focused_element() -> Optional[dict]:
256
+ try:
257
+ desktop = Atspi.get_desktop(0)
258
+ n = desktop.get_child_count()
259
+ except Exception:
260
+ return None
261
+ for i in range(n):
262
+ app = safe(lambda i=i: desktop.get_child_at_index(i))
263
+ if app is None: continue
264
+ hit = _find_focused(app, 0)
265
+ if hit is not None:
266
+ return node_to_dict(hit)
267
+ return None
268
+
269
+
270
+ def _find_focused(acc: Any, depth: int) -> Optional[Any]:
271
+ if acc is None or depth > 12: return None
272
+ try:
273
+ ss = acc.get_state_set()
274
+ if ss is not None and ss.contains(Atspi.StateType.FOCUSED):
275
+ return acc
276
+ except Exception:
277
+ pass
278
+ try:
279
+ n = acc.get_child_count()
280
+ except Exception:
281
+ return None
282
+ for i in range(n):
283
+ hit = _find_focused(safe(lambda i=i: acc.get_child_at_index(i)), depth + 1)
284
+ if hit is not None:
285
+ return hit
286
+ return None
287
+
288
+
289
+ # ── Command dispatch ─────────────────────────────────────────────
290
+
291
+ def cmd_get_tree(process_id: Optional[int]) -> dict:
292
+ app = active_application(process_id)
293
+ out: list = []
294
+ if app is not None:
295
+ walk(app, out)
296
+ return {"elements": out, "truncated": len(out) > MAX_TREE_NODES}
297
+
298
+
299
+ def cmd_find(name: Optional[str], role: Optional[str], process_id: Optional[int]) -> dict:
300
+ # Implement find as a post-filter over the tree walk — simpler than
301
+ # deep-diving the collection interface and more predictable.
302
+ tree = cmd_get_tree(process_id).get("elements", [])
303
+ if name is None and role is None:
304
+ return {"elements": tree}
305
+
306
+ name_l = name.lower() if name else None
307
+ role_l = role.lower() if role else None
308
+
309
+ def matches(el: dict) -> bool:
310
+ if name_l is not None:
311
+ el_name = (el.get("name") or "").lower()
312
+ if name_l not in el_name:
313
+ return False
314
+ if role_l is not None:
315
+ el_role = (el.get("controlType") or "").lower()
316
+ if role_l not in el_role:
317
+ return False
318
+ return True
319
+
320
+ hits = [e for e in tree if matches(e)]
321
+ return {"elements": hits}
322
+
323
+
324
+ def cmd_focused() -> dict:
325
+ el = focused_element()
326
+ return {"element": el}
327
+
328
+
329
+ def main() -> int:
330
+ p = argparse.ArgumentParser()
331
+ p.add_argument('--cmd', required=True, choices=['get-tree', 'find', 'focused'])
332
+ p.add_argument('--name', default=None)
333
+ p.add_argument('--role', default=None)
334
+ p.add_argument('--process-id', type=int, default=None)
335
+ args = p.parse_args()
336
+
337
+ try:
338
+ if args.cmd == 'get-tree':
339
+ result = cmd_get_tree(args.process_id)
340
+ elif args.cmd == 'find':
341
+ result = cmd_find(args.name, args.role, args.process_id)
342
+ elif args.cmd == 'focused':
343
+ result = cmd_focused()
344
+ else:
345
+ result = {"error": f"unknown command: {args.cmd}"}
346
+ except Exception as exc:
347
+ result = {"error": str(exc)}
348
+ sys.stdout.write(json.dumps(result))
349
+ return 1
350
+
351
+ sys.stdout.write(json.dumps(result))
352
+ return 0
353
+
354
+
355
+ if __name__ == '__main__':
356
+ sys.exit(main())