@testdriverai/agent 7.8.0-canary.10

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 (528) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +4 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +1 -0
  5. package/CHANGELOG.md +953 -0
  6. package/README.md +81 -0
  7. package/agent/events.js +135 -0
  8. package/agent/index.js +2450 -0
  9. package/agent/interface.js +35 -0
  10. package/agent/lib/analytics.js +22 -0
  11. package/agent/lib/censorship.js +75 -0
  12. package/agent/lib/commander.js +246 -0
  13. package/agent/lib/commands.js +1684 -0
  14. package/agent/lib/config.js +60 -0
  15. package/agent/lib/generator.js +91 -0
  16. package/agent/lib/http.js +144 -0
  17. package/agent/lib/logger.js +56 -0
  18. package/agent/lib/outputs.js +29 -0
  19. package/agent/lib/parser.js +209 -0
  20. package/agent/lib/redraw.js +386 -0
  21. package/agent/lib/resources/cursor-2.png +0 -0
  22. package/agent/lib/sandbox.js +1104 -0
  23. package/agent/lib/sdk.js +633 -0
  24. package/agent/lib/session.js +25 -0
  25. package/agent/lib/source-mapper.js +342 -0
  26. package/agent/lib/subimage/index.js +77 -0
  27. package/agent/lib/subimage/opencv.js +69 -0
  28. package/agent/lib/system.js +204 -0
  29. package/agent/lib/theme.js +14 -0
  30. package/agent/lib/valid-version.js +21 -0
  31. package/agent/lib/validation.js +169 -0
  32. package/ai/.claude-plugin/plugin.json +9 -0
  33. package/ai/agents/testdriver.md +638 -0
  34. package/ai/skills/testdriver-ai/SKILL.md +204 -0
  35. package/ai/skills/testdriver-assert/SKILL.md +315 -0
  36. package/ai/skills/testdriver-aws-setup/SKILL.md +448 -0
  37. package/ai/skills/testdriver-cache/SKILL.md +221 -0
  38. package/ai/skills/testdriver-caching/SKILL.md +124 -0
  39. package/ai/skills/testdriver-captcha/SKILL.md +158 -0
  40. package/ai/skills/testdriver-ci-cd/SKILL.md +602 -0
  41. package/ai/skills/testdriver-click/SKILL.md +286 -0
  42. package/ai/skills/testdriver-client/SKILL.md +477 -0
  43. package/ai/skills/testdriver-cloud/SKILL.md +119 -0
  44. package/ai/skills/testdriver-customizing-devices/SKILL.md +319 -0
  45. package/ai/skills/testdriver-dashcam/SKILL.md +418 -0
  46. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +401 -0
  47. package/ai/skills/testdriver-device-config/SKILL.md +317 -0
  48. package/ai/skills/testdriver-double-click/SKILL.md +102 -0
  49. package/ai/skills/testdriver-elements/SKILL.md +605 -0
  50. package/ai/skills/testdriver-enterprise/SKILL.md +114 -0
  51. package/ai/skills/testdriver-errors/SKILL.md +246 -0
  52. package/ai/skills/testdriver-events/SKILL.md +356 -0
  53. package/ai/skills/testdriver-examples/SKILL.md +7 -0
  54. package/ai/skills/testdriver-exec/SKILL.md +317 -0
  55. package/ai/skills/testdriver-find/SKILL.md +829 -0
  56. package/ai/skills/testdriver-focus-application/SKILL.md +293 -0
  57. package/ai/skills/testdriver-generating-tests/SKILL.md +36 -0
  58. package/ai/skills/testdriver-hover/SKILL.md +278 -0
  59. package/ai/skills/testdriver-locating-elements/SKILL.md +71 -0
  60. package/ai/skills/testdriver-making-assertions/SKILL.md +32 -0
  61. package/ai/skills/testdriver-mcp/SKILL.md +7 -0
  62. package/ai/skills/testdriver-mcp-workflow/SKILL.md +410 -0
  63. package/ai/skills/testdriver-mouse-down/SKILL.md +161 -0
  64. package/ai/skills/testdriver-mouse-up/SKILL.md +164 -0
  65. package/ai/skills/testdriver-parse/SKILL.md +236 -0
  66. package/ai/skills/testdriver-performing-actions/SKILL.md +54 -0
  67. package/ai/skills/testdriver-press-keys/SKILL.md +348 -0
  68. package/ai/skills/testdriver-provision/SKILL.md +331 -0
  69. package/ai/skills/testdriver-quickstart/SKILL.md +144 -0
  70. package/ai/skills/testdriver-redraw/SKILL.md +214 -0
  71. package/ai/skills/testdriver-reusable-code/SKILL.md +249 -0
  72. package/ai/skills/testdriver-right-click/SKILL.md +123 -0
  73. package/ai/skills/testdriver-running-tests/SKILL.md +185 -0
  74. package/ai/skills/testdriver-screenshot/SKILL.md +248 -0
  75. package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
  76. package/ai/skills/testdriver-scroll/SKILL.md +335 -0
  77. package/ai/skills/testdriver-secrets/SKILL.md +115 -0
  78. package/ai/skills/testdriver-self-hosted/SKILL.md +65 -0
  79. package/ai/skills/testdriver-test-writer/SKILL.md +448 -0
  80. package/ai/skills/testdriver-testdriver/SKILL.md +628 -0
  81. package/ai/skills/testdriver-testdriver-mechanic/SKILL.md +165 -0
  82. package/ai/skills/testdriver-type/SKILL.md +357 -0
  83. package/ai/skills/testdriver-variables/SKILL.md +111 -0
  84. package/ai/skills/testdriver-wait/SKILL.md +50 -0
  85. package/ai/skills/testdriver-waiting-for-elements/SKILL.md +90 -0
  86. package/ai/skills/testdriver-what-is-testdriver/SKILL.md +54 -0
  87. package/bin/testdriverai.js +22 -0
  88. package/debugger/bg.png +0 -0
  89. package/debugger/icon.png +0 -0
  90. package/debugger/index.html +469 -0
  91. package/debugger/td.png +0 -0
  92. package/debugger/tray-buffered.png +0 -0
  93. package/debugger/tray.png +0 -0
  94. package/docs/GITHUB_COMMENTS.md +330 -0
  95. package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
  96. package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
  97. package/docs/TEST-GITHUB-COMMENTS.md +129 -0
  98. package/docs/_data/examples-manifest.json +177 -0
  99. package/docs/_data/examples-manifest.schema.json +41 -0
  100. package/docs/_scripts/extract-example-urls.js +165 -0
  101. package/docs/_scripts/generate-examples.js +560 -0
  102. package/docs/_scripts/generate-skills.js +154 -0
  103. package/docs/_scripts/link-replacer.js +164 -0
  104. package/docs/_scripts/upload-docs-to-openai.js +284 -0
  105. package/docs/changelog.mdx +161 -0
  106. package/docs/claude-mcp-plugin.mdx +160 -0
  107. package/docs/docs.json +442 -0
  108. package/docs/github-integration-setup.md +266 -0
  109. package/docs/guide/best-practices-polling.mdx +174 -0
  110. package/docs/images/content/account/newprojectsettings.png +0 -0
  111. package/docs/images/content/account/projectpage.png +0 -0
  112. package/docs/images/content/account/projectreplays.png +0 -0
  113. package/docs/images/content/account/team-manage.png +0 -0
  114. package/docs/images/content/account/teampage.png +0 -0
  115. package/docs/images/content/extension/cursor.svg +1 -0
  116. package/docs/images/content/extension/vscode.svg +57 -0
  117. package/docs/images/content/extension/windsurf.svg +3 -0
  118. package/docs/images/content/parse/output.png +0 -0
  119. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  120. package/docs/images/content/side-by-side.png +0 -0
  121. package/docs/images/content/vscode/ide-full.png +0 -0
  122. package/docs/images/content/vscode/running.png +0 -0
  123. package/docs/images/content/vscode/v7-chat.png +0 -0
  124. package/docs/images/content/vscode/v7-choose-agent.png +0 -0
  125. package/docs/images/content/vscode/v7-full.png +0 -0
  126. package/docs/images/content/vscode/v7-onboarding.png +0 -0
  127. package/docs/images/content/vscode/vscode-2-assert.png +0 -0
  128. package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
  129. package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
  130. package/docs/images/content/vscode/vscode-file-creation.png +0 -0
  131. package/docs/images/content/vscode/vscode-install.png +0 -0
  132. package/docs/images/content/vscode/vscode-overview.png +0 -0
  133. package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
  134. package/docs/images/content/vscode/vscode-stopchat.png +0 -0
  135. package/docs/images/content/vscode/vscode-stoptest.png +0 -0
  136. package/docs/images/content/vscode/vscode-tdservice.png +0 -0
  137. package/docs/images/content/vscode/vscode-test-output.png +0 -0
  138. package/docs/images/content/vscode/vscode-testhistory.png +0 -0
  139. package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
  140. package/docs/images/content/vscode/vscode-testpane.png +0 -0
  141. package/docs/images/template/dark.png +0 -0
  142. package/docs/images/template/icon.png +0 -0
  143. package/docs/images/template/light.png +0 -0
  144. package/docs/snippets/calendar-link.mdx +4 -0
  145. package/docs/snippets/gitignore-warning.mdx +7 -0
  146. package/docs/snippets/lifecycle-warning.mdx +6 -0
  147. package/docs/snippets/test-prereqs.mdx +12 -0
  148. package/docs/snippets/tests/assert-replay.mdx +7 -0
  149. package/docs/snippets/tests/assert-yaml.mdx +8 -0
  150. package/docs/snippets/tests/exec-js-replay.mdx +7 -0
  151. package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
  152. package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
  153. package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
  154. package/docs/snippets/tests/hover-image-replay.mdx +7 -0
  155. package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
  156. package/docs/snippets/tests/hover-text-replay.mdx +7 -0
  157. package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
  158. package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
  159. package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
  160. package/docs/snippets/tests/match-image-replay.mdx +7 -0
  161. package/docs/snippets/tests/match-image-yaml.mdx +17 -0
  162. package/docs/snippets/tests/press-keys-replay.mdx +7 -0
  163. package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
  164. package/docs/snippets/tests/remember-replay.mdx +7 -0
  165. package/docs/snippets/tests/remember-yaml.mdx +28 -0
  166. package/docs/snippets/tests/scroll-replay.mdx +7 -0
  167. package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
  168. package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
  169. package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
  170. package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
  171. package/docs/snippets/tests/scroll-yaml.mdx +30 -0
  172. package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
  173. package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
  174. package/docs/snippets/tests/type-replay.mdx +7 -0
  175. package/docs/snippets/tests/type-yaml.mdx +28 -0
  176. package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
  177. package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
  178. package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
  179. package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
  180. package/docs/snippets/tests/wait-replay.mdx +7 -0
  181. package/docs/snippets/tests/wait-yaml.mdx +13 -0
  182. package/docs/styles.css +65 -0
  183. package/docs/v6/account/dashboard.mdx +16 -0
  184. package/docs/v6/account/enterprise.mdx +110 -0
  185. package/docs/v6/account/pricing.mdx +33 -0
  186. package/docs/v6/account/projects.mdx +33 -0
  187. package/docs/v6/account/team.mdx +35 -0
  188. package/docs/v6/action/ami.mdx +109 -0
  189. package/docs/v6/action/performance.mdx +105 -0
  190. package/docs/v6/action/secrets.mdx +93 -0
  191. package/docs/v6/apps/chrome-extensions.mdx +48 -0
  192. package/docs/v6/apps/desktop-apps.mdx +93 -0
  193. package/docs/v6/apps/mobile-apps.mdx +26 -0
  194. package/docs/v6/apps/static-websites.mdx +54 -0
  195. package/docs/v6/apps/tauri-apps.mdx +361 -0
  196. package/docs/v6/bugs/jira.mdx +232 -0
  197. package/docs/v6/cli/overview.mdx +66 -0
  198. package/docs/v6/commands/assert.mdx +45 -0
  199. package/docs/v6/commands/exec.mdx +276 -0
  200. package/docs/v6/commands/focus-application.mdx +44 -0
  201. package/docs/v6/commands/hover-image.mdx +69 -0
  202. package/docs/v6/commands/hover-text.mdx +47 -0
  203. package/docs/v6/commands/if.mdx +53 -0
  204. package/docs/v6/commands/match-image.mdx +67 -0
  205. package/docs/v6/commands/press-keys.mdx +87 -0
  206. package/docs/v6/commands/remember.mdx +49 -0
  207. package/docs/v6/commands/run.mdx +44 -0
  208. package/docs/v6/commands/scroll-until-image.mdx +66 -0
  209. package/docs/v6/commands/scroll-until-text.mdx +60 -0
  210. package/docs/v6/commands/scroll.mdx +69 -0
  211. package/docs/v6/commands/type.mdx +45 -0
  212. package/docs/v6/commands/wait-for-image.mdx +54 -0
  213. package/docs/v6/commands/wait-for-text.mdx +48 -0
  214. package/docs/v6/commands/wait.mdx +45 -0
  215. package/docs/v6/exporting/junit.mdx +218 -0
  216. package/docs/v6/exporting/playwright.mdx +197 -0
  217. package/docs/v6/features/auto-healing.mdx +144 -0
  218. package/docs/v6/features/generation.mdx +116 -0
  219. package/docs/v6/features/parallel-testing.mdx +151 -0
  220. package/docs/v6/features/reusable-snippets.mdx +131 -0
  221. package/docs/v6/features/selectorless.mdx +80 -0
  222. package/docs/v6/features/visual-assertions.mdx +139 -0
  223. package/docs/v6/getting-started/ci.mdx +146 -0
  224. package/docs/v6/getting-started/cli.mdx +91 -0
  225. package/docs/v6/getting-started/editing.mdx +100 -0
  226. package/docs/v6/getting-started/playwright.mdx +342 -0
  227. package/docs/v6/getting-started/running.mdx +48 -0
  228. package/docs/v6/getting-started/self-hosting.mdx +408 -0
  229. package/docs/v6/getting-started/vscode.mdx +88 -0
  230. package/docs/v6/guide/assertions.mdx +189 -0
  231. package/docs/v6/guide/authentication.mdx +136 -0
  232. package/docs/v6/guide/code.mdx +65 -0
  233. package/docs/v6/guide/dashcam.mdx +118 -0
  234. package/docs/v6/guide/environment-variables.mdx +26 -0
  235. package/docs/v6/guide/lifecycle.mdx +242 -0
  236. package/docs/v6/guide/locating.mdx +141 -0
  237. package/docs/v6/guide/protips.mdx +43 -0
  238. package/docs/v6/guide/variables.mdx +143 -0
  239. package/docs/v6/guide/waiting.mdx +130 -0
  240. package/docs/v6/importing/csv.mdx +196 -0
  241. package/docs/v6/importing/gherkin.mdx +143 -0
  242. package/docs/v6/importing/jira.mdx +164 -0
  243. package/docs/v6/importing/testrail.mdx +162 -0
  244. package/docs/v6/integrations/electron.mdx +146 -0
  245. package/docs/v6/integrations/netlify.mdx +100 -0
  246. package/docs/v6/integrations/vercel.mdx +125 -0
  247. package/docs/v6/interactive/explore.mdx +99 -0
  248. package/docs/v6/interactive/run.mdx +52 -0
  249. package/docs/v6/interactive/save.mdx +63 -0
  250. package/docs/v6/overview/comparison.mdx +101 -0
  251. package/docs/v6/overview/faq.mdx +162 -0
  252. package/docs/v6/overview/performance.mdx +52 -0
  253. package/docs/v6/overview/quickstart.mdx +137 -0
  254. package/docs/v6/overview/what-is-testdriver.mdx +85 -0
  255. package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
  256. package/docs/v6/scenarios/cookie-banner.mdx +32 -0
  257. package/docs/v6/scenarios/file-upload.mdx +33 -0
  258. package/docs/v6/scenarios/form-filling.mdx +32 -0
  259. package/docs/v6/scenarios/log-in.mdx +75 -0
  260. package/docs/v6/scenarios/pdf-generation.mdx +25 -0
  261. package/docs/v6/scenarios/spell-check.mdx +22 -0
  262. package/docs/v6/security/action.mdx +84 -0
  263. package/docs/v6/security/agent.mdx +73 -0
  264. package/docs/v6/security/platform.mdx +77 -0
  265. package/docs/v6/tutorials/advanced-test.mdx +81 -0
  266. package/docs/v6/tutorials/basic-test.mdx +45 -0
  267. package/docs/v7/_drafts/agents.mdx +843 -0
  268. package/docs/v7/_drafts/architecture.mdx +399 -0
  269. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  270. package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -0
  271. package/docs/v7/_drafts/best-practices.mdx +486 -0
  272. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  273. package/docs/v7/_drafts/caching-selectors.mdx +424 -0
  274. package/docs/v7/_drafts/caching.mdx +366 -0
  275. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  276. package/docs/v7/_drafts/commands/assert.mdx +45 -0
  277. package/docs/v7/_drafts/commands/exec.mdx +276 -0
  278. package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
  279. package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
  280. package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
  281. package/docs/v7/_drafts/commands/if.mdx +53 -0
  282. package/docs/v7/_drafts/commands/match-image.mdx +67 -0
  283. package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
  284. package/docs/v7/_drafts/commands/remember.mdx +49 -0
  285. package/docs/v7/_drafts/commands/run.mdx +44 -0
  286. package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
  287. package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
  288. package/docs/v7/_drafts/commands/scroll.mdx +69 -0
  289. package/docs/v7/_drafts/commands/type.mdx +45 -0
  290. package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
  291. package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
  292. package/docs/v7/_drafts/commands/wait.mdx +45 -0
  293. package/docs/v7/_drafts/configuration.mdx +378 -0
  294. package/docs/v7/_drafts/contributing.mdx +174 -0
  295. package/docs/v7/_drafts/core.mdx +458 -0
  296. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  297. package/docs/v7/_drafts/debugging.mdx +349 -0
  298. package/docs/v7/_drafts/error-handling.mdx +501 -0
  299. package/docs/v7/_drafts/faq.mdx +393 -0
  300. package/docs/v7/_drafts/hooks.mdx +360 -0
  301. package/docs/v7/_drafts/init-command.mdx +95 -0
  302. package/docs/v7/_drafts/installation.mdx +420 -0
  303. package/docs/v7/_drafts/migration.mdx +562 -0
  304. package/docs/v7/_drafts/observable.mdx +604 -0
  305. package/docs/v7/_drafts/playwright.mdx +342 -0
  306. package/docs/v7/_drafts/plugin-migration.mdx +220 -0
  307. package/docs/v7/_drafts/powerful.mdx +419 -0
  308. package/docs/v7/_drafts/presets.mdx +210 -0
  309. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  310. package/docs/v7/_drafts/prompt-cache.mdx +200 -0
  311. package/docs/v7/_drafts/provision.mdx +390 -0
  312. package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
  313. package/docs/v7/_drafts/readme.mdx +135 -0
  314. package/docs/v7/_drafts/reports.mdx +414 -0
  315. package/docs/v7/_drafts/scalable.mdx +763 -0
  316. package/docs/v7/_drafts/screenshot.mdx +155 -0
  317. package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
  318. package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
  319. package/docs/v7/_drafts/sdk-migration.mdx +474 -0
  320. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  321. package/docs/v7/_drafts/self-hosting.mdx +369 -0
  322. package/docs/v7/_drafts/test-recording.mdx +382 -0
  323. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  324. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  325. package/docs/v7/_drafts/vitest.mdx +535 -0
  326. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  327. package/docs/v7/ai.mdx +205 -0
  328. package/docs/v7/assert.mdx +316 -0
  329. package/docs/v7/aws-setup.mdx +449 -0
  330. package/docs/v7/cache.mdx +223 -0
  331. package/docs/v7/caching.mdx +128 -0
  332. package/docs/v7/captcha.mdx +159 -0
  333. package/docs/v7/ci-cd.mdx +603 -0
  334. package/docs/v7/click.mdx +287 -0
  335. package/docs/v7/client.mdx +478 -0
  336. package/docs/v7/copilot/auto-healing.mdx +265 -0
  337. package/docs/v7/copilot/creating-tests.mdx +156 -0
  338. package/docs/v7/copilot/github.mdx +143 -0
  339. package/docs/v7/copilot/running-tests.mdx +149 -0
  340. package/docs/v7/copilot/setup.mdx +143 -0
  341. package/docs/v7/customizing-devices.mdx +319 -0
  342. package/docs/v7/dashcam.mdx +419 -0
  343. package/docs/v7/debugging-with-screenshots.mdx +402 -0
  344. package/docs/v7/device-config.mdx +317 -0
  345. package/docs/v7/double-click.mdx +102 -0
  346. package/docs/v7/elements.mdx +606 -0
  347. package/docs/v7/enterprise.mdx +9 -0
  348. package/docs/v7/errors.mdx +248 -0
  349. package/docs/v7/events.mdx +358 -0
  350. package/docs/v7/examples/ai.mdx +72 -0
  351. package/docs/v7/examples/assert.mdx +72 -0
  352. package/docs/v7/examples/captcha-api.mdx +92 -0
  353. package/docs/v7/examples/chrome-extension.mdx +132 -0
  354. package/docs/v7/examples/drag-and-drop.mdx +100 -0
  355. package/docs/v7/examples/element-not-found.mdx +67 -0
  356. package/docs/v7/examples/exec-output.mdx +85 -0
  357. package/docs/v7/examples/exec-pwsh.mdx +83 -0
  358. package/docs/v7/examples/focus-window.mdx +62 -0
  359. package/docs/v7/examples/hover-image.mdx +94 -0
  360. package/docs/v7/examples/hover-text.mdx +69 -0
  361. package/docs/v7/examples/installer.mdx +91 -0
  362. package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
  363. package/docs/v7/examples/match-image.mdx +96 -0
  364. package/docs/v7/examples/press-keys.mdx +92 -0
  365. package/docs/v7/examples/scroll-keyboard.mdx +79 -0
  366. package/docs/v7/examples/scroll-until-image.mdx +81 -0
  367. package/docs/v7/examples/scroll-until-text.mdx +109 -0
  368. package/docs/v7/examples/scroll.mdx +81 -0
  369. package/docs/v7/examples/type.mdx +92 -0
  370. package/docs/v7/examples/windows-installer.mdx +89 -0
  371. package/docs/v7/exec.mdx +318 -0
  372. package/docs/v7/find.mdx +830 -0
  373. package/docs/v7/focus-application.mdx +294 -0
  374. package/docs/v7/generating-tests.mdx +36 -0
  375. package/docs/v7/hosted.mdx +158 -0
  376. package/docs/v7/hover.mdx +279 -0
  377. package/docs/v7/locating-elements.mdx +71 -0
  378. package/docs/v7/making-assertions.mdx +32 -0
  379. package/docs/v7/mcp.mdx +9 -0
  380. package/docs/v7/mouse-down.mdx +161 -0
  381. package/docs/v7/mouse-up.mdx +164 -0
  382. package/docs/v7/parse.mdx +237 -0
  383. package/docs/v7/performing-actions.mdx +54 -0
  384. package/docs/v7/press-keys.mdx +349 -0
  385. package/docs/v7/provision.mdx +333 -0
  386. package/docs/v7/quickstart.mdx +173 -0
  387. package/docs/v7/redraw.mdx +216 -0
  388. package/docs/v7/reusable-code.mdx +249 -0
  389. package/docs/v7/right-click.mdx +123 -0
  390. package/docs/v7/running-tests.mdx +185 -0
  391. package/docs/v7/screenshot.mdx +249 -0
  392. package/docs/v7/screenshots.mdx +186 -0
  393. package/docs/v7/scroll.mdx +336 -0
  394. package/docs/v7/secrets.mdx +115 -0
  395. package/docs/v7/self-hosted.mdx +149 -0
  396. package/docs/v7/type.mdx +358 -0
  397. package/docs/v7/variables.mdx +111 -0
  398. package/docs/v7/wait.mdx +52 -0
  399. package/docs/v7/waiting-for-elements.mdx +90 -0
  400. package/docs/v7/what-is-testdriver.mdx +54 -0
  401. package/eslint.config.js +67 -0
  402. package/examples/ai.test.mjs +31 -0
  403. package/examples/assert.test.mjs +47 -0
  404. package/examples/chrome-extension.test.mjs +97 -0
  405. package/examples/config.mjs +5 -0
  406. package/examples/element-not-found.test.mjs +27 -0
  407. package/examples/exec-output.test.mjs +60 -0
  408. package/examples/exec-pwsh.test.mjs +58 -0
  409. package/examples/findall-coffee-icons.test.mjs +42 -0
  410. package/examples/focus-window.test.mjs +37 -0
  411. package/examples/formatted-logging.test.mjs +27 -0
  412. package/examples/hover-image.test.mjs +53 -0
  413. package/examples/hover-text-with-description.test.mjs +57 -0
  414. package/examples/hover-text.test.mjs +28 -0
  415. package/examples/installer.test.mjs +50 -0
  416. package/examples/launch-vscode-linux.test.mjs +55 -0
  417. package/examples/match-image.test.mjs +55 -0
  418. package/examples/parse.test.mjs +19 -0
  419. package/examples/press-keys.test.mjs +44 -0
  420. package/examples/prompt.test.mjs +34 -0
  421. package/examples/scroll-keyboard.test.mjs +38 -0
  422. package/examples/scroll-until-image.test.mjs +40 -0
  423. package/examples/scroll.test.mjs +42 -0
  424. package/examples/type.test.mjs +46 -0
  425. package/examples/windows-installer.test.mjs +54 -0
  426. package/index.js +2 -0
  427. package/interfaces/cli/commands/init.js +438 -0
  428. package/interfaces/cli/commands/setup.js +382 -0
  429. package/interfaces/cli/lib/base.js +285 -0
  430. package/interfaces/cli.js +20 -0
  431. package/interfaces/junit-reporter.js +290 -0
  432. package/interfaces/logger.js +388 -0
  433. package/interfaces/readline.js +234 -0
  434. package/interfaces/shared-test-state.mjs +64 -0
  435. package/interfaces/vitest-plugin.d.ts +115 -0
  436. package/interfaces/vitest-plugin.mjs +1698 -0
  437. package/lib/captcha/solver.js +358 -0
  438. package/lib/core/Dashcam.js +533 -0
  439. package/lib/core/index.d.ts +172 -0
  440. package/lib/core/index.js +12 -0
  441. package/lib/environments.json +18 -0
  442. package/lib/github-comment-formatter.js +263 -0
  443. package/lib/github-comment.mjs +452 -0
  444. package/lib/init-project.js +575 -0
  445. package/lib/presets/index.mjs +331 -0
  446. package/lib/resolve-channel.js +46 -0
  447. package/lib/sentry.js +417 -0
  448. package/lib/vitest/hooks.d.ts +57 -0
  449. package/lib/vitest/hooks.mjs +674 -0
  450. package/lib/vitest/setup-aws.mjs +247 -0
  451. package/lib/vitest/setup-self-hosted.mjs +151 -0
  452. package/lib/vitest/setup.mjs +46 -0
  453. package/manual/captcha-api.test.mjs +51 -0
  454. package/manual/drag-and-drop.test.mjs +59 -0
  455. package/manual/flake-diffthreshold-001.test.mjs +9 -0
  456. package/manual/flake-diffthreshold-01.test.mjs +9 -0
  457. package/manual/flake-diffthreshold-05.test.mjs +9 -0
  458. package/manual/flake-noredraw-cache.test.mjs +9 -0
  459. package/manual/flake-noredraw-nocache.test.mjs +9 -0
  460. package/manual/flake-redraw-cache.test.mjs +9 -0
  461. package/manual/flake-redraw-nocache.test.mjs +9 -0
  462. package/manual/flake-rocket-match.test.mjs +30 -0
  463. package/manual/flake-shared.mjs +51 -0
  464. package/manual/no-provision.test.mjs +31 -0
  465. package/manual/packer-hover-image.test.mjs +176 -0
  466. package/manual/scroll-until-text.test.mjs +68 -0
  467. package/manual/test-init-command.js +223 -0
  468. package/mcp-server/README.md +322 -0
  469. package/mcp-server/dist/codegen.d.ts +9 -0
  470. package/mcp-server/dist/codegen.js +165 -0
  471. package/mcp-server/dist/mcp-app.html +114 -0
  472. package/mcp-server/dist/package.json +1 -0
  473. package/mcp-server/dist/provision-types.d.ts +290 -0
  474. package/mcp-server/dist/provision-types.js +174 -0
  475. package/mcp-server/dist/server.d.ts +6 -0
  476. package/mcp-server/dist/server.mjs +1925 -0
  477. package/mcp-server/dist/session.d.ts +85 -0
  478. package/mcp-server/dist/session.js +152 -0
  479. package/mcp-server/mcp-app.html +28 -0
  480. package/mcp-server/mcp-config.example.json +19 -0
  481. package/mcp-server/package-lock.json +4027 -0
  482. package/mcp-server/package.json +31 -0
  483. package/mcp-server/src/codegen.ts +189 -0
  484. package/mcp-server/src/mcp-app.css +360 -0
  485. package/mcp-server/src/mcp-app.ts +547 -0
  486. package/mcp-server/src/provision-types.ts +209 -0
  487. package/mcp-server/src/server.ts +2391 -0
  488. package/mcp-server/src/session.ts +194 -0
  489. package/mcp-server/tsconfig.json +16 -0
  490. package/mcp-server/vite.config.ts +23 -0
  491. package/package.json +158 -0
  492. package/schema.json +1046 -0
  493. package/scripts/generate-skills.js +94 -0
  494. package/sdk-log-formatter.js +1157 -0
  495. package/sdk.d.ts +1486 -0
  496. package/sdk.js +4336 -0
  497. package/setup/aws/cloudformation.yaml +463 -0
  498. package/setup/aws/disable-defender.sh +42 -0
  499. package/setup/aws/install-dev-runner.sh +79 -0
  500. package/setup/aws/spawn-runner.sh +289 -0
  501. package/test/captcha-solver.test.mjs +152 -0
  502. package/test/chrome-remote-debugging.test.mjs +66 -0
  503. package/test/duckduckgo/experiment.test.mjs +28 -0
  504. package/test/duckduckgo/setup.test.mjs +29 -0
  505. package/test/manual/debug-locate-response.js +82 -0
  506. package/test/manual/reconnect-provision.test.mjs +49 -0
  507. package/test/manual/test-console-logs.test.mjs +42 -0
  508. package/test/manual/test-find-api.js +73 -0
  509. package/test/manual/test-init.sh +54 -0
  510. package/test/manual/test-prompt-cache.js +97 -0
  511. package/test/manual/test-provision-auth.mjs +22 -0
  512. package/test/manual/test-sandbox-render.js +29 -0
  513. package/test/manual/test-sdk-methods.js +15 -0
  514. package/test/manual/test-sdk-refactor.js +53 -0
  515. package/test/manual/test-stack-trace.mjs +57 -0
  516. package/test/manual/verify-element-api.js +89 -0
  517. package/test/manual/verify-types.js +0 -0
  518. package/test/manual-unawaited-promise.test.mjs +31 -0
  519. package/vitest.config.mjs +58 -0
  520. package/vitest.runner.config.mjs +33 -0
  521. package/vscode-extension/.vscodeignore +12 -0
  522. package/vscode-extension/README.md +94 -0
  523. package/vscode-extension/media/icon.png +0 -0
  524. package/vscode-extension/package-lock.json +4126 -0
  525. package/vscode-extension/package.json +86 -0
  526. package/vscode-extension/src/extension.ts +829 -0
  527. package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
  528. package/vscode-extension/tsconfig.json +16 -0
@@ -0,0 +1,1684 @@
1
+ // the actual commands to interact with the system
2
+ const { createSDK } = require("./sdk.js");
3
+ const vm = require("vm");
4
+ const theme = require("./theme.js");
5
+
6
+ const fs = require("fs").promises; // Using the promises version for async operations
7
+ const { findTemplateImage } = require("./subimage/index");
8
+ const path = require("path");
9
+ const Jimp = require("jimp");
10
+ const os = require("os");
11
+ const cliProgress = require("cli-progress");
12
+ const { createRedraw } = require("./redraw.js");
13
+
14
+ const { events } = require("../events.js");
15
+
16
+ /**
17
+ * Helper to detect if arguments are using object-based API or positional API
18
+ * @param {Array} args - The arguments passed to a command
19
+ * @param {Array<string>} knownKeys - Keys that would be present in object-based call
20
+ * @returns {boolean} True if using object-based API
21
+ */
22
+ const isObjectArgs = (args, knownKeys) => {
23
+ if (args.length === 0) return false;
24
+ if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])) {
25
+ // Check if it has at least one known key
26
+ return knownKeys.some(key => key in args[0]);
27
+ }
28
+ return false;
29
+ };
30
+
31
+ /**
32
+ * Error When a match is not found
33
+ * these should be recoverable by --heal
34
+ **/
35
+ class MatchError extends Error {
36
+ constructor(message, fatal = false) {
37
+ super(message);
38
+ this.fatal = fatal;
39
+ this.attachScreenshot = true;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Error when something is wrong with th command
45
+ **/
46
+ class CommandError extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.fatal = true;
50
+ this.attachScreenshot = false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Normalize redraw options from new thresholds format or legacy format.
56
+ * New: { enabled: true, thresholds: { screen: 0.05, network: true } }
57
+ * Legacy: { enabled: true, diffThreshold: 0.1, screenRedraw: true, networkMonitor: true }
58
+ * @param {Object} opts - Raw redraw options object
59
+ * @returns {Object} Normalised { enabled, screenRedraw, networkMonitor }
60
+ */
61
+ const normalizeRedrawOpts = (opts) => {
62
+ if (!opts || typeof opts !== 'object') return { enabled: !!opts };
63
+ const result = { enabled: opts.enabled !== false };
64
+ if (opts.thresholds && typeof opts.thresholds === 'object') {
65
+ result.screenRedraw = opts.thresholds.screen !== false;
66
+ result.networkMonitor = !!opts.thresholds.network;
67
+ } else {
68
+ result.screenRedraw = opts.screenRedraw !== undefined ? opts.screenRedraw : true;
69
+ result.networkMonitor = opts.networkMonitor !== undefined ? opts.networkMonitor : false;
70
+ }
71
+ return result;
72
+ };
73
+
74
+ /**
75
+ * Extract redraw options from command options
76
+ * @param {Object} options - Command options that may contain redraw settings
77
+ * @returns {Object} Redraw options object
78
+ */
79
+ const extractRedrawOptions = (options = {}) => {
80
+ // Support nested redraw object (new or legacy format)
81
+ if (options.redraw && typeof options.redraw === 'object') {
82
+ return normalizeRedrawOpts(options.redraw);
83
+ }
84
+
85
+ // Support flat options for convenience (legacy)
86
+ const redrawOpts = {};
87
+ if ('redrawEnabled' in options) redrawOpts.enabled = options.redrawEnabled;
88
+ if ('redrawScreenRedraw' in options) redrawOpts.screenRedraw = options.redrawScreenRedraw;
89
+ if ('redrawNetworkMonitor' in options) redrawOpts.networkMonitor = options.redrawNetworkMonitor;
90
+ if ('redrawDiffThreshold' in options) redrawOpts.screenRedraw = true;
91
+
92
+ return redrawOpts;
93
+ };
94
+
95
+ // Factory function that creates commands with the provided emitter
96
+ const createCommands = (
97
+ emitter,
98
+ system,
99
+ sandbox,
100
+ config,
101
+ sessionInstance,
102
+ getCurrentFilePath,
103
+ redrawThreshold = 0.01,
104
+ getDashcamElapsedTime = null,
105
+ getSoftAssertMode = () => false, // getter for soft assert mode (used by act())
106
+ ) => {
107
+ // Create SDK instance with emitter, config, and session
108
+ const sdk = createSDK(emitter, config, sessionInstance);
109
+ // Create redraw instance with the system - support both number and object for backward compatibility
110
+ const defaultRedrawOptions = typeof redrawThreshold === 'number'
111
+ ? { diffThreshold: redrawThreshold }
112
+ : redrawThreshold;
113
+ const redraw = createRedraw(emitter, system, sandbox, defaultRedrawOptions);
114
+
115
+ // Helper method to resolve file paths relative to the current file
116
+ const resolveRelativePath = (relativePath) => {
117
+ // If it's already an absolute path, return as-is
118
+ if (path.isAbsolute(relativePath)) {
119
+ return relativePath;
120
+ }
121
+
122
+ // Get the current file path dynamically
123
+ const currentFilePath = getCurrentFilePath();
124
+
125
+ // For relative paths, resolve relative to the current file's directory
126
+ if (currentFilePath) {
127
+ return path.resolve(path.dirname(currentFilePath), relativePath);
128
+ }
129
+
130
+ // Fallback to workingDir
131
+ return path.resolve(config.TD_WORKING_DIR || process.cwd(), relativePath);
132
+ };
133
+
134
+ const niceSeconds = (ms) => {
135
+ return Math.round(ms / 1000);
136
+ };
137
+
138
+ const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
139
+
140
+ /**
141
+ * Track an interaction via HTTP API (fire-and-forget)
142
+ * @param {Object} data - Interaction data
143
+ * @param {string} data.interactionType - Type of interaction (click, type, assert, etc.)
144
+ * @param {string} [data.prompt] - Description/prompt for the interaction
145
+ * @param {Object} [data.input] - Input data (varies by interaction type)
146
+ * @param {Object} [data.coordinates] - Coordinates {x, y}
147
+ * @param {number} data.timestamp - Absolute epoch timestamp
148
+ * @param {number} [data.duration] - Duration in ms
149
+ * @param {boolean} data.success - Whether the interaction succeeded
150
+ * @param {string} [data.error] - Error message if failed
151
+ * @param {boolean} [data.cacheHit] - Whether cache was used
152
+ * @param {string} [data.selector] - Selector ID
153
+ * @param {boolean} [data.selectorUsed] - Whether selector was used
154
+ * @param {number} [data.confidence] - AI confidence score
155
+ * @param {string} [data.reasoning] - AI reasoning
156
+ * @param {number} [data.similarity] - Cache similarity score
157
+ * @param {string} [data.screenshotUrl] - S3 key for screenshot
158
+ * @param {boolean} [data.isSecret] - Whether interaction contains sensitive data
159
+ */
160
+ const trackInteraction = (data) => {
161
+ const sessionId = sessionInstance?.get();
162
+ if (!sessionId) return;
163
+
164
+ sdk.req("/api/v7.0.0/testdriver/interaction/track", {
165
+ session: sessionId,
166
+ type: data.interactionType,
167
+ coordinates: data.coordinates,
168
+ input: data.input,
169
+ prompt: data.prompt,
170
+ selectorUsed: data.selectorUsed,
171
+ selector: data.selector,
172
+ cacheHit: data.cacheHit,
173
+ status: "completed",
174
+ success: data.success,
175
+ error: data.error,
176
+ duration: data.duration,
177
+ timestamp: data.timestamp,
178
+ isSecret: data.isSecret,
179
+ confidence: data.confidence,
180
+ reasoning: data.reasoning,
181
+ similarity: data.similarity,
182
+ screenshotUrl: data.screenshotUrl,
183
+ }).catch((err) => {
184
+ console.warn(`Failed to track ${data.interactionType} interaction:`, err.message);
185
+ });
186
+ };
187
+
188
+ const findImageOnScreen = async (
189
+ relativePath,
190
+ haystack,
191
+ restrictToWindow,
192
+ ) => {
193
+ // add .png to relative path if not already there
194
+ if (!relativePath.endsWith(".png")) {
195
+ relativePath = relativePath + ".png";
196
+ }
197
+
198
+ let needle = relativePath;
199
+
200
+ // check if the file exists
201
+ if (!fs.access(needle)) {
202
+ throw new CommandError(
203
+ `Image does not exist or do not have access: ${needle}`,
204
+ );
205
+ }
206
+
207
+ const bar1 = new cliProgress.SingleBar(
208
+ {},
209
+ cliProgress.Presets.shades_classic,
210
+ );
211
+
212
+ let thresholds = [0.9, 0.8, 0.7];
213
+
214
+ let scaleFactors = [1, 0.5, 2, 0.75, 1.25, 1.5];
215
+
216
+ let result = null;
217
+ let highestThreshold = 0;
218
+
219
+ let totalOperations = thresholds.length * scaleFactors.length;
220
+ bar1.start(totalOperations, 0);
221
+
222
+ for (let scaleFactor of scaleFactors) {
223
+ let needleSize = 1 / scaleFactor;
224
+
225
+ const scaledNeedle = await Jimp.read(path.join(needle));
226
+ scaledNeedle.scale(needleSize);
227
+
228
+ const haystackImage = await Jimp.read(haystack);
229
+
230
+ if (
231
+ scaledNeedle.bitmap.width > haystackImage.bitmap.width ||
232
+ scaledNeedle.bitmap.height > haystackImage.bitmap.height
233
+ ) {
234
+ // Needle is larger than haystack, skip this scale factor
235
+ continue;
236
+ }
237
+
238
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "scaledNeedle_"));
239
+ const scaledNeedlePath = path.join(
240
+ tempDir,
241
+ `scaledNeedle_${needleSize}.png`,
242
+ );
243
+ await scaledNeedle.writeAsync(scaledNeedlePath);
244
+
245
+ for (let threshold of thresholds) {
246
+ if (threshold >= highestThreshold) {
247
+ let results = await findTemplateImage(
248
+ haystack,
249
+ scaledNeedlePath,
250
+ threshold,
251
+ );
252
+
253
+ // throw away any results that are not within the active window
254
+ let activeWindow = await system.activeWin();
255
+
256
+ // filter out text that is not in the active window
257
+ if (restrictToWindow) {
258
+ results = results.filter((el) => {
259
+ return (
260
+ el.centerX > activeWindow.bounds.x &&
261
+ el.centerX <
262
+ activeWindow.bounds.x + activeWindow.bounds.width &&
263
+ el.centerY > activeWindow.bounds.y &&
264
+ el.centerY < activeWindow.bounds.y + activeWindow.bounds.height
265
+ );
266
+ });
267
+ }
268
+
269
+ if (results.length) {
270
+ result = { ...results[0], threshold, scaleFactor, needleSize };
271
+ highestThreshold = threshold;
272
+ break;
273
+ }
274
+ }
275
+
276
+ bar1.increment();
277
+ }
278
+ }
279
+
280
+ bar1.stop();
281
+
282
+ return result;
283
+ };
284
+
285
+ const assert = async (assertion, shouldThrow = false, options = {}) => {
286
+ // Log asserting action
287
+ const { formatter } = require("../../sdk-log-formatter.js");
288
+ const assertingMessage = formatter.formatAsserting(assertion);
289
+ emitter.emit(events.log.log, assertingMessage);
290
+
291
+ // Capture absolute timestamp at the very start of the command
292
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
293
+ const assertTimestamp = Date.now();
294
+ const assertStartTime = assertTimestamp;
295
+
296
+ // Extract cache options
297
+ const { threshold = 0.05, cacheKey, os, resolution, ai } = options;
298
+
299
+ // Debug log cache settings
300
+ emitter.emit(
301
+ events.log.debug,
302
+ `🔍 assert() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey.substring(0, 8)}...` : ""})`,
303
+ );
304
+
305
+ // Use v7 endpoint for assert with caching support
306
+ let response = await sdk.req("assert", {
307
+ expect: assertion,
308
+ image: await system.captureScreenBase64(),
309
+ threshold,
310
+ cacheKey,
311
+ os,
312
+ resolution,
313
+ ai,
314
+ });
315
+
316
+ const assertDuration = Date.now() - assertStartTime;
317
+
318
+ // Handle both old (string) and new (object) response formats
319
+ // New v7 API returns: { data: { passed, reasoning, content, cacheHit... }, cacheHit, similarity }
320
+ // Old API returns: { data: "The task passed/failed..." }
321
+ let passed;
322
+ let responseText;
323
+ let cacheHit = false;
324
+ let similarity = null;
325
+
326
+ let confidence = null;
327
+ let reasoning = null;
328
+
329
+ if (typeof response.data === 'object' && response.data !== null) {
330
+ // New structured response
331
+ passed = response.data.passed;
332
+ responseText = response.data.content || response.data.reasoning || '';
333
+ cacheHit = response.cacheHit || response.data.cacheHit || false;
334
+ similarity = response.similarity || response.data.cacheSimilarity;
335
+ confidence = response.confidence != null ? response.confidence : (response.data.confidence != null ? response.data.confidence : null);
336
+ reasoning = response.data.reasoning || null;
337
+ } else {
338
+ // Old string response (backward compatibility)
339
+ responseText = response.data || '';
340
+ passed = responseText.indexOf("The task passed") > -1;
341
+ }
342
+
343
+ // Log the result with cache info
344
+ emitter.emit(events.log.narration, formatter.formatAssertResult(passed, responseText, assertDuration, cacheHit), true);
345
+
346
+ // Track interaction with success/failure (fire-and-forget)
347
+ trackInteraction({
348
+ interactionType: "assert",
349
+ prompt: assertion,
350
+ timestamp: assertTimestamp,
351
+ duration: assertDuration,
352
+ success: passed,
353
+ error: passed ? undefined : responseText,
354
+ cacheHit: cacheHit,
355
+ confidence: confidence,
356
+ reasoning: reasoning,
357
+ similarity: similarity,
358
+ screenshotUrl: response?.screenshotKey ?? null,
359
+ });
360
+
361
+ if (passed) {
362
+ return true;
363
+ } else {
364
+ if (shouldThrow) {
365
+ // Is fatal, otherwise it just changes the assertion to be true
366
+ const errorMessage = `AI Assertion failed: ${assertion}\n${responseText}`;
367
+ throw new MatchError(errorMessage, true);
368
+ } else {
369
+ return false;
370
+ }
371
+ }
372
+ };
373
+
374
+ /**
375
+ * Scroll the screen in a direction
376
+ * @param {string} [direction='down'] - Direction to scroll ('up', 'down', 'left', 'right')
377
+ * @param {Object} [options] - Additional options
378
+ * @param {number} [options.amount=300] - Amount to scroll in pixels
379
+ * @param {Object} [options.redraw] - Redraw detection options
380
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
381
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
382
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
383
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
384
+ */
385
+ const scroll = async (direction = 'down', options = {}) => {
386
+ // Capture absolute timestamp at the very start of the command
387
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
388
+ const scrollTimestamp = Date.now();
389
+ const scrollStartTime = scrollTimestamp;
390
+ // Convert number to object format
391
+ if (typeof options === 'number') {
392
+ options = { amount: options };
393
+ }
394
+
395
+ let { amount = 300 } = options;
396
+ const redrawOptions = extractRedrawOptions(options);
397
+
398
+ await redraw.start(redrawOptions);
399
+
400
+ amount = parseInt(amount, 10);
401
+
402
+ const before = await system.captureScreenBase64();
403
+ let scrollSuccess = true;
404
+ let scrollError;
405
+ let actionEndTime;
406
+
407
+ try {
408
+ switch (direction) {
409
+ case "up":
410
+ await sandbox.send({
411
+ type: "scroll",
412
+ amount,
413
+ direction,
414
+ });
415
+ actionEndTime = Date.now();
416
+ break;
417
+ case "down":
418
+ await sandbox.send({
419
+ type: "scroll",
420
+ amount,
421
+ direction,
422
+ });
423
+ actionEndTime = Date.now();
424
+ break;
425
+ case "left":
426
+ console.error("Not Supported");
427
+ scrollSuccess = false;
428
+ scrollError = "Left scroll not supported";
429
+ break;
430
+ case "right":
431
+ console.error("Not Supported");
432
+ scrollSuccess = false;
433
+ scrollError = "Right scroll not supported";
434
+ break;
435
+ default:
436
+ scrollSuccess = false;
437
+ scrollError = "Direction not found";
438
+ throw new CommandError("Direction not found");
439
+ }
440
+
441
+ const actionDuration = actionEndTime ? actionEndTime - scrollStartTime : Date.now() - scrollStartTime;
442
+
443
+ // Log nested scroll action completion
444
+ const { formatter } = require("../../sdk-log-formatter.js");
445
+ emitter.emit(
446
+ events.log.narration,
447
+ formatter.formatScrollResult(direction, amount, actionDuration),
448
+ true,
449
+ );
450
+
451
+ // Wait for redraw and track duration
452
+ // Increase timeout for scroll operations as they can take 1-2 seconds to complete
453
+ const redrawStartTime = Date.now();
454
+ await redraw.wait(5000, redrawOptions);
455
+ const redrawDuration = Date.now() - redrawStartTime;
456
+
457
+ const after = await system.captureScreenBase64();
458
+
459
+ if (before === after) {
460
+ emitter.emit(
461
+ events.log.warn,
462
+ "Attempted to scroll, but the screen did not change. You may need to click a non-interactive element to focus the scrollable area first.",
463
+ );
464
+ }
465
+
466
+ // Log nested redraw completion
467
+ emitter.emit(
468
+ events.log.narration,
469
+ formatter.formatRedrawComplete(redrawDuration),
470
+ true,
471
+ );
472
+
473
+ // Track interaction success (fire-and-forget)
474
+ const scrollDuration = Date.now() - scrollStartTime;
475
+ trackInteraction({
476
+ interactionType: "scroll",
477
+ input: { direction, amount },
478
+ timestamp: scrollTimestamp,
479
+ duration: scrollDuration,
480
+ success: scrollSuccess,
481
+ error: scrollError,
482
+ });
483
+ } catch (error) {
484
+ // Track interaction failure (fire-and-forget)
485
+ const scrollDuration = Date.now() - scrollStartTime;
486
+ trackInteraction({
487
+ interactionType: "scroll",
488
+ input: { direction, amount },
489
+ timestamp: scrollTimestamp,
490
+ duration: scrollDuration,
491
+ success: false,
492
+ error: error.message,
493
+ });
494
+ throw error;
495
+ }
496
+ };
497
+
498
+ /**
499
+ * Perform a mouse click action
500
+ * @param {Object|number} options - Options object or x coordinate (for backward compatibility)
501
+ * @param {number} options.x - X coordinate
502
+ * @param {number} options.y - Y coordinate
503
+ * @param {string} [options.action='click'] - Click action ('click', 'right-click', 'double-click', 'hover', 'mouseDown', 'mouseUp')
504
+ * @param {string} [options.prompt] - Prompt for tracking
505
+ * @param {boolean} [options.cacheHit] - Whether cache was hit
506
+ * @param {string} [options.selector] - Selector used
507
+ * @param {boolean} [options.selectorUsed] - Whether selector was used
508
+ * @param {Object} [options.redraw] - Redraw detection options
509
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
510
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
511
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
512
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
513
+ */
514
+ const click = async (...args) => {
515
+ // Capture absolute timestamp at the very start of the command
516
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
517
+ const clickTimestamp = Date.now();
518
+ const clickStartTime = clickTimestamp;
519
+ let x, y, action, elementData, redrawOptions;
520
+
521
+ // Handle both object and positional argument styles
522
+ if (isObjectArgs(args, ['x', 'y', 'action', 'prompt', 'cacheHit', 'selector'])) {
523
+ const { x: xPos, y: yPos, action: actionArg = 'click', redraw: redrawOpts, ...rest } = args[0];
524
+ x = xPos;
525
+ y = yPos;
526
+ action = actionArg;
527
+ elementData = rest;
528
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
529
+ } else {
530
+ // Legacy positional: click(x, y, action, elementData)
531
+ [x, y, action = 'click', elementData = {}] = args;
532
+ redrawOptions = extractRedrawOptions(elementData);
533
+ }
534
+
535
+ try {
536
+ await redraw.start(redrawOptions);
537
+
538
+ let button = "left";
539
+ let double = false;
540
+
541
+ if (action === "right-click") {
542
+ button = "right";
543
+ }
544
+ if (action === "double-click") {
545
+ double = true;
546
+ }
547
+
548
+ // Show nested action details
549
+ const actionText = action.split("-").join("");
550
+ const clickActionLogStart = Date.now();
551
+
552
+ x = parseInt(x);
553
+ y = parseInt(y);
554
+
555
+ // Add absolute timestamp for sandbox events
556
+ elementData.timestamp = Date.now();
557
+
558
+ // Update the action log with duration
559
+ const clickMoveEndTime = Date.now();
560
+ const { formatter } = require("../../sdk-log-formatter.js");
561
+ emitter.emit(
562
+ events.log.narration,
563
+ formatter.formatClickResult(button, x, y, clickMoveEndTime - clickActionLogStart),
564
+ true,
565
+ );
566
+
567
+ if (action !== "hover") {
568
+ // Update timestamp for the actual click action
569
+ elementData.timestamp = Date.now();
570
+
571
+
572
+ if (action === "click" || action === "left-click") {
573
+ await sandbox.send({ type: "leftClick", x, y, ...elementData });
574
+ } else if (action === "right-click") {
575
+ await sandbox.send({ type: "rightClick", x, y, ...elementData });
576
+ } else if (action === "middle-click") {
577
+ await sandbox.send({ type: "middleClick", x, y, ...elementData });
578
+ } else if (action === "double-click") {
579
+ await sandbox.send({ type: "doubleClick", x, y, ...elementData });
580
+ } else if (action === "mouseDown") {
581
+ await sandbox.send({ type: "mousePress", button: "left", x, y, ...elementData });
582
+ } else if (action === "mouseUp") {
583
+ // Move first to create drag motion, then release
584
+ // (pyautogui.mouseUp with x/y teleports instead of dragging)
585
+ await sandbox.send({ type: "moveMouse", x, y, ...elementData });
586
+ await sandbox.send({
587
+ type: "mouseRelease",
588
+ button: "left",
589
+ ...elementData
590
+ });
591
+ }
592
+
593
+ emitter.emit(events.mouseClick, { x, y, button, click, double });
594
+
595
+ // Track action duration (before redraw wait)
596
+ const actionEndTime = Date.now();
597
+ const actionDuration = actionEndTime - clickStartTime;
598
+
599
+ // Track interaction (fire-and-forget)
600
+ if (elementData.prompt) {
601
+ trackInteraction({
602
+ interactionType: "click",
603
+ prompt: elementData.prompt,
604
+ input: { x, y, action },
605
+ timestamp: clickTimestamp,
606
+ duration: actionDuration,
607
+ success: true,
608
+ cacheHit: elementData.cacheHit,
609
+ selector: elementData.selector,
610
+ selectorUsed: elementData.selectorUsed,
611
+ confidence: elementData.confidence ?? null,
612
+ reasoning: elementData.reasoning ?? null,
613
+ similarity: elementData.similarity ?? null,
614
+ screenshotUrl: elementData.screenshotUrl ?? null,
615
+ });
616
+ }
617
+
618
+ // Wait for redraw and track duration
619
+ const redrawStartTime = Date.now();
620
+ await redraw.wait(5000, redrawOptions);
621
+ const redrawDuration = Date.now() - redrawStartTime;
622
+
623
+ // Log nested redraw completion
624
+ emitter.emit(
625
+ events.log.narration,
626
+ formatter.formatRedrawComplete(redrawDuration),
627
+ true,
628
+ );
629
+ } else {
630
+ // For hover action (within click function)
631
+ const redrawStartTime = Date.now();
632
+ await redraw.wait(5000, redrawOptions);
633
+ const redrawDuration = Date.now() - redrawStartTime;
634
+ const actionDuration = Date.now() - clickStartTime - redrawDuration;
635
+
636
+ // Log nested redraw completion
637
+ emitter.emit(
638
+ events.log.narration,
639
+ formatter.formatRedrawComplete(redrawDuration),
640
+ true,
641
+ );
642
+ }
643
+
644
+ return;
645
+ } catch (error) {
646
+ // Track interaction failure (fire-and-forget)
647
+ if (elementData.prompt) {
648
+ const clickDuration = Date.now() - clickStartTime;
649
+ trackInteraction({
650
+ interactionType: "click",
651
+ prompt: elementData.prompt,
652
+ input: { x, y, action },
653
+ timestamp: clickTimestamp,
654
+ duration: clickDuration,
655
+ success: false,
656
+ error: error.message,
657
+ cacheHit: elementData.cacheHit,
658
+ selector: elementData.selector,
659
+ selectorUsed: elementData.selectorUsed,
660
+ confidence: elementData.confidence ?? null,
661
+ reasoning: elementData.reasoning ?? null,
662
+ similarity: elementData.similarity ?? null,
663
+ });
664
+ }
665
+ throw error;
666
+ }
667
+ };
668
+
669
+ /**
670
+ * Hover at coordinates
671
+ * @param {Object|number} options - Options object or x coordinate (for backward compatibility)
672
+ * @param {number} options.x - X coordinate
673
+ * @param {number} options.y - Y coordinate
674
+ * @param {string} [options.prompt] - Prompt for tracking
675
+ * @param {boolean} [options.cacheHit] - Whether cache was hit
676
+ * @param {string} [options.selector] - Selector used
677
+ * @param {boolean} [options.selectorUsed] - Whether selector was used
678
+ */
679
+ const hover = async (...args) => {
680
+ // Capture absolute timestamp at the very start of the command
681
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
682
+ const hoverTimestamp = Date.now();
683
+ const hoverStartTime = hoverTimestamp;
684
+ let x, y, elementData, redrawOptions;
685
+
686
+ // Handle both object and positional argument styles
687
+ if (isObjectArgs(args, ['x', 'y', 'prompt', 'cacheHit', 'selector'])) {
688
+ const { x: xPos, y: yPos, redraw: redrawOpts, ...rest } = args[0];
689
+ x = xPos;
690
+ y = yPos;
691
+ elementData = rest;
692
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
693
+ } else {
694
+ // Legacy positional: hover(x, y, elementData)
695
+ [x, y, elementData = {}] = args;
696
+ redrawOptions = extractRedrawOptions(elementData);
697
+ }
698
+
699
+ try {
700
+ emitter.emit(events.log.narration, theme.dim(`hovering at ${x}, ${y}...`));
701
+
702
+ await redraw.start(redrawOptions);
703
+
704
+ x = parseInt(x);
705
+ y = parseInt(y);
706
+
707
+ // Add absolute timestamp for sandbox events
708
+ elementData.timestamp = Date.now();
709
+
710
+ await sandbox.send({ type: "moveMouse", x, y, ...elementData });
711
+
712
+ // Track interaction (fire-and-forget)
713
+ const actionEndTime = Date.now();
714
+ const actionDuration = actionEndTime - hoverStartTime;
715
+
716
+ if (elementData.prompt) {
717
+ trackInteraction({
718
+ interactionType: "hover",
719
+ prompt: elementData.prompt,
720
+ input: { x, y },
721
+ timestamp: hoverTimestamp,
722
+ duration: actionDuration,
723
+ success: true,
724
+ cacheHit: elementData.cacheHit,
725
+ selector: elementData.selector,
726
+ selectorUsed: elementData.selectorUsed,
727
+ confidence: elementData.confidence ?? null,
728
+ reasoning: elementData.reasoning ?? null,
729
+ similarity: elementData.similarity ?? null,
730
+ screenshotUrl: elementData.screenshotUrl ?? null,
731
+ });
732
+ }
733
+
734
+ // Wait for redraw and track duration
735
+ const redrawStartTime = Date.now();
736
+ await redraw.wait(2500, redrawOptions);
737
+ const redrawDuration = Date.now() - redrawStartTime;
738
+
739
+ // Log action completion with separate durations
740
+ const { formatter } = require("../../sdk-log-formatter.js");
741
+ const completionMessage = formatter.formatActionComplete("hover", elementData.prompt, {
742
+ actionDuration,
743
+ redrawDuration,
744
+ cacheHit: elementData.cacheHit,
745
+ });
746
+ emitter.emit(events.log.log, completionMessage);
747
+
748
+ return;
749
+ } catch (error) {
750
+ // Track interaction failure (fire-and-forget)
751
+ if (elementData.prompt) {
752
+ const hoverDuration = Date.now() - hoverStartTime;
753
+ trackInteraction({
754
+ interactionType: "hover",
755
+ prompt: elementData.prompt,
756
+ input: { x, y },
757
+ timestamp: hoverTimestamp,
758
+ duration: hoverDuration,
759
+ success: false,
760
+ error: error.message,
761
+ cacheHit: elementData.cacheHit,
762
+ selector: elementData.selector,
763
+ selectorUsed: elementData.selectorUsed,
764
+ confidence: elementData.confidence ?? null,
765
+ reasoning: elementData.reasoning ?? null,
766
+ similarity: elementData.similarity ?? null,
767
+ screenshotUrl: elementData.screenshotUrl ?? null,
768
+ });
769
+ }
770
+ throw error;
771
+ }
772
+ };
773
+
774
+ let commands = {
775
+ scroll: scroll,
776
+ click: click,
777
+ hover: hover,
778
+ /**
779
+ * Hover over text on screen
780
+ * @param {Object|string} options - Options object or description (for backward compatibility)
781
+ * @param {string} options.description - Description of the element to find
782
+ * @param {string} [options.action='click'] - Action to perform
783
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
784
+ */
785
+ "hover-text": async (...args) => {
786
+ let description, text, action, timeout;
787
+
788
+ // Handle both object and positional argument styles
789
+ if (isObjectArgs(args, ['description', 'text', 'action', 'timeout'])) {
790
+ ({ description, text, action = 'click', timeout = 5000 } = args[0]);
791
+ } else {
792
+ // Legacy positional: hoverText(description, action, timeout)
793
+ [description, action = 'click', timeout = 5000] = args;
794
+ }
795
+
796
+ // Use text if provided, otherwise fall back to description
797
+ // This handles both the new spec (text + description) and legacy usage (just description)
798
+ description = text || description;
799
+
800
+ if (!description) {
801
+ throw new CommandError("hover-text requires either a text or description parameter");
802
+ }
803
+
804
+ description = description.toString();
805
+
806
+ emitter.emit(
807
+ events.log.narration,
808
+ theme.dim(`searching for "${description}"...`),
809
+ );
810
+
811
+ // wait for the text to appear on screen
812
+ await commands["wait-for-text"]({ text: description, timeout });
813
+
814
+ emitter.emit(events.log.narration, theme.dim("thinking..."), true);
815
+
816
+ let response = await sdk.req("find", {
817
+ element: description,
818
+ image: await system.captureScreenBase64(),
819
+ });
820
+
821
+ if (!response || !response.coordinates) {
822
+ throw new MatchError("No text on screen matches description");
823
+ }
824
+
825
+ // Perform the action using the located coordinates
826
+ if (action === "hover") {
827
+ await commands.hover({ x: response.coordinates.x, y: response.coordinates.y });
828
+ } else {
829
+ await click({ x: response.coordinates.x, y: response.coordinates.y, action });
830
+ }
831
+
832
+ return response;
833
+ },
834
+ /**
835
+ * Hover over an image on screen
836
+ * @param {Object|string} options - Options object or description (for backward compatibility)
837
+ * @param {string} options.description - Description of the image to find
838
+ * @param {string} [options.action='click'] - Action to perform
839
+ */
840
+ "hover-image": async (...args) => {
841
+ let description, action;
842
+
843
+ // Handle both object and positional argument styles
844
+ if (isObjectArgs(args, ['description', 'action'])) {
845
+ ({ description, action = 'click' } = args[0]);
846
+ } else {
847
+ // Legacy positional: hoverImage(description, action)
848
+ [description, action = 'click'] = args;
849
+ }
850
+
851
+ emitter.emit(
852
+ events.log.narration,
853
+ theme.dim(`searching for image: "${description}"...`),
854
+ );
855
+
856
+ let response = await sdk.req("find", {
857
+ element: description,
858
+ image: await system.captureScreenBase64(),
859
+ });
860
+
861
+ if (!response || !response.coordinates) {
862
+ throw new MatchError("No image or icon on screen matches description");
863
+ }
864
+
865
+ // Perform the action using the located coordinates
866
+ if (action === "hover") {
867
+ await commands.hover({ x: response.coordinates.x, y: response.coordinates.y });
868
+ } else {
869
+ await click({ x: response.coordinates.x, y: response.coordinates.y, action });
870
+ }
871
+
872
+ return response;
873
+ },
874
+ /**
875
+ * Match and interact with an image template
876
+ * @param {Object|string} options - Options object or path (for backward compatibility)
877
+ * @param {string} options.path - Path to the image template
878
+ * @param {string} [options.action='click'] - Action to perform
879
+ * @param {boolean} [options.invert=false] - Invert the match
880
+ */
881
+ "match-image": async (...args) => {
882
+ let relativePath, action, invert;
883
+
884
+ // Handle both object and positional argument styles
885
+ if (isObjectArgs(args, ['path', 'action', 'invert'])) {
886
+ ({ path: relativePath, action = 'click', invert = false } = args[0]);
887
+ } else {
888
+ // Legacy positional: matchImage(relativePath, action, invert)
889
+ [relativePath, action = 'click', invert = false] = args;
890
+ }
891
+
892
+ emitter.emit(
893
+ events.log.narration,
894
+ theme.dim(`${action} on image template "${relativePath}"...`),
895
+ );
896
+
897
+ // Resolve the image path relative to the current file
898
+ const resolvedPath = resolveRelativePath(relativePath);
899
+
900
+ let image = await system.captureScreenPNG();
901
+
902
+ let result = await findImageOnScreen(resolvedPath, image);
903
+
904
+ if (invert) {
905
+ result = !result;
906
+ }
907
+
908
+ if (!result) {
909
+ throw new CommandError(`Image not found: ${resolvedPath}`);
910
+ } else {
911
+ if (action === "click") {
912
+ await click({ x: result.centerX, y: result.centerY, action });
913
+ } else if (action === "hover") {
914
+ await hover({ x: result.centerX, y: result.centerY });
915
+ }
916
+ }
917
+
918
+ return true;
919
+ },
920
+ /**
921
+ * Type text
922
+ * @param {string|number} text - Text to type
923
+ * @param {Object} [options] - Additional options
924
+ * @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
925
+ * @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
926
+ * @param {Object} [options.redraw] - Redraw detection options
927
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
928
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
929
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
930
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
931
+ */
932
+ "type": async (text, options = {}) => {
933
+ const { formatter } = require("../../sdk-log-formatter.js");
934
+ // Capture absolute timestamp at the very start of the command
935
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
936
+ const typeTimestamp = Date.now();
937
+ const typeStartTime = typeTimestamp;
938
+ const { delay = 250, secret = false, redraw: redrawOpts, ...elementData } = options;
939
+ const redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...options });
940
+
941
+ // Validate that text parameter is provided
942
+ if (text === undefined || text === null) {
943
+ throw new CommandError("type() requires a text parameter. Received: " + text);
944
+ }
945
+
946
+ // Log parent action with text
947
+ if (secret) {
948
+ emitter.emit(events.log.narration, formatter.getPrefix("type") + " " + theme.yellow.bold("Type") + " " + theme.dim(`secret "****"`));
949
+ } else {
950
+ emitter.emit(events.log.narration, formatter.getPrefix("type") + " " + theme.yellow.bold("Type") + " " + theme.cyan(`"${text}"`));
951
+ }
952
+
953
+ await redraw.start(redrawOptions);
954
+
955
+ text = text.toString();
956
+
957
+ // Add absolute timestamp for sandbox events
958
+ elementData.timestamp = Date.now();
959
+
960
+ // Actually type the text in the sandbox
961
+ await sandbox.send({ type: "write", text, delay, ...elementData });
962
+
963
+ // Update the action log with duration
964
+ const typeActionEndTime = Date.now();
965
+ emitter.emit(events.log.narration, formatter.formatTypeResult(text, secret, typeActionEndTime - typeStartTime), true);
966
+
967
+ // Track interaction (fire-and-forget)
968
+ const typeDuration = Date.now() - typeStartTime;
969
+ trackInteraction({
970
+ interactionType: "type",
971
+ // Store masked text if secret, otherwise store actual text
972
+ input: { text: secret ? "****" : text, delay },
973
+ timestamp: typeTimestamp,
974
+ duration: typeDuration,
975
+ success: true,
976
+ isSecret: secret, // Flag this interaction if it contains a secret
977
+ });
978
+
979
+ const redrawStartTime = Date.now();
980
+ await redraw.wait(5000, redrawOptions);
981
+ const redrawDuration = Date.now() - redrawStartTime;
982
+
983
+ // Log nested redraw completion
984
+ emitter.emit(
985
+ events.log.narration,
986
+ formatter.formatRedrawComplete(redrawDuration),
987
+ true,
988
+ );
989
+
990
+ return;
991
+ },
992
+ /**
993
+ * Press keyboard keys
994
+ * @param {Array} keys - Array of keys to press
995
+ * @param {Object} [options] - Additional options
996
+ * @param {Object} [options.redraw] - Redraw detection options
997
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
998
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
999
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
1000
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
1001
+ */
1002
+ "press-keys": async (keys, options = {}) => {
1003
+ const { formatter } = require("../../sdk-log-formatter.js");
1004
+ // Capture absolute timestamp at the very start of the command
1005
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1006
+ const pressKeysTimestamp = Date.now();
1007
+ const pressKeysStartTime = pressKeysTimestamp;
1008
+ const redrawOptions = extractRedrawOptions(options);
1009
+ const keysDisplay = Array.isArray(keys) ? keys.join(", ") : keys;
1010
+
1011
+ // Log parent action
1012
+ emitter.emit(
1013
+ events.log.narration,
1014
+ formatter.getPrefix("pressKeys") + " " + theme.yellow.bold("PressKeys") + " " + theme.cyan(`${keysDisplay}`),
1015
+ );
1016
+
1017
+ await redraw.start(redrawOptions);
1018
+
1019
+ // Log nested action details
1020
+ const pressKeysActionLogStart = Date.now();
1021
+
1022
+ // finally, press the keys
1023
+ await sandbox.send({ type: "press", keys });
1024
+
1025
+ // Update the action log with duration
1026
+ const pressKeysActionEndTime = Date.now();
1027
+ emitter.emit(
1028
+ events.log.narration,
1029
+ formatter.formatPressKeysResult(keysDisplay, pressKeysActionEndTime - pressKeysActionLogStart),
1030
+ true,
1031
+ );
1032
+
1033
+ // Track interaction (fire-and-forget)
1034
+ const pressKeysDuration = Date.now() - pressKeysStartTime;
1035
+ trackInteraction({
1036
+ interactionType: "pressKeys",
1037
+ input: { keys },
1038
+ timestamp: pressKeysTimestamp,
1039
+ duration: pressKeysDuration,
1040
+ success: true,
1041
+ });
1042
+
1043
+ const redrawStartTime = Date.now();
1044
+ await redraw.wait(5000, redrawOptions);
1045
+ const redrawDuration = Date.now() - redrawStartTime;
1046
+
1047
+ // Log nested redraw completion
1048
+ emitter.emit(
1049
+ events.log.narration,
1050
+ formatter.formatRedrawComplete(redrawDuration),
1051
+ true,
1052
+ );
1053
+
1054
+ return;
1055
+ },
1056
+ /**
1057
+ * Wait for specified time
1058
+ * @param {number} [timeout=3000] - Time to wait in milliseconds
1059
+ * @param {Object} [options] - Additional options (reserved for future use)
1060
+ */
1061
+ "wait": async (timeout = 3000, options = {}) => {
1062
+ // Capture absolute timestamp at the very start of the command
1063
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1064
+ const waitTimestamp = Date.now();
1065
+ const waitStartTime = waitTimestamp;
1066
+ emitter.emit(events.log.narration, theme.dim(`waiting ${timeout}ms...`));
1067
+ const result = await delay(timeout);
1068
+
1069
+ // Track interaction (fire-and-forget)
1070
+ const waitDuration = Date.now() - waitStartTime;
1071
+ trackInteraction({
1072
+ interactionType: "wait",
1073
+ input: { timeout },
1074
+ timestamp: waitTimestamp,
1075
+ duration: waitDuration,
1076
+ success: true,
1077
+ });
1078
+
1079
+ return result;
1080
+ },
1081
+ /**
1082
+ * Wait for image to appear on screen
1083
+ * @param {Object|string} options - Options object or description (for backward compatibility)
1084
+ * @param {string} options.description - Description of the image
1085
+ * @param {number} [options.timeout=10000] - Timeout in milliseconds
1086
+ */
1087
+ "wait-for-image": async (...args) => {
1088
+ // Capture absolute timestamp at the very start of the command
1089
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1090
+ const waitForImageTimestamp = Date.now();
1091
+ let description, timeout;
1092
+
1093
+ // Handle both object and positional argument styles
1094
+ if (isObjectArgs(args, ['description', 'timeout'])) {
1095
+ ({ description, timeout = 10000 } = args[0]);
1096
+ } else {
1097
+ // Legacy positional: waitForImage(description, timeout)
1098
+ [description, timeout = 10000] = args;
1099
+ }
1100
+
1101
+ emitter.emit(
1102
+ events.log.narration,
1103
+ theme.dim(
1104
+ `waiting for an image matching description "${description}"...`,
1105
+ ),
1106
+ true,
1107
+ );
1108
+
1109
+ let startTime = new Date().getTime();
1110
+ let durationPassed = 0;
1111
+ let passed = false;
1112
+
1113
+ while (durationPassed < timeout && !passed) {
1114
+ passed = await assert(
1115
+ `An image matching the description "${description}" appears on screen.`,
1116
+ false,
1117
+ );
1118
+
1119
+ durationPassed = new Date().getTime() - startTime;
1120
+ if (!passed) {
1121
+ emitter.emit(
1122
+ events.log.narration,
1123
+ theme.dim(
1124
+ `${niceSeconds(durationPassed)} seconds have passed without finding an image matching the description "${description}"`,
1125
+ ),
1126
+ true,
1127
+ );
1128
+ await delay(2500);
1129
+ }
1130
+ }
1131
+
1132
+ if (passed) {
1133
+ emitter.emit(
1134
+ events.log.narration,
1135
+ theme.dim(
1136
+ `An image matching the description \"${description}\" found!`,
1137
+ ),
1138
+ true,
1139
+ );
1140
+
1141
+ // Track interaction success (fire-and-forget)
1142
+ const waitForImageDuration = Date.now() - startTime;
1143
+ trackInteraction({
1144
+ interactionType: "waitForImage",
1145
+ prompt: description,
1146
+ input: { timeout },
1147
+ timestamp: waitForImageTimestamp,
1148
+ duration: waitForImageDuration,
1149
+ success: true,
1150
+ });
1151
+
1152
+ return;
1153
+ } else {
1154
+ // Track interaction failure (fire-and-forget)
1155
+ const errorMsg = `Timed out (${niceSeconds(timeout)} seconds) while searching for an image matching the description \"${description}\"`;
1156
+ const waitForImageDuration = Date.now() - startTime;
1157
+ trackInteraction({
1158
+ interactionType: "waitForImage",
1159
+ prompt: description,
1160
+ input: { timeout },
1161
+ timestamp: waitForImageTimestamp,
1162
+ duration: waitForImageDuration,
1163
+ success: false,
1164
+ error: errorMsg,
1165
+ });
1166
+
1167
+ throw new MatchError(errorMsg);
1168
+ }
1169
+ },
1170
+ /**
1171
+ * Wait for text to appear on screen
1172
+ * @param {Object|string} options - Options object or text (for backward compatibility)
1173
+ * @param {string} options.text - Text to wait for
1174
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
1175
+ * @param {Object} [options.redraw] - Redraw detection options
1176
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
1177
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
1178
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
1179
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
1180
+ */
1181
+ "wait-for-text": async (...args) => {
1182
+ // Capture absolute timestamp at the very start of the command
1183
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1184
+ const waitForTextTimestamp = Date.now();
1185
+ let text, timeout, redrawOptions;
1186
+
1187
+ // Handle both object and positional argument styles
1188
+ if (isObjectArgs(args, ['text', 'timeout'])) {
1189
+ const { redraw: redrawOpts, ...rest } = args[0];
1190
+ ({ text, timeout = 5000 } = rest);
1191
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
1192
+ } else {
1193
+ // Legacy positional: waitForText(text, timeout)
1194
+ [text, timeout = 5000] = args;
1195
+ redrawOptions = {};
1196
+ }
1197
+
1198
+ await redraw.start(redrawOptions);
1199
+
1200
+ emitter.emit(
1201
+ events.log.narration,
1202
+ theme.dim(`waiting for text: "${text}"...`),
1203
+ true,
1204
+ );
1205
+
1206
+ let startTime = new Date().getTime();
1207
+ let durationPassed = 0;
1208
+
1209
+ let passed = false;
1210
+
1211
+ while (durationPassed < timeout && !passed) {
1212
+ const response = await sdk.req("find", {
1213
+ element: text,
1214
+ image: await system.captureScreenBase64(),
1215
+ });
1216
+
1217
+ passed = !!(response && response.coordinates);
1218
+
1219
+ durationPassed = new Date().getTime() - startTime;
1220
+
1221
+ if (!passed) {
1222
+ emitter.emit(
1223
+ events.log.narration,
1224
+ theme.dim(
1225
+ `${niceSeconds(durationPassed)} seconds have passed without finding "${text}"`,
1226
+ ),
1227
+ true,
1228
+ );
1229
+ await delay(2500);
1230
+ }
1231
+ }
1232
+
1233
+ if (passed) {
1234
+ emitter.emit(events.log.narration, theme.dim(`"${text}" found!`), true);
1235
+
1236
+ // Track interaction success (fire-and-forget)
1237
+ const waitForTextDuration = Date.now() - startTime;
1238
+ trackInteraction({
1239
+ interactionType: "waitForText",
1240
+ prompt: text,
1241
+ input: { timeout },
1242
+ timestamp: waitForTextTimestamp,
1243
+ duration: waitForTextDuration,
1244
+ success: true,
1245
+ });
1246
+
1247
+ return;
1248
+ } else {
1249
+ // Track interaction failure (fire-and-forget)
1250
+ const errorMsg = `Timed out (${niceSeconds(timeout)} seconds) while searching for "${text}"`;
1251
+ const waitForTextDuration = Date.now() - startTime;
1252
+ trackInteraction({
1253
+ interactionType: "waitForText",
1254
+ prompt: text,
1255
+ input: { timeout },
1256
+ timestamp: waitForTextTimestamp,
1257
+ duration: waitForTextDuration,
1258
+ success: false,
1259
+ error: errorMsg,
1260
+ });
1261
+
1262
+ throw new MatchError(errorMsg);
1263
+ }
1264
+ },
1265
+ /**
1266
+ * Scroll until text is found
1267
+ * @param {Object|string} options - Options object or text (for backward compatibility)
1268
+ * @param {string} options.text - Text to find
1269
+ * @param {string} [options.direction='down'] - Scroll direction
1270
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
1271
+ * @param {boolean} [options.invert=false] - Invert the match
1272
+ * @param {Object} [options.redraw] - Redraw detection options
1273
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
1274
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
1275
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
1276
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
1277
+ */
1278
+ "scroll-until-text": async (...args) => {
1279
+ let text, direction, maxDistance, invert, redrawOptions;
1280
+
1281
+ // Handle both object and positional argument styles
1282
+ if (isObjectArgs(args, ['text', 'direction', 'maxDistance', 'invert'])) {
1283
+ const { redraw: redrawOpts, ...rest } = args[0];
1284
+ ({ text, direction = 'down', maxDistance = 10000, invert = false } = rest);
1285
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
1286
+ } else {
1287
+ // Legacy positional: scrollUntilText(text, direction, maxDistance, invert)
1288
+ [text, direction = 'down', maxDistance = 10000, invert = false] = args;
1289
+ redrawOptions = {};
1290
+ }
1291
+
1292
+ await redraw.start(redrawOptions);
1293
+
1294
+ emitter.emit(
1295
+ events.log.narration,
1296
+ theme.dim(`scrolling for text: "${text}"...`),
1297
+ true,
1298
+ );
1299
+
1300
+ let scrollDistance = 0;
1301
+ let incrementDistance = 500;
1302
+ let passed = false;
1303
+
1304
+ while (scrollDistance <= maxDistance && !passed) {
1305
+ const response = await sdk.req("find", {
1306
+ element: text,
1307
+ image: await system.captureScreenBase64(),
1308
+ });
1309
+
1310
+ passed = !!(response && response.coordinates);
1311
+
1312
+ if (invert) {
1313
+ passed = !passed;
1314
+ }
1315
+
1316
+ if (!passed) {
1317
+ emitter.emit(
1318
+ events.log.narration,
1319
+ theme.dim(
1320
+ `scrolling ${direction} ${incrementDistance}px. ${scrollDistance + incrementDistance}/${maxDistance}px scrolled...`,
1321
+ ),
1322
+ true,
1323
+ );
1324
+ await scroll(direction, { amount: incrementDistance });
1325
+ scrollDistance = scrollDistance + incrementDistance;
1326
+ }
1327
+ }
1328
+
1329
+ if (passed) {
1330
+ emitter.emit(events.log.narration, theme.dim(`"${text}" found!`), true);
1331
+ return;
1332
+ } else {
1333
+ throw new MatchError(
1334
+ `Scrolled ${scrollDistance} pixels without finding "${text}"`,
1335
+ );
1336
+ }
1337
+ },
1338
+ /**
1339
+ * Scroll until image is found
1340
+ * @param {Object|string} options - Options object or description (for backward compatibility)
1341
+ * @param {string} [options.description] - Description of the image
1342
+ * @param {string} [options.direction='down'] - Scroll direction
1343
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
1344
+ * @param {string} [options.method='mouse'] - Scroll method
1345
+ * @param {string} [options.path] - Path to image template
1346
+ * @param {boolean} [options.invert=false] - Invert the match
1347
+ */
1348
+ "scroll-until-image": async (...args) => {
1349
+ let description, direction, maxDistance, method, imagePath, invert;
1350
+
1351
+ // Handle both object and positional argument styles
1352
+ if (isObjectArgs(args, ['description', 'direction', 'maxDistance', 'method', 'path', 'invert'])) {
1353
+ ({ description, direction = 'down', maxDistance = 10000, method = 'mouse', path: imagePath, invert = false } = args[0]);
1354
+ } else {
1355
+ // Legacy positional: scrollUntilImage(description, direction, maxDistance, method, path, invert)
1356
+ [description, direction = 'down', maxDistance = 10000, method = 'mouse', imagePath, invert = false] = args;
1357
+ }
1358
+
1359
+ const needle = description || imagePath;
1360
+
1361
+ if (!needle) {
1362
+ throw new CommandError("No description or path provided");
1363
+ }
1364
+
1365
+ if (description && imagePath) {
1366
+ throw new CommandError(
1367
+ "Only one of description or path can be provided",
1368
+ );
1369
+ }
1370
+
1371
+ emitter.emit(
1372
+ events.log.narration,
1373
+ theme.dim(`scrolling for an image matching "${needle}"...`),
1374
+ true,
1375
+ );
1376
+
1377
+ let scrollDistance = 0;
1378
+ let incrementDistance = 500;
1379
+ let passed = false;
1380
+
1381
+ while (scrollDistance <= maxDistance && !passed) {
1382
+ if (description) {
1383
+ passed = await assert(
1384
+ `An image matching the description "${description}" appears on screen.`,
1385
+ false,
1386
+ false,
1387
+ invert,
1388
+ );
1389
+ }
1390
+
1391
+ if (imagePath) {
1392
+ // Don't throw if not found. We only want to know if it's found or not.
1393
+ passed = await commands["match-image"]({ path: imagePath }).catch(
1394
+ console.warn,
1395
+ );
1396
+ }
1397
+
1398
+ if (!passed) {
1399
+ emitter.emit(
1400
+ events.log.narration,
1401
+ theme.dim(`scrolling ${direction} ${incrementDistance} pixels...`),
1402
+ true,
1403
+ );
1404
+ await scroll(direction, { amount: incrementDistance });
1405
+ scrollDistance = scrollDistance + incrementDistance;
1406
+ }
1407
+ }
1408
+
1409
+ if (passed) {
1410
+ emitter.emit(
1411
+ events.log.narration,
1412
+ theme.dim(`"${needle}" found!`),
1413
+ true,
1414
+ );
1415
+ return;
1416
+ } else {
1417
+ throw new CommandError(
1418
+ `Scrolled ${scrollDistance} pixels without finding an image matching "${needle}"`,
1419
+ );
1420
+ }
1421
+ },
1422
+ /**
1423
+ * Focus an application by name
1424
+ * @param {string} name - Application name
1425
+ * @param {Object} [options] - Additional options
1426
+ * @param {Object} [options.redraw] - Redraw detection options
1427
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
1428
+ * @param {Object} [options.redraw.thresholds] - Threshold configuration
1429
+ * @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
1430
+ * @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
1431
+ */
1432
+ "focus-application": async (name, options = {}) => {
1433
+ const redrawOptions = extractRedrawOptions(options);
1434
+ await redraw.start(redrawOptions);
1435
+
1436
+ await sandbox.send({
1437
+ type: "commands.focus-application",
1438
+ name,
1439
+ });
1440
+ await redraw.wait(1000, redrawOptions);
1441
+ return "The application was focused.";
1442
+ },
1443
+ /**
1444
+ * Extract information from the screen using AI
1445
+ * @param {Object|string} options - Options object or description (for backward compatibility)
1446
+ * @param {string} options.description - What to extract
1447
+ */
1448
+ "extract": async (...args) => {
1449
+ // Capture absolute timestamp at the very start of the command
1450
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1451
+ const rememberTimestamp = Date.now();
1452
+ const rememberStartTime = rememberTimestamp;
1453
+ let description;
1454
+
1455
+ // Handle both object and positional argument styles
1456
+ if (isObjectArgs(args, ['description'])) {
1457
+ ({ description } = args[0]);
1458
+ } else {
1459
+ // Legacy positional: remember(description)
1460
+ [description] = args;
1461
+ }
1462
+
1463
+ try {
1464
+ let result = await sdk.req("remember", {
1465
+ image: await system.captureScreenBase64(),
1466
+ description,
1467
+ });
1468
+
1469
+ // Track interaction success
1470
+ const rememberDuration = Date.now() - rememberStartTime;
1471
+ trackInteraction({
1472
+ interactionType: "extract",
1473
+ prompt: description,
1474
+ timestamp: rememberTimestamp,
1475
+ duration: rememberDuration,
1476
+ success: true,
1477
+ });
1478
+
1479
+ return result.data;
1480
+ } catch (error) {
1481
+ // Track interaction failure
1482
+ const rememberDuration = Date.now() - rememberStartTime;
1483
+ trackInteraction({
1484
+ interactionType: "extract",
1485
+ prompt: description,
1486
+ timestamp: rememberTimestamp,
1487
+ duration: rememberDuration,
1488
+ success: false,
1489
+ error: error.message,
1490
+ });
1491
+ throw error;
1492
+ }
1493
+ },
1494
+ /**
1495
+ * Make an AI-powered assertion
1496
+ * @param {string} assertion - Assertion to check
1497
+ * @param {Object} [options] - Additional options
1498
+ * @param {number} [options.threshold] - Cache threshold (0-1). Lower values require closer matches. Set to -1 to disable cache.
1499
+ * @param {string} [options.cacheKey] - Cache key for grouping cached assertions (enables caching when provided)
1500
+ * @param {string} [options.os] - Operating system identifier for cache partitioning
1501
+ * @param {string} [options.resolution] - Screen resolution for cache partitioning
1502
+ */
1503
+ "assert": async (assertion, options = {}) => {
1504
+ // In soft assert mode (during act()), don't throw on failure
1505
+ const shouldThrow = !getSoftAssertMode();
1506
+ let response = await assert(assertion, shouldThrow, options);
1507
+
1508
+ return response;
1509
+ },
1510
+ /**
1511
+ * Execute code in the sandbox
1512
+ * @param {Object|string} options - Options object or language (for backward compatibility)
1513
+ * @param {string} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
1514
+ * @param {string} options.code - Code to execute
1515
+ * @param {number} [options.timeout] - Timeout in milliseconds
1516
+ * @param {boolean} [options.silent=false] - Suppress output
1517
+ */
1518
+ "exec": async (...args) => {
1519
+ const { formatter } = require("../../sdk-log-formatter.js");
1520
+ let language, code, timeout, silent;
1521
+
1522
+ // Handle both object and positional argument styles
1523
+ if (isObjectArgs(args, ['language', 'code', 'timeout', 'silent'])) {
1524
+ ({ language = 'pwsh', code, timeout, silent = false } = args[0]);
1525
+ } else {
1526
+ // Legacy positional: exec(language, code, timeout, silent)
1527
+ [language = 'pwsh', code, timeout, silent = false] = args;
1528
+ }
1529
+
1530
+ // Log parent action
1531
+ emitter.emit(events.log.narration, formatter.getPrefix("action") + " " + theme.cyan.bold("Exec") + " " + theme.magenta(`[${language}]`), true);
1532
+
1533
+ // Log nested command details (truncate to first line)
1534
+ const firstLine = code.split('\n')[0];
1535
+ const codeDisplay = code.includes('\n') ? firstLine + '...' : firstLine;
1536
+ emitter.emit(events.log.log, formatter.formatCodeLine(codeDisplay));
1537
+
1538
+ let plat = system.platform();
1539
+
1540
+ if (language == "pwsh" || language == "sh") {
1541
+ if (language === "pwsh" && sandbox.os === "linux") {
1542
+ emitter.emit(
1543
+ events.log.log,
1544
+ theme.yellow(
1545
+ `⚠️ Warning: You are using 'pwsh' exec command on a Linux sandbox. This may fail. Consider using 'bash' or 'sh' for Linux environments.`,
1546
+ ),
1547
+ true,
1548
+ );
1549
+ }
1550
+
1551
+ if (language === "sh" && sandbox.os === "windows") {
1552
+ emitter.emit(
1553
+ events.log.log,
1554
+ theme.yellow(
1555
+ `⚠️ Warning: You are using 'sh' exec command on a Windows sandbox. This will fail. Automatically switching to 'pwsh' for Windows environments.`,
1556
+ ),
1557
+ true,
1558
+ );
1559
+ // Automatically switch to pwsh for Windows
1560
+ language = "pwsh";
1561
+ }
1562
+
1563
+ const execActionLogStart = Date.now();
1564
+
1565
+ let result = null;
1566
+
1567
+ const execTimeout = timeout || 300000;
1568
+ result = await sandbox.send({
1569
+ type: "commands.run",
1570
+ command: code,
1571
+ timeout: execTimeout,
1572
+ }, execTimeout);
1573
+
1574
+ const execActionEndTime = Date.now();
1575
+ const execDuration = execActionEndTime - execActionLogStart;
1576
+
1577
+ // const debugMode = process.env.VERBOSE || process.env.TD_DEBUG;
1578
+ // if (debugMode) {
1579
+ // console.log(result);
1580
+ // }
1581
+
1582
+ if (result.out && result.out.returncode !== 0) {
1583
+ emitter.emit(
1584
+ events.log.narration,
1585
+ formatter.formatExecComplete(result.out.returncode, execDuration),
1586
+ true,
1587
+ );
1588
+ throw new MatchError(
1589
+ `Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,
1590
+ );
1591
+ } else {
1592
+ emitter.emit(
1593
+ events.log.narration,
1594
+ formatter.formatExecComplete(0, execDuration),
1595
+ true,
1596
+ );
1597
+
1598
+ if (!silent && result.out?.stdout) {
1599
+ emitter.emit(events.log.log, theme.dim(` stdout:`), true);
1600
+ emitter.emit(events.log.log, theme.dim(` ${result.out.stdout}`), true);
1601
+ }
1602
+
1603
+ if (!silent && result.out.stderr) {
1604
+ emitter.emit(events.log.log, theme.dim(` stderr:`), true);
1605
+ emitter.emit(events.log.log, theme.dim(` ${result.out.stderr}`), true);
1606
+ }
1607
+
1608
+ return result.out?.stdout?.trim();
1609
+ }
1610
+ } else if (language == "js") {
1611
+ emitter.emit(events.log.narration, theme.dim(`running js...`), true);
1612
+
1613
+ emitter.emit(
1614
+ events.log.narration,
1615
+ theme.dim(`running value of \`${plat}\` in local JS vm...`),
1616
+ true,
1617
+ );
1618
+
1619
+ emitter.emit(events.log.log, "");
1620
+ emitter.emit(events.log.log, "------");
1621
+
1622
+ const context = vm.createContext({
1623
+ require,
1624
+ console,
1625
+ fs,
1626
+ process,
1627
+ fetch,
1628
+ });
1629
+
1630
+ let scriptCode = "(async function() {\n" + code + "\n})();";
1631
+
1632
+ const script = new vm.Script(scriptCode);
1633
+
1634
+ try {
1635
+ await script.runInNewContext(context);
1636
+ } catch (e) {
1637
+ // Log the error to the emitter instead of console.error to maintain consistency
1638
+ emitter.emit(
1639
+ events.log.debug,
1640
+ `JavaScript execution error: ${e.message}`,
1641
+ );
1642
+ // Wait a tick to allow any promise rejections to be handled
1643
+ throw new CommandError(`Error running script: ${e.message}`);
1644
+ }
1645
+
1646
+ // wait for context.result to resolve
1647
+ let stepResult = await context.result;
1648
+
1649
+ // conver it to string
1650
+ if (typeof stepResult === "object") {
1651
+ stepResult = JSON.stringify(stepResult, null, 2);
1652
+ } else if (typeof stepResult === "function") {
1653
+ stepResult = stepResult.toString();
1654
+ }
1655
+
1656
+ emitter.emit(events.log.log, "------");
1657
+ emitter.emit(events.log.log, "");
1658
+
1659
+ if (!stepResult) {
1660
+ emitter.emit(events.log.log, `No result returned from script`, true);
1661
+ } else {
1662
+ /* The above JavaScript code is checking if the variable `silent` is falsy (not true) and if
1663
+ so, it emits log events using an emitter. The emitted log events include the
1664
+ theme.dim(`Result:`) and the value of the `stepResult` variable. */
1665
+ // if (!silent) {
1666
+ // emitter.emit(events.log.log, theme.dim(`Result:`), true);
1667
+ // emitter.emit(events.log.log, stepResult, true);
1668
+ // }
1669
+ }
1670
+
1671
+ return stepResult;
1672
+ // }
1673
+ } else {
1674
+ throw new CommandError(`Language not supported: ${language}`);
1675
+ }
1676
+ },
1677
+ };
1678
+
1679
+ // Return the commands, assert function, and redraw instance
1680
+ return { commands, assert, redraw };
1681
+ };
1682
+
1683
+ // Export the factory function
1684
+ module.exports = { createCommands };