@aryacode/dsh-engineering 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/LICENSE +21 -0
  3. package/README.md +230 -2
  4. package/cordis.patch.yml +123 -0
  5. package/lib/client.js +3448 -0
  6. package/lib/index.d.ts +2 -0
  7. package/lib/index.js +9 -0
  8. package/locale/en.json +6 -0
  9. package/locale/zh.json +6 -0
  10. package/package.json +94 -3
  11. package/vendor/ftview/README.md +55 -0
  12. package/vendor/ftview/SKILL.md +74 -0
  13. package/vendor/ftview/lib/checks.d.ts +27 -0
  14. package/vendor/ftview/lib/checks.js +39 -0
  15. package/vendor/ftview/lib/core.d.ts +208 -0
  16. package/vendor/ftview/lib/core.js +160 -0
  17. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  18. package/vendor/ftview/lib/edit/drafts.js +84 -0
  19. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  20. package/vendor/ftview/lib/edit/operations.js +126 -0
  21. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  22. package/vendor/ftview/lib/edit/prepare.js +133 -0
  23. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  24. package/vendor/ftview/lib/edit/tools.js +198 -0
  25. package/vendor/ftview/lib/edit.d.ts +30 -0
  26. package/vendor/ftview/lib/edit.js +38 -0
  27. package/vendor/ftview/lib/index.d.ts +31 -0
  28. package/vendor/ftview/lib/index.js +36 -0
  29. package/vendor/ftview/lib/model.d.ts +80 -0
  30. package/vendor/ftview/lib/model.js +267 -0
  31. package/vendor/ftview/lib/session.d.ts +48 -0
  32. package/vendor/ftview/lib/session.js +98 -0
  33. package/vendor/ftview/lib/skill.d.ts +29 -0
  34. package/vendor/ftview/lib/skill.js +43 -0
  35. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  36. package/vendor/ftview/lib/studio/discover.js +87 -0
  37. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  38. package/vendor/ftview/lib/studio/preflight.js +64 -0
  39. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  40. package/vendor/ftview/lib/studio/tools.js +83 -0
  41. package/vendor/ftview/lib/studio.d.ts +30 -0
  42. package/vendor/ftview/lib/studio.js +37 -0
  43. package/vendor/ftview/lib/tools.d.ts +14 -0
  44. package/vendor/ftview/lib/tools.js +202 -0
  45. package/vendor/l5x/README.md +62 -0
  46. package/vendor/l5x/SKILL.md +81 -0
  47. package/vendor/l5x/lib/approvals.d.ts +50 -0
  48. package/vendor/l5x/lib/approvals.js +93 -0
  49. package/vendor/l5x/lib/client.js +412 -0
  50. package/vendor/l5x/lib/client.js.map +1 -0
  51. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  52. package/vendor/l5x/lib/core/errors.js +48 -0
  53. package/vendor/l5x/lib/core/params.d.ts +49 -0
  54. package/vendor/l5x/lib/core/params.js +39 -0
  55. package/vendor/l5x/lib/core/result.d.ts +46 -0
  56. package/vendor/l5x/lib/core/result.js +21 -0
  57. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  58. package/vendor/l5x/lib/core/tool.js +19 -0
  59. package/vendor/l5x/lib/drafts.d.ts +66 -0
  60. package/vendor/l5x/lib/drafts.js +98 -0
  61. package/vendor/l5x/lib/editing.d.ts +30 -0
  62. package/vendor/l5x/lib/editing.js +42 -0
  63. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  64. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  65. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  66. package/vendor/l5x/lib/engine/analysis.js +258 -0
  67. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  68. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  69. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  70. package/vendor/l5x/lib/engine/change-set.js +446 -0
  71. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  72. package/vendor/l5x/lib/engine/configuration.js +42 -0
  73. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  74. package/vendor/l5x/lib/engine/details.js +173 -0
  75. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  76. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  77. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  78. package/vendor/l5x/lib/engine/download-review.js +63 -0
  79. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  80. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  81. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  82. package/vendor/l5x/lib/engine/observations.js +67 -0
  83. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  84. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  85. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  86. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  87. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  88. package/vendor/l5x/lib/engine/review.js +123 -0
  89. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  90. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  91. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  92. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  93. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  94. package/vendor/l5x/lib/engine/usage.js +92 -0
  95. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  96. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  97. package/vendor/l5x/lib/fs.d.ts +17 -0
  98. package/vendor/l5x/lib/fs.js +36 -0
  99. package/vendor/l5x/lib/index.d.ts +36 -0
  100. package/vendor/l5x/lib/index.js +42 -0
  101. package/vendor/l5x/lib/live.d.ts +48 -0
  102. package/vendor/l5x/lib/live.js +48 -0
  103. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  104. package/vendor/l5x/lib/model/aois.js +112 -0
  105. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  106. package/vendor/l5x/lib/model/errors.js +18 -0
  107. package/vendor/l5x/lib/model/index.d.ts +20 -0
  108. package/vendor/l5x/lib/model/index.js +19 -0
  109. package/vendor/l5x/lib/model/module.d.ts +37 -0
  110. package/vendor/l5x/lib/model/module.js +54 -0
  111. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  112. package/vendor/l5x/lib/model/modules.js +55 -0
  113. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  114. package/vendor/l5x/lib/model/parse.js +116 -0
  115. package/vendor/l5x/lib/model/project.d.ts +15 -0
  116. package/vendor/l5x/lib/model/project.js +31 -0
  117. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  118. package/vendor/l5x/lib/model/routines.js +183 -0
  119. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  120. package/vendor/l5x/lib/model/tags.js +119 -0
  121. package/vendor/l5x/lib/model/types.d.ts +304 -0
  122. package/vendor/l5x/lib/model/types.js +12 -0
  123. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  124. package/vendor/l5x/lib/model/udts.js +81 -0
  125. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  126. package/vendor/l5x/lib/model/xml.js +291 -0
  127. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  128. package/vendor/l5x/lib/model/xref.js +154 -0
  129. package/vendor/l5x/lib/session.d.ts +119 -0
  130. package/vendor/l5x/lib/session.js +236 -0
  131. package/vendor/l5x/lib/skill.d.ts +30 -0
  132. package/vendor/l5x/lib/skill.js +44 -0
  133. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  134. package/vendor/l5x/lib/studio-state.js +79 -0
  135. package/vendor/l5x/lib/studio.d.ts +29 -0
  136. package/vendor/l5x/lib/studio.js +40 -0
  137. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  138. package/vendor/l5x/lib/tools/checks.js +140 -0
  139. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  140. package/vendor/l5x/lib/tools/common.js +84 -0
  141. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  142. package/vendor/l5x/lib/tools/controller.js +170 -0
  143. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  144. package/vendor/l5x/lib/tools/drafts.js +270 -0
  145. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  146. package/vendor/l5x/lib/tools/live.js +175 -0
  147. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  148. package/vendor/l5x/lib/tools/read.js +245 -0
  149. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  150. package/vendor/l5x/lib/tools/references.js +269 -0
  151. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  152. package/vendor/l5x/lib/tools/studio.js +193 -0
  153. package/vendor/l5x/poll-worker.mjs +77 -0
  154. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  155. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  156. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  157. package/vendor/mwi/README.md +60 -0
  158. package/vendor/mwi/SKILL.md +87 -0
  159. package/vendor/mwi/bridge/README.md +8 -0
  160. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  161. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  162. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  163. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  164. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  165. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  229. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  230. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  231. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  232. package/vendor/mwi/code/mw_code.py +1258 -0
  233. package/vendor/mwi/legacy/edit-session.js +50 -0
  234. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  235. package/vendor/mwi/legacy/native-code.js +83 -0
  236. package/vendor/mwi/legacy/native-groups.js +75 -0
  237. package/vendor/mwi/legacy/native-structure.js +143 -0
  238. package/vendor/mwi/legacy/native-variables.js +77 -0
  239. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  240. package/vendor/mwi/legacy/pou-package.js +66 -0
  241. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  242. package/vendor/mwi/lib/core/bridge.js +53 -0
  243. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  244. package/vendor/mwi/lib/core/code-engine.js +109 -0
  245. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  246. package/vendor/mwi/lib/core/errors.js +47 -0
  247. package/vendor/mwi/lib/core/params.d.ts +52 -0
  248. package/vendor/mwi/lib/core/params.js +46 -0
  249. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  250. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  251. package/vendor/mwi/lib/core/result.d.ts +45 -0
  252. package/vendor/mwi/lib/core/result.js +20 -0
  253. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  254. package/vendor/mwi/lib/core/tool.js +55 -0
  255. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  256. package/vendor/mwi/lib/edit/compare.js +70 -0
  257. package/vendor/mwi/lib/edit.d.ts +20 -0
  258. package/vendor/mwi/lib/edit.js +32 -0
  259. package/vendor/mwi/lib/index.d.ts +21 -0
  260. package/vendor/mwi/lib/index.js +33 -0
  261. package/vendor/mwi/lib/read/service.d.ts +117 -0
  262. package/vendor/mwi/lib/read/service.js +229 -0
  263. package/vendor/mwi/lib/read.d.ts +21 -0
  264. package/vendor/mwi/lib/read.js +33 -0
  265. package/vendor/mwi/lib/session/project.d.ts +34 -0
  266. package/vendor/mwi/lib/session/project.js +54 -0
  267. package/vendor/mwi/lib/session/service.d.ts +167 -0
  268. package/vendor/mwi/lib/session/service.js +566 -0
  269. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  270. package/vendor/mwi/lib/session/stage.js +187 -0
  271. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  272. package/vendor/mwi/lib/session/workspace.js +90 -0
  273. package/vendor/mwi/lib/skill.d.ts +36 -0
  274. package/vendor/mwi/lib/skill.js +56 -0
  275. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  276. package/vendor/mwi/lib/tools/edit.js +286 -0
  277. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  278. package/vendor/mwi/lib/tools/read.js +161 -0
  279. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  280. package/vendor/mwi/lib/tools/session.js +166 -0
  281. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  282. package/vendor/mwi/lib/tools/verify.js +85 -0
  283. package/vendor/mwi/lib/verify.d.ts +17 -0
  284. package/vendor/mwi/lib/verify.js +29 -0
@@ -0,0 +1,448 @@
1
+ """Screenshot capture: screen, a window, or the desktop, for agent observation.
2
+
3
+ An agent driving a GUI has to be able to *see* it. This module provides the eyes:
4
+ capture the whole virtual desktop, or one window found by title substring or handle.
5
+ Images are written to PNG under a scratch directory and the **path** is returned, so
6
+ the caller can attach the file rather than receive a wall of base64.
7
+
8
+ Pillow does the encoding, and ``ImageGrab`` handles the multi-monitor case --
9
+ measured on this machine, the virtual desktop is 6400x1615 across three screens, so
10
+ "grab the primary screen" would miss most of it.
11
+
12
+ Everything here is read-only: it captures pixels and never sends input. Input lives in
13
+ :mod:`motionworks_iec_mcp.uia`, deliberately separate, because looking and touching
14
+ carry very different risk.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ import time
21
+ from pathlib import Path
22
+
23
+ #: Where captures are written. Kept in the workspace so the agent can read them.
24
+ CAPTURE_DIR = Path(__file__).resolve().parents[2] / ".mcp_captures"
25
+
26
+ #: How many captures to keep. A full-desktop grab of this three-monitor setup is
27
+ #: about 4 MB, so an unbounded directory would grow by that much per screenshot and
28
+ #: fill the workspace during a long session.
29
+ KEEP_CAPTURES = 40
30
+
31
+
32
+ class CaptureError(RuntimeError):
33
+ """A screenshot could not be taken."""
34
+
35
+
36
+ def _prepare() -> None:
37
+ CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
38
+
39
+
40
+ def prune(keep: int = KEEP_CAPTURES) -> int:
41
+ """Delete the oldest captures beyond ``keep``. Returns how many were removed.
42
+
43
+ Called after every capture. Screenshots are cheap to retake and only useful while
44
+ investigating something, so keeping the newest few is the right trade against
45
+ filling the workspace.
46
+ """
47
+ try:
48
+ files = sorted(
49
+ (p for p in CAPTURE_DIR.glob("*.png") if p.is_file()),
50
+ key=lambda p: p.stat().st_mtime,
51
+ reverse=True,
52
+ )
53
+ except OSError:
54
+ return 0
55
+ removed = 0
56
+ for path in files[keep:]:
57
+ try:
58
+ path.unlink()
59
+ removed += 1
60
+ except OSError:
61
+ continue
62
+ return removed
63
+
64
+
65
+ def capture_screen(name: str = "screen") -> Path:
66
+ """Capture every monitor into one image and return the file path."""
67
+ if sys.platform != "win32":
68
+ raise CaptureError("screen capture is only implemented on Windows")
69
+ try:
70
+ from PIL import ImageGrab
71
+ except ImportError as exc: # pragma: no cover
72
+ raise CaptureError(f"Pillow is required for capture: {exc}") from exc
73
+
74
+ _prepare()
75
+ try:
76
+ image = ImageGrab.grab(all_screens=True)
77
+ except Exception as exc: # noqa: BLE001
78
+ raise CaptureError(f"could not capture the screen: {exc}") from exc
79
+
80
+ path = CAPTURE_DIR / f"{name}-{int(time.time())}.png"
81
+ image.save(path)
82
+ prune()
83
+ return path
84
+
85
+
86
+ def list_windows(min_width: int = 40, min_height: int = 40) -> list[dict[str, object]]:
87
+ """Visible top-level windows, largest first.
88
+
89
+ Windows below ``min_width``/``min_height`` are skipped: a GUI app owns many tiny
90
+ invisible helper windows, and listing them buries the one that matters. Titles are
91
+ lower-cased for matching, because the same application reports different casing.
92
+ """
93
+ if sys.platform != "win32":
94
+ return []
95
+ import win32gui
96
+
97
+ found: list[dict[str, object]] = []
98
+
99
+ def visit(hwnd, _):
100
+ if not win32gui.IsWindowVisible(hwnd):
101
+ return True
102
+ left, top, right, bottom = win32gui.GetWindowRect(hwnd)
103
+ width, height = right - left, bottom - top
104
+ if width < min_width or height < min_height:
105
+ return True
106
+ title = win32gui.GetWindowText(hwnd)
107
+ found.append(
108
+ {
109
+ "handle": hwnd,
110
+ "title": title,
111
+ "lower": title.casefold(),
112
+ "left": left,
113
+ "top": top,
114
+ "right": right,
115
+ "bottom": bottom,
116
+ "width": width,
117
+ "height": height,
118
+ }
119
+ )
120
+ return True
121
+
122
+ win32gui.EnumWindows(visit, None)
123
+ found.sort(key=lambda w: int(w["width"]) * int(w["height"]), reverse=True)
124
+ return found
125
+
126
+
127
+ def find_window(
128
+ title_contains: str | None = None,
129
+ handle: int | None = None,
130
+ exclude: tuple[str, ...] = (),
131
+ ) -> dict[str, object] | None:
132
+ """The best window matching a handle or a title substring.
133
+
134
+ Matching is case-insensitive and substring-based, so ``"motionworks"`` finds
135
+ ``"MotionWorks IEC 3 Pro - TopCutter"`` without the caller knowing the version
136
+ string. The **largest** match wins, so a splash or tooltip does not shadow the
137
+ main window.
138
+
139
+ ``exclude`` rejects titles containing any of those substrings. It is needed
140
+ because a substring search is otherwise fooled by *other* windows that merely
141
+ mention the application: measured here, a Chrome tab titled "MCP server for
142
+ **MotionWorks** IEC offline files" is 1936x1048 and therefore LARGER than the
143
+ 733x441 licence dialog, so it won and the dialog was never found.
144
+ """
145
+ if handle is not None:
146
+ import win32gui
147
+
148
+ if not win32gui.IsWindow(handle):
149
+ return None
150
+ left, top, right, bottom = win32gui.GetWindowRect(handle)
151
+ title = win32gui.GetWindowText(handle)
152
+ return {
153
+ "handle": handle,
154
+ "title": title,
155
+ "lower": title.casefold(),
156
+ "left": left,
157
+ "top": top,
158
+ "right": right,
159
+ "bottom": bottom,
160
+ "width": right - left,
161
+ "height": bottom - top,
162
+ }
163
+ if not title_contains:
164
+ return None
165
+ needle = title_contains.casefold()
166
+ matches = [
167
+ w
168
+ for w in list_windows()
169
+ if needle in str(w["lower"])
170
+ and not any(word.casefold() in str(w["lower"]) for word in exclude)
171
+ ]
172
+ return matches[0] if matches else None
173
+
174
+
175
+ def _grab_window_pixels(hwnd: int, path: Path) -> bool:
176
+ """Capture a window's rectangle from the screen, or ask it to paint itself.
177
+
178
+ Two approaches, in this order, because measurement here rejected the more
179
+ "correct-looking" one:
180
+
181
+ * **``PrintWindow``** — asks the window to render into a bitmap, immune to
182
+ occlusion and screen coordinates. On the MotionWorks trial dialog it returns 0
183
+ for both ``PW_RENDERFULLCONTENT`` and the plain flag, and the result is an
184
+ entirely black bitmap, so it cannot be relied on for this application.
185
+ * **Screen rectangle** — grab the window's rectangle out of a full-desktop
186
+ capture. This works, but the **offset must be right**: ``ImageGrab`` returns the
187
+ virtual desktop starting at its top-left, which on this machine is x=-4480. A
188
+ crop taken with the raw window coordinates is 4480 pixels off and comes back
189
+ black, which is exactly the bug this replaces.
190
+
191
+ Returns True when pixels were written.
192
+ """
193
+ import ctypes
194
+ from ctypes import wintypes
195
+
196
+ from PIL import Image
197
+
198
+ if _print_window_pixels(hwnd, path):
199
+ return True
200
+
201
+ user32 = ctypes.windll.user32
202
+ rectangle = wintypes.RECT()
203
+ if not user32.GetWindowRect(hwnd, ctypes.byref(rectangle)):
204
+ return False
205
+ virtual_left = user32.GetSystemMetrics(76) # SM_XVIRTUALSCREEN
206
+ virtual_top = user32.GetSystemMetrics(77)
207
+ virtual_width = user32.GetSystemMetrics(78)
208
+ virtual_height = user32.GetSystemMetrics(79)
209
+ if virtual_width <= 0 or virtual_height <= 0:
210
+ return False
211
+
212
+ try:
213
+ from PIL import ImageGrab
214
+
215
+ full = ImageGrab.grab(all_screens=True)
216
+ except Exception: # noqa: BLE001
217
+ return False
218
+
219
+ box = (
220
+ rectangle.left - virtual_left,
221
+ rectangle.top - virtual_top,
222
+ rectangle.right - virtual_left,
223
+ rectangle.bottom - virtual_top,
224
+ )
225
+ box = (
226
+ max(0, box[0]),
227
+ max(0, box[1]),
228
+ min(full.size[0], box[2]),
229
+ min(full.size[1], box[3]),
230
+ )
231
+ if box[2] - box[0] <= 0 or box[3] - box[1] <= 0:
232
+ return False
233
+ try:
234
+ full.crop(box).save(path)
235
+ except Exception: # noqa: BLE001
236
+ return False
237
+ prune()
238
+ return True
239
+
240
+
241
+ def _print_window_pixels(hwnd: int, path: Path) -> bool:
242
+ """The ``PrintWindow`` route. Returns False when the window paints nothing."""
243
+ import ctypes
244
+ from ctypes import wintypes
245
+
246
+ from PIL import Image
247
+
248
+ user32 = ctypes.windll.user32
249
+ gdi32 = ctypes.windll.gdi32
250
+
251
+ rectangle = wintypes.RECT()
252
+ if not user32.GetWindowRect(hwnd, ctypes.byref(rectangle)):
253
+ return False
254
+ width = rectangle.right - rectangle.left
255
+ height = rectangle.bottom - rectangle.top
256
+ if width <= 0 or height <= 0:
257
+ return False
258
+
259
+ window_dc = user32.GetWindowDC(hwnd)
260
+ if not window_dc:
261
+ return False
262
+ memory_dc = gdi32.CreateCompatibleDC(window_dc)
263
+ bitmap = gdi32.CreateCompatibleBitmap(window_dc, width, height)
264
+ if not memory_dc or not bitmap:
265
+ gdi32.DeleteDC(memory_dc)
266
+ user32.ReleaseDC(hwnd, window_dc)
267
+ return False
268
+
269
+ try:
270
+ gdi32.SelectObject(memory_dc, bitmap)
271
+ if not user32.PrintWindow(hwnd, memory_dc, 2) and not user32.PrintWindow(
272
+ hwnd, memory_dc, 0
273
+ ):
274
+ return False
275
+
276
+ class BITMAPINFOHEADER(ctypes.Structure):
277
+ _fields_ = [
278
+ ("biSize", wintypes.DWORD),
279
+ ("biWidth", wintypes.LONG),
280
+ ("biHeight", wintypes.LONG),
281
+ ("biPlanes", wintypes.WORD),
282
+ ("biBitCount", wintypes.WORD),
283
+ ("biCompression", wintypes.DWORD),
284
+ ("biSizeImage", wintypes.DWORD),
285
+ ("biXPelsPerMeter", wintypes.LONG),
286
+ ("biYPelsPerMeter", wintypes.LONG),
287
+ ("biClrUsed", wintypes.DWORD),
288
+ ("biClrImportant", wintypes.DWORD),
289
+ ]
290
+
291
+ header = BITMAPINFOHEADER()
292
+ header.biSize = ctypes.sizeof(BITMAPINFOHEADER)
293
+ header.biWidth = width
294
+ header.biHeight = -height # negative: top-down rows
295
+ header.biPlanes = 1
296
+ header.biBitCount = 32
297
+ header.biCompression = 0 # BI_RGB
298
+
299
+ buffer = ctypes.create_string_buffer(width * height * 4)
300
+ if not gdi32.GetDIBits(
301
+ memory_dc, bitmap, 0, height, buffer, ctypes.byref(header), 0
302
+ ):
303
+ return False
304
+ # A composited window reports success but yields an all-black bitmap, so
305
+ # treat "no non-zero pixel" as a failure and let the caller fall back.
306
+ if not any(buffer[: min(len(buffer), 4096)]):
307
+ return False
308
+ image = Image.frombuffer("RGB", (width, height), buffer, "raw", "BGRX", 0, 1)
309
+ image.save(path)
310
+ prune()
311
+ return True
312
+ finally:
313
+ gdi32.DeleteObject(bitmap)
314
+ gdi32.DeleteDC(memory_dc)
315
+ user32.ReleaseDC(hwnd, window_dc)
316
+
317
+
318
+ def capture_window(
319
+ title_contains: str | None = None,
320
+ handle: int | None = None,
321
+ name: str = "window",
322
+ padding: int = 0,
323
+ exclude: tuple[str, ...] = (),
324
+ ) -> tuple[Path, dict[str, object]]:
325
+ """Capture one window and return ``(path, window)``.
326
+
327
+ The window is brought to the foreground first, so the pixels captured are the
328
+ window's own rather than whatever was covering it.
329
+ """
330
+ if sys.platform != "win32":
331
+ raise CaptureError("window capture is only implemented on Windows")
332
+ window = find_window(
333
+ title_contains=title_contains, handle=handle, exclude=exclude
334
+ )
335
+ if window is None:
336
+ wanted = title_contains if title_contains else f"handle {handle}"
337
+ raise CaptureError(f"no visible window matching {wanted!r}")
338
+
339
+ _prepare()
340
+ import win32gui
341
+
342
+ hwnd = int(window["handle"])
343
+ try:
344
+ if win32gui.IsIconic(hwnd):
345
+ win32gui.ShowWindow(hwnd, 9) # SW_RESTORE
346
+ win32gui.SetForegroundWindow(hwnd)
347
+ except Exception: # noqa: BLE001 - foreground rights can be denied; still capture
348
+ pass
349
+ time.sleep(0.35)
350
+
351
+ path = CAPTURE_DIR / f"{name}-{hwnd}-{int(time.time())}.png"
352
+ if not _grab_window_pixels(hwnd, path):
353
+ raise CaptureError(f"could not capture window {hwnd}")
354
+
355
+ window["capture"] = str(path)
356
+ return path, window
357
+
358
+
359
+ def describe_windows(limit: int = 25) -> str:
360
+ """A readable listing of the visible windows, for a tool reply."""
361
+ windows = list_windows()[:limit]
362
+ if not windows:
363
+ return "No visible windows of a usable size."
364
+ lines = [f"{len(windows)} visible window(s), largest first:"]
365
+ for w in windows:
366
+ title = w["title"] or "(untitled)"
367
+ lines.append(
368
+ f" {w['width']:>5}x{w['height']:<5} at ({w['left']},{w['top']}) "
369
+ f"hwnd={w['handle']} {str(title)[:70]}"
370
+ )
371
+ return "\n".join(lines)
372
+
373
+
374
+ #: Common install locations for the Tesseract CLI.
375
+ TESSERACT_CANDIDATES = (
376
+ Path(r"C:\Program Files\Tesseract-OCR\tesseract.exe"),
377
+ Path(r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe"),
378
+ )
379
+
380
+
381
+ def tesseract_path() -> Path | None:
382
+ """The Tesseract executable, if it is installed."""
383
+ import shutil as _shutil
384
+
385
+ found = _shutil.which("tesseract")
386
+ if found:
387
+ return Path(found)
388
+ for candidate in TESSERACT_CANDIDATES:
389
+ if candidate.is_file():
390
+ return candidate
391
+ return None
392
+
393
+
394
+ def read_text(image: Path | None = None, psm: int = 6) -> str:
395
+ """OCR an image (or the screen, if none is given) into plain text.
396
+
397
+ OCR matters even though UI Automation exists, because **accessibility is not
398
+ always available**. The MotionWorks trial dialog exposes six elements and reports
399
+ every clickable control as a ``Pane``; anything the application draws itself is
400
+ invisible to UI Automation, and only pixels remain.
401
+
402
+ Tesseract is invoked as a **subprocess**, not through ``pytesseract``, so there is
403
+ no extra Python dependency -- only the executable, which is already installed
404
+ here. ``--psm 6`` treats the image as a uniform block of text, which suits dialogs
405
+ better than the default page-segmentation mode.
406
+ """
407
+ import subprocess
408
+ import tempfile
409
+
410
+ executable = tesseract_path()
411
+ if executable is None:
412
+ raise CaptureError(
413
+ "Tesseract is not installed, so text cannot be read from the image. "
414
+ "Install it (winget install UB-Mannheim.TesseractOCR) or use "
415
+ "mw_ui_elements for windows that expose accessibility."
416
+ )
417
+
418
+ if image is None:
419
+ image = capture_screen("ocr")
420
+ if not Path(image).is_file():
421
+ raise CaptureError(f"no such image: {image}")
422
+
423
+ with tempfile.TemporaryDirectory() as tmp:
424
+ stem = Path(tmp) / "out"
425
+ try:
426
+ subprocess.run(
427
+ [
428
+ str(executable),
429
+ str(image),
430
+ str(stem),
431
+ "--psm",
432
+ str(psm),
433
+ ],
434
+ stdout=subprocess.DEVNULL,
435
+ stderr=subprocess.DEVNULL,
436
+ timeout=120,
437
+ creationflags=0x08000000,
438
+ )
439
+ except Exception as exc: # noqa: BLE001
440
+ raise CaptureError(f"OCR failed to run: {exc}") from exc
441
+ text_file = stem.with_suffix(".txt")
442
+ try:
443
+ raw = text_file.read_text(encoding="utf-8", errors="replace")
444
+ except OSError as exc:
445
+ raise CaptureError(f"OCR produced no text: {exc}") from exc
446
+
447
+ lines = [line.rstrip() for line in raw.splitlines()]
448
+ return "\n".join(line for line in lines if line.strip())