testdriverai 6.2.2 → 7.1.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 (300) hide show
  1. package/.github/workflows/acceptance-linux.yml +75 -0
  2. package/.github/workflows/acceptance-sdk-tests.yml +133 -0
  3. package/.vscode/settings.json +5 -1
  4. package/AGENTS.md +550 -0
  5. package/CODEOWNERS +0 -1
  6. package/README.md +126 -0
  7. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  8. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  9. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  10. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  11. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  12. package/_testdriver/lifecycle/prerun.yaml +15 -0
  13. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  14. package/agent/index.js +300 -85
  15. package/agent/interface.js +15 -0
  16. package/agent/lib/cache.js +142 -0
  17. package/agent/lib/commander.js +1 -39
  18. package/agent/lib/commands.js +910 -296
  19. package/agent/lib/redraw.js +129 -41
  20. package/agent/lib/sandbox.js +29 -6
  21. package/agent/lib/sdk.js +22 -0
  22. package/agent/lib/system.js +0 -3
  23. package/agent/lib/validation.js +1 -7
  24. package/debug-locate-response.js +82 -0
  25. package/debugger/index.html +15 -4
  26. package/docs/ARCHITECTURE.md +424 -0
  27. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  28. package/docs/MIGRATION.md +425 -0
  29. package/docs/PRESETS.md +210 -0
  30. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  31. package/docs/SDK_AWESOME_LOGS.md +468 -0
  32. package/docs/TEST_RECORDING.md +388 -0
  33. package/docs/docs.json +286 -152
  34. package/docs/guide/best-practices-polling.mdx +154 -0
  35. package/docs/sdk-browser-rendering.md +167 -0
  36. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  37. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  38. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  39. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  40. package/docs/v6/overview/comparison.mdx +101 -0
  41. package/docs/v7/README.md +135 -0
  42. package/docs/v7/api/ai.mdx +205 -0
  43. package/docs/v7/api/assert.mdx +285 -0
  44. package/docs/v7/api/assertions.mdx +403 -0
  45. package/docs/v7/api/click.mdx +287 -0
  46. package/docs/v7/api/client.mdx +322 -0
  47. package/docs/v7/api/dashcam.mdx +497 -0
  48. package/docs/v7/api/doubleClick.mdx +102 -0
  49. package/docs/v7/api/elements.mdx +479 -0
  50. package/docs/v7/api/exec.mdx +346 -0
  51. package/docs/v7/api/find.mdx +316 -0
  52. package/docs/v7/api/focusApplication.mdx +294 -0
  53. package/docs/v7/api/hover.mdx +279 -0
  54. package/docs/v7/api/mouseDown.mdx +161 -0
  55. package/docs/v7/api/mouseUp.mdx +164 -0
  56. package/docs/v7/api/pressKeys.mdx +349 -0
  57. package/docs/v7/api/rightClick.mdx +123 -0
  58. package/docs/v7/api/sandbox.mdx +404 -0
  59. package/docs/v7/api/scroll.mdx +300 -0
  60. package/docs/v7/api/type.mdx +314 -0
  61. package/docs/v7/commands/assert.mdx +45 -0
  62. package/docs/v7/commands/exec.mdx +282 -0
  63. package/docs/v7/commands/focus-application.mdx +44 -0
  64. package/docs/v7/commands/hover-image.mdx +69 -0
  65. package/docs/v7/commands/hover-text.mdx +47 -0
  66. package/docs/v7/commands/if.mdx +53 -0
  67. package/docs/v7/commands/match-image.mdx +67 -0
  68. package/docs/v7/commands/press-keys.mdx +87 -0
  69. package/docs/v7/commands/remember.mdx +49 -0
  70. package/docs/v7/commands/run.mdx +44 -0
  71. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  72. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  73. package/docs/v7/commands/scroll.mdx +69 -0
  74. package/docs/v7/commands/type.mdx +45 -0
  75. package/docs/v7/commands/wait-for-image.mdx +54 -0
  76. package/docs/v7/commands/wait-for-text.mdx +48 -0
  77. package/docs/v7/commands/wait.mdx +45 -0
  78. package/docs/v7/getting-started/configuration.mdx +380 -0
  79. package/docs/v7/getting-started/quickstart.mdx +332 -0
  80. package/docs/v7/guides/best-practices.mdx +486 -0
  81. package/docs/v7/guides/caching-ai.mdx +215 -0
  82. package/docs/v7/guides/caching-selectors.mdx +292 -0
  83. package/docs/v7/guides/caching.mdx +366 -0
  84. package/docs/v7/guides/ci-cd/azure.mdx +587 -0
  85. package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
  86. package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
  87. package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
  88. package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
  89. package/docs/v7/guides/ci-cd/travis.mdx +438 -0
  90. package/docs/v7/guides/debugging.mdx +349 -0
  91. package/docs/v7/guides/faq.mdx +393 -0
  92. package/docs/v7/guides/migration.mdx +562 -0
  93. package/docs/v7/guides/performance.mdx +517 -0
  94. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  95. package/docs/v7/guides/troubleshooting.mdx +526 -0
  96. package/docs/v7/guides/vitest-plugin.mdx +477 -0
  97. package/docs/v7/guides/vitest.mdx +535 -0
  98. package/docs/v7/platforms/linux.mdx +308 -0
  99. package/docs/v7/platforms/macos.mdx +433 -0
  100. package/docs/v7/platforms/windows.mdx +430 -0
  101. package/docs/v7/playwright.mdx +342 -0
  102. package/docs/v7/presets/chrome-extension.mdx +223 -0
  103. package/docs/v7/presets/chrome.mdx +287 -0
  104. package/docs/v7/presets/electron.mdx +435 -0
  105. package/docs/v7/presets/vscode.mdx +398 -0
  106. package/docs/v7/presets/webapp.mdx +396 -0
  107. package/docs/v7/progressive-apis/CORE.md +459 -0
  108. package/docs/v7/progressive-apis/HOOKS.md +360 -0
  109. package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
  110. package/docs/v7/progressive-apis/PROVISION.md +266 -0
  111. package/eslint.config.js +19 -1
  112. package/interfaces/cli/lib/base.js +10 -4
  113. package/interfaces/logger.js +2 -1
  114. package/interfaces/shared-test-state.mjs +69 -0
  115. package/interfaces/vitest-plugin.mjs +830 -0
  116. package/package.json +29 -5
  117. package/schema.json +8 -29
  118. package/scripts/view-test-results.mjs +96 -0
  119. package/sdk-log-formatter.js +714 -0
  120. package/sdk.d.ts +1028 -0
  121. package/sdk.js +2567 -0
  122. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  123. package/setup/aws/cloudformation.yaml +9 -2
  124. package/src/core/Dashcam.js +469 -0
  125. package/src/core/index.d.ts +150 -0
  126. package/src/core/index.js +12 -0
  127. package/src/presets/index.mjs +331 -0
  128. package/src/vitest/extended.mjs +108 -0
  129. package/src/vitest/hooks.d.ts +119 -0
  130. package/src/vitest/hooks.mjs +298 -0
  131. package/src/vitest/index.mjs +64 -0
  132. package/src/vitest/lifecycle.mjs +277 -0
  133. package/src/vitest/utils.mjs +150 -0
  134. package/test/dashcam.test.js +137 -0
  135. package/test/mcp-example-test.yaml +27 -0
  136. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  137. package/testdriver/acceptance-sdk/README.md +128 -0
  138. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  139. package/testdriver/acceptance-sdk/assert.test.mjs +26 -0
  140. package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
  141. package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
  142. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +58 -0
  143. package/testdriver/acceptance-sdk/element-not-found.test.mjs +25 -0
  144. package/testdriver/acceptance-sdk/exec-js.test.mjs +43 -0
  145. package/testdriver/acceptance-sdk/exec-output.test.mjs +59 -0
  146. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +57 -0
  147. package/testdriver/acceptance-sdk/focus-window.test.mjs +36 -0
  148. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +26 -0
  149. package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
  150. package/testdriver/acceptance-sdk/hover-image.test.mjs +34 -0
  151. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +38 -0
  152. package/testdriver/acceptance-sdk/hover-text.test.mjs +27 -0
  153. package/testdriver/acceptance-sdk/match-image.test.mjs +36 -0
  154. package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
  155. package/testdriver/acceptance-sdk/press-keys.test.mjs +50 -0
  156. package/testdriver/acceptance-sdk/prompt.test.mjs +33 -0
  157. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +38 -0
  158. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +39 -0
  159. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +28 -0
  160. package/testdriver/acceptance-sdk/scroll.test.mjs +41 -0
  161. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  162. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +420 -0
  163. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  164. package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
  165. package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
  166. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  167. package/testdriver/acceptance-sdk/type.test.mjs +45 -0
  168. package/verify-element-api.js +89 -0
  169. package/verify-types.js +0 -0
  170. package/vitest.config.example.js +19 -0
  171. package/vitest.config.mjs +66 -0
  172. package/vitest.config.mjs.bak +44 -0
  173. package/.github/workflows/acceptance-v6.yml +0 -169
  174. package/.vscode/mcp.json +0 -9
  175. package/docs/overview/comparison.mdx +0 -82
  176. package/testdriver/lifecycle/prerun.yaml +0 -17
  177. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  178. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  179. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  180. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  181. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  182. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  183. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  184. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  185. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  186. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  187. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  188. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  189. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  190. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  191. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  192. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  193. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  194. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  195. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  196. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  197. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  198. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  199. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  200. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  201. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  202. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  203. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  204. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  205. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  206. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  207. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  208. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  209. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  210. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  211. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  212. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  213. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  214. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  215. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  216. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  217. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  218. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  219. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  220. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  221. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  222. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  223. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  224. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  225. /package/docs/{account → v6/account}/projects.mdx +0 -0
  226. /package/docs/{account → v6/account}/team.mdx +0 -0
  227. /package/docs/{action → v6/action}/ami.mdx +0 -0
  228. /package/docs/{action → v6/action}/performance.mdx +0 -0
  229. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  230. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  231. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  232. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  233. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  234. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  235. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  236. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  237. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  238. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  239. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  240. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  241. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  242. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  243. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  244. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  245. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  246. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  247. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  248. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  249. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  250. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  251. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  252. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  253. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  254. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  255. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  256. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  257. /package/docs/{features → v6/features}/generation.mdx +0 -0
  258. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  259. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  260. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  261. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  262. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  263. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  264. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  265. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  266. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  267. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  268. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  269. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  270. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  271. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  272. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  273. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  274. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  275. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  276. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  277. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  278. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  279. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  280. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  281. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  282. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  283. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  284. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  285. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  286. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  287. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  288. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  289. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  290. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  291. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  292. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  293. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  294. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  295. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  296. /package/docs/{security → v6/security}/action.mdx +0 -0
  297. /package/docs/{security → v6/security}/agent.mdx +0 -0
  298. /package/docs/{security → v6/security}/platform.mdx +0 -0
  299. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  300. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
@@ -13,6 +13,21 @@ const { createRedraw } = require("./redraw.js");
13
13
 
14
14
  const { events } = require("../events.js");
15
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
+
16
31
  /**
17
32
  * Error When a match is not found
18
33
  * these should be recoverable by --heal
@@ -36,6 +51,28 @@ class CommandError extends Error {
36
51
  }
37
52
  }
38
53
 
54
+ /**
55
+ * Extract redraw options from command options
56
+ * @param {Object} options - Command options that may contain redraw settings
57
+ * @returns {Object} Redraw options object
58
+ */
59
+ const extractRedrawOptions = (options = {}) => {
60
+ const redrawOpts = {};
61
+
62
+ // Support nested redraw object: { redraw: { enabled: false, diffThreshold: 0.5 } }
63
+ if (options.redraw && typeof options.redraw === 'object') {
64
+ return options.redraw;
65
+ }
66
+
67
+ // Support flat options for convenience
68
+ if ('redrawEnabled' in options) redrawOpts.enabled = options.redrawEnabled;
69
+ if ('redrawScreenRedraw' in options) redrawOpts.screenRedraw = options.redrawScreenRedraw;
70
+ if ('redrawNetworkMonitor' in options) redrawOpts.networkMonitor = options.redrawNetworkMonitor;
71
+ if ('redrawDiffThreshold' in options) redrawOpts.diffThreshold = options.redrawDiffThreshold;
72
+
73
+ return redrawOpts;
74
+ };
75
+
39
76
  // Factory function that creates commands with the provided emitter
40
77
  const createCommands = (
41
78
  emitter,
@@ -44,11 +81,16 @@ const createCommands = (
44
81
  config,
45
82
  sessionInstance,
46
83
  getCurrentFilePath,
84
+ redrawThreshold = 0.1,
85
+ getDashcamElapsedTime = null,
47
86
  ) => {
48
87
  // Create SDK instance with emitter, config, and session
49
88
  const sdk = createSDK(emitter, config, sessionInstance);
50
- // Create redraw instance with the system
51
- const redraw = createRedraw(emitter, system, sandbox);
89
+ // Create redraw instance with the system - support both number and object for backward compatibility
90
+ const defaultRedrawOptions = typeof redrawThreshold === 'number'
91
+ ? { diffThreshold: redrawThreshold }
92
+ : redrawThreshold;
93
+ const redraw = createRedraw(emitter, system, sandbox, defaultRedrawOptions);
52
94
 
53
95
  // Helper method to resolve file paths relative to the current file
54
96
  const resolveRelativePath = (relativePath) => {
@@ -73,6 +115,7 @@ const createCommands = (
73
115
  return Math.round(ms / 1000);
74
116
  };
75
117
  const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
118
+
76
119
  const findImageOnScreen = async (
77
120
  relativePath,
78
121
  haystack,
@@ -170,104 +213,105 @@ const createCommands = (
170
213
  return result;
171
214
  };
172
215
 
173
- const assert = async (
174
- assertion,
175
- shouldThrow = false,
176
- async = false,
177
- invert = false,
178
- ) => {
179
- if (async) {
180
- shouldThrow = true;
181
- }
182
-
216
+ const assert = async (assertion, shouldThrow = false) => {
183
217
  const handleAssertResponse = (response) => {
184
218
  emitter.emit(events.log.log, response);
185
219
 
186
220
  let valid = response.indexOf("The task passed") > -1;
187
221
 
188
- if (invert) {
189
- valid = !valid;
190
- }
191
-
192
222
  if (valid) {
193
223
  return true;
194
224
  } else {
195
225
  if (shouldThrow) {
196
- // Is fatal, othewise it just changes the assertion to be true
197
- throw new MatchError(
198
- `AI Assertion failed ${invert && "(Inverted)"}`,
199
- true,
200
- );
226
+ // Is fatal, otherwise it just changes the assertion to be true
227
+ const errorMessage = `AI Assertion failed: ${assertion}\n${response}`;
228
+ throw new MatchError(errorMessage, true);
201
229
  } else {
202
230
  return false;
203
231
  }
204
232
  }
205
233
  };
206
234
 
235
+ // Log asserting action
236
+ const { formatter } = require("../../sdk-log-formatter.js");
237
+ const assertingMessage = formatter.formatAsserting(assertion);
238
+ emitter.emit(events.log.log, assertingMessage);
239
+
207
240
  emitter.emit(events.log.narration, `thinking...`);
208
241
 
209
- if (async) {
210
- await sdk
211
- .req("assert", {
212
- expect: assertion,
213
- image: await system.captureScreenBase64(),
214
- })
215
- .then((response) => {
216
- return handleAssertResponse(response.data);
242
+ const assertStartTime = Date.now();
243
+ let response = await sdk.req("assert", {
244
+ expect: assertion,
245
+ image: await system.captureScreenBase64(),
246
+ });
247
+ const assertDuration = Date.now() - assertStartTime;
248
+
249
+ // Determine if assertion passed or failed
250
+ const assertionPassed = response.data.indexOf("The task passed") > -1;
251
+
252
+ // Track interaction with success/failure
253
+ const sessionId = sessionInstance?.get();
254
+ if (sessionId) {
255
+ try {
256
+ await sandbox.send({
257
+ type: "trackInteraction",
258
+ interactionType: "assert",
259
+ session: sessionId,
260
+ prompt: assertion,
261
+ timestamp: assertStartTime,
262
+ duration: assertDuration,
263
+ success: assertionPassed,
264
+ error: assertionPassed ? undefined : response.data,
217
265
  });
218
-
219
- return true;
220
- } else {
221
- let response = await sdk.req("assert", {
222
- expect: assertion,
223
- image: await system.captureScreenBase64(),
224
- });
225
- return handleAssertResponse(response.data);
266
+ } catch (err) {
267
+ console.warn("Failed to track assert interaction:", err.message);
268
+ }
226
269
  }
270
+
271
+ return handleAssertResponse(response.data);
227
272
  };
228
- const scroll = async (direction = "down", amount = 300, method = "mouse") => {
229
- await redraw.start();
230
273
 
231
- amount = parseInt(amount, 10);
274
+ /**
275
+ * Scroll the screen in a direction
276
+ * @param {string} [direction='down'] - Direction to scroll ('up', 'down', 'left', 'right')
277
+ * @param {Object} [options] - Additional options
278
+ * @param {number} [options.amount=300] - Amount to scroll in pixels
279
+ * @param {Object} [options.redraw] - Redraw detection options
280
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
281
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
282
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
283
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
284
+ */
285
+ const scroll = async (direction = 'down', options = {}) => {
286
+ let { amount = 300 } = options;
287
+ const redrawOptions = extractRedrawOptions(options);
288
+
289
+ emitter.emit(
290
+ events.log.narration,
291
+ theme.dim(`scrolling ${direction} ${amount}px...`),
292
+ );
232
293
 
233
- // if direction is down, amount should be negative
234
- if (direction === "down") {
235
- amount = -Math.abs(amount);
236
- } else if (direction === "up") {
237
- amount = Math.abs(amount);
238
- }
294
+ await redraw.start(redrawOptions);
295
+
296
+ amount = parseInt(amount, 10);
239
297
 
240
298
  const before = await system.captureScreenBase64();
241
299
  switch (direction) {
242
300
  case "up":
243
- if (method === "mouse") {
244
- await sandbox.send({
245
- os: "linux",
246
- type: "scroll",
247
- amount,
248
- direction,
249
- });
250
- } else {
251
- await sandbox.send({ os: "linux", type: "press", keys: ["pageup"] });
252
- }
253
- await redraw.wait(2500);
301
+ await sandbox.send({
302
+ type: "scroll",
303
+ amount,
304
+ direction,
305
+ });
306
+ await redraw.wait(2500, redrawOptions);
254
307
  break;
255
308
  case "down":
256
- if (method === "mouse") {
257
- await sandbox.send({
258
- os: "linux",
259
- type: "scroll",
260
- amount,
261
- direction,
262
- });
263
- } else {
264
- await sandbox.send({
265
- os: "linux",
266
- type: "press",
267
- keys: ["pagedown"],
268
- });
269
- }
270
- await redraw.wait(2500);
309
+ await sandbox.send({
310
+ type: "scroll",
311
+ amount,
312
+ direction,
313
+ });
314
+ await redraw.wait(2500, redrawOptions);
271
315
  break;
272
316
  case "left":
273
317
  console.error("Not Supported");
@@ -288,150 +332,387 @@ const createCommands = (
288
332
  }
289
333
  };
290
334
 
291
- // perform a mouse click
292
- // click, right-click, double-click, hover
293
- const click = async (x, y, action = "click") => {
294
- await redraw.start();
335
+ /**
336
+ * Perform a mouse click action
337
+ * @param {Object|number} options - Options object or x coordinate (for backward compatibility)
338
+ * @param {number} options.x - X coordinate
339
+ * @param {number} options.y - Y coordinate
340
+ * @param {string} [options.action='click'] - Click action ('click', 'right-click', 'double-click', 'hover', 'mouseDown', 'mouseUp')
341
+ * @param {string} [options.prompt] - Prompt for tracking
342
+ * @param {boolean} [options.cacheHit] - Whether cache was hit
343
+ * @param {string} [options.selector] - Selector used
344
+ * @param {boolean} [options.selectorUsed] - Whether selector was used
345
+ * @param {Object} [options.redraw] - Redraw detection options
346
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
347
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
348
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
349
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
350
+ */
351
+ const click = async (...args) => {
352
+ const clickStartTime = Date.now();
353
+ let x, y, action, elementData, redrawOptions;
354
+
355
+ // Handle both object and positional argument styles
356
+ if (isObjectArgs(args, ['x', 'y', 'action', 'prompt', 'cacheHit', 'selector'])) {
357
+ const { x: xPos, y: yPos, action: actionArg = 'click', redraw: redrawOpts, ...rest } = args[0];
358
+ x = xPos;
359
+ y = yPos;
360
+ action = actionArg;
361
+ elementData = rest;
362
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
363
+ } else {
364
+ // Legacy positional: click(x, y, action, elementData)
365
+ [x, y, action = 'click', elementData = {}] = args;
366
+ redrawOptions = extractRedrawOptions(elementData);
367
+ }
368
+
369
+ try {
370
+ await redraw.start(redrawOptions);
295
371
 
296
- let button = "left";
297
- let double = false;
372
+ let button = "left";
373
+ let double = false;
298
374
 
299
- if (action === "right-click") {
300
- button = "right";
301
- }
302
- if (action === "double-click") {
303
- double = true;
304
- }
375
+ if (action === "right-click") {
376
+ button = "right";
377
+ }
378
+ if (action === "double-click") {
379
+ double = true;
380
+ }
305
381
 
306
- emitter.emit(
307
- events.log.narration,
308
- theme.dim(`${action} ${button} clicking at ${x}, ${y}...`),
309
- true,
310
- );
382
+ emitter.emit(
383
+ events.log.narration,
384
+ theme.dim(`${action} ${button} clicking at ${x}, ${y}...`),
385
+ true,
386
+ );
311
387
 
312
- x = parseInt(x);
313
- y = parseInt(y);
388
+ x = parseInt(x);
389
+ y = parseInt(y);
314
390
 
315
- await sandbox.send({ os: "linux", type: "moveMouse", x, y });
391
+ // Add dashcam timestamp if available
392
+ if (getDashcamElapsedTime) {
393
+ const elapsed = getDashcamElapsedTime();
394
+ if (elapsed !== null) {
395
+ elementData.timestamp = elapsed;
396
+ }
397
+ }
316
398
 
317
- emitter.emit(events.mouseMove, { x, y });
399
+ await sandbox.send({ type: "moveMouse", x, y, ...elementData });
318
400
 
319
- await delay(2500); // wait for the mouse to move
401
+ emitter.emit(events.mouseMove, { x, y });
320
402
 
321
- if (action !== "hover") {
322
- if (action === "click" || action === "left-click") {
323
- await sandbox.send({ os: "linux", type: "leftClick" });
324
- } else if (action === "right-click") {
325
- await sandbox.send({ os: "linux", type: "rightClick" });
326
- } else if (action === "middle-click") {
327
- await sandbox.send({ os: "linux", type: "middleClick" });
328
- } else if (action === "double-click") {
329
- await sandbox.send({ os: "linux", type: "doubleClick" });
330
- } else if (action === "drag-start") {
331
- await sandbox.send({ os: "linux", type: "mousePress", button: "left" });
332
- } else if (action === "drag-end") {
333
- await sandbox.send({
334
- os: "linux",
335
- type: "mouseRelease",
336
- button: "left",
337
- });
403
+ await delay(2500); // wait for the mouse to move
404
+
405
+ if (action !== "hover") {
406
+ // Update timestamp for the actual click action
407
+ if (getDashcamElapsedTime) {
408
+ const elapsed = getDashcamElapsedTime();
409
+ if (elapsed !== null) {
410
+ elementData.timestamp = elapsed;
411
+ }
412
+ }
413
+
414
+ if (action === "click" || action === "left-click") {
415
+ await sandbox.send({ type: "leftClick", x, y, ...elementData });
416
+ } else if (action === "right-click") {
417
+ await sandbox.send({ type: "rightClick", x, y, ...elementData });
418
+ } else if (action === "middle-click") {
419
+ await sandbox.send({ type: "middleClick", x, y, ...elementData });
420
+ } else if (action === "double-click") {
421
+ await sandbox.send({ type: "doubleClick", x, y, ...elementData });
422
+ } else if (action === "mouseDown") {
423
+ await sandbox.send({ type: "mousePress", button: "left", x, y, ...elementData });
424
+ } else if (action === "mouseUp") {
425
+ await sandbox.send({
426
+ type: "mouseRelease",
427
+ button: "left",
428
+ x,
429
+ y,
430
+ ...elementData
431
+ });
432
+ }
433
+
434
+ emitter.emit(events.mouseClick, { x, y, button, click, double });
435
+
436
+ // Track interaction
437
+ const sessionId = sessionInstance?.get();
438
+ if (sessionId && elementData.prompt) {
439
+ try {
440
+ const clickDuration = Date.now() - clickStartTime;
441
+ await sandbox.send({
442
+ type: "trackInteraction",
443
+ interactionType: "click",
444
+ session: sessionId,
445
+ prompt: elementData.prompt,
446
+ input: { x, y, action },
447
+ timestamp: clickStartTime,
448
+ duration: clickDuration,
449
+ success: true,
450
+ cacheHit: elementData.cacheHit,
451
+ selector: elementData.selector,
452
+ selectorUsed: elementData.selectorUsed,
453
+ });
454
+ } catch (err) {
455
+ console.warn("Failed to track click interaction:", err.message);
456
+ }
457
+ }
338
458
  }
339
459
 
340
- emitter.emit(events.mouseClick, { x, y, button, click, double });
460
+ await redraw.wait(5000, redrawOptions);
461
+
462
+ return;
463
+ } catch (error) {
464
+ // Track interaction failure
465
+ const sessionId = sessionInstance?.get();
466
+ if (sessionId && elementData.prompt) {
467
+ try {
468
+ const clickDuration = Date.now() - clickStartTime;
469
+ await sandbox.send({
470
+ type: "trackInteraction",
471
+ interactionType: "click",
472
+ session: sessionId,
473
+ prompt: elementData.prompt,
474
+ input: { x, y, action },
475
+ timestamp: clickStartTime,
476
+ duration: clickDuration,
477
+ success: false,
478
+ error: error.message,
479
+ cacheHit: elementData.cacheHit,
480
+ selector: elementData.selector,
481
+ selectorUsed: elementData.selectorUsed,
482
+ });
483
+ } catch (err) {
484
+ console.warn("Failed to track click interaction:", err.message);
485
+ }
486
+ }
487
+ throw error;
341
488
  }
489
+ };
342
490
 
343
- await redraw.wait(5000);
491
+ /**
492
+ * Hover at coordinates
493
+ * @param {Object|number} options - Options object or x coordinate (for backward compatibility)
494
+ * @param {number} options.x - X coordinate
495
+ * @param {number} options.y - Y coordinate
496
+ * @param {string} [options.prompt] - Prompt for tracking
497
+ * @param {boolean} [options.cacheHit] - Whether cache was hit
498
+ * @param {string} [options.selector] - Selector used
499
+ * @param {boolean} [options.selectorUsed] - Whether selector was used
500
+ */
501
+ const hover = async (...args) => {
502
+ const hoverStartTime = Date.now();
503
+ let x, y, elementData, redrawOptions;
504
+
505
+ // Handle both object and positional argument styles
506
+ if (isObjectArgs(args, ['x', 'y', 'prompt', 'cacheHit', 'selector'])) {
507
+ const { x: xPos, y: yPos, redraw: redrawOpts, ...rest } = args[0];
508
+ x = xPos;
509
+ y = yPos;
510
+ elementData = rest;
511
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
512
+ } else {
513
+ // Legacy positional: hover(x, y, elementData)
514
+ [x, y, elementData = {}] = args;
515
+ redrawOptions = extractRedrawOptions(elementData);
516
+ }
517
+
518
+ try {
519
+ emitter.emit(events.log.narration, theme.dim(`hovering at ${x}, ${y}...`));
344
520
 
345
- return;
346
- };
521
+ await redraw.start(redrawOptions);
347
522
 
348
- const hover = async (x, y) => {
349
- await redraw.start();
523
+ x = parseInt(x);
524
+ y = parseInt(y);
350
525
 
351
- x = parseInt(x);
352
- y = parseInt(y);
526
+ // Add dashcam timestamp if available
527
+ if (getDashcamElapsedTime) {
528
+ const elapsed = getDashcamElapsedTime();
529
+ if (elapsed !== null) {
530
+ elementData.timestamp = elapsed;
531
+ }
532
+ }
533
+
534
+ await sandbox.send({ type: "moveMouse", x, y, ...elementData });
353
535
 
354
- await sandbox.send({ os: "linux", type: "moveMouse", x, y });
536
+ // Track interaction
537
+ const sessionId = sessionInstance?.get();
538
+ if (sessionId && elementData.prompt) {
539
+ try {
540
+ const hoverDuration = Date.now() - hoverStartTime;
541
+ await sandbox.send({
542
+ type: "trackInteraction",
543
+ interactionType: "hover",
544
+ session: sessionId,
545
+ prompt: elementData.prompt,
546
+ input: { x, y },
547
+ timestamp: hoverStartTime,
548
+ duration: hoverDuration,
549
+ success: true,
550
+ cacheHit: elementData.cacheHit,
551
+ selector: elementData.selector,
552
+ selectorUsed: elementData.selectorUsed,
553
+ });
554
+ } catch (err) {
555
+ console.warn("Failed to track hover interaction:", err.message);
556
+ }
557
+ }
355
558
 
356
- await redraw.wait(2500);
559
+ await redraw.wait(2500, redrawOptions);
357
560
 
358
- return;
561
+ return;
562
+ } catch (error) {
563
+ // Track interaction failure
564
+ const sessionId = sessionInstance?.get();
565
+ if (sessionId && elementData.prompt) {
566
+ try {
567
+ const hoverDuration = Date.now() - hoverStartTime;
568
+ await sandbox.send({
569
+ type: "trackInteraction",
570
+ interactionType: "hover",
571
+ session: sessionId,
572
+ prompt: elementData.prompt,
573
+ input: { x, y },
574
+ timestamp: hoverStartTime,
575
+ duration: hoverDuration,
576
+ success: false,
577
+ error: error.message,
578
+ cacheHit: elementData.cacheHit,
579
+ selector: elementData.selector,
580
+ selectorUsed: elementData.selectorUsed,
581
+ });
582
+ } catch (err) {
583
+ console.warn("Failed to track hover interaction:", err.message);
584
+ }
585
+ }
586
+ throw error;
587
+ }
359
588
  };
360
589
 
361
590
  let commands = {
362
591
  scroll: scroll,
363
592
  click: click,
364
593
  hover: hover,
365
- // method, levenshein, dice, or combined
366
- // leven = this is turbo, all around good for text similarity
367
- // dice = this is good for short strings, but not as good for long strings
368
- // turbo (default) = turbo of both, with a 2x preference for levenshtein
369
- "hover-text": async (
370
- text,
371
- description = null,
372
- action = "click",
373
- method = "turbo",
374
- timeout = 5000, // we pass this to the subsequent wait-for-text block
375
- ) => {
594
+ /**
595
+ * Hover over text on screen
596
+ * @param {Object|string} options - Options object or text (for backward compatibility)
597
+ * @param {string} options.text - Text to find and hover over
598
+ * @param {string|null} [options.description] - Optional description of the element
599
+ * @param {string} [options.action='click'] - Action to perform
600
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
601
+ */
602
+ "hover-text": async (...args) => {
603
+ let text, description, action, timeout;
604
+
605
+ // Handle both object and positional argument styles
606
+ if (isObjectArgs(args, ['text', 'description', 'action', 'timeout'])) {
607
+ ({ text, description = null, action = 'click', timeout = 5000 } = args[0]);
608
+ } else {
609
+ // Legacy positional: hoverText(text, description, action, timeout)
610
+ [text, description = null, action = 'click', timeout = 5000] = args;
611
+ }
612
+
613
+ emitter.emit(
614
+ events.log.narration,
615
+ theme.dim(
616
+ `searching for "${text}"${description ? ` (${description})` : ""}...`,
617
+ ),
618
+ );
619
+
376
620
  text = text ? text.toString() : null;
377
621
 
378
622
  // wait for the text to appear on screen
379
- await commands["wait-for-text"](text, timeout);
623
+ await commands["wait-for-text"]({ text, timeout });
380
624
 
381
625
  description = description ? description.toString() : null;
382
626
 
383
627
  emitter.emit(events.log.narration, theme.dim("thinking..."), true);
384
628
 
385
- let response = await sdk.req(
386
- "hover/text",
387
- {
388
- needle: text,
389
- method,
390
- image: await system.captureScreenBase64(),
391
- intent: action,
392
- description,
393
- displayMultiple: 1,
394
- },
395
- (chunk) => {
396
- if (chunk.type === "closeMatches") {
397
- emitter.emit(events.matches.show, chunk.data);
398
- }
399
- },
400
- );
629
+ // Combine text and description into element parameter
630
+ let element = text;
631
+ if (description) {
632
+ element = `"${text}" with description ${description}`;
633
+ }
634
+
635
+ let response = await sdk.req("find", {
636
+ element,
637
+ image: await system.captureScreenBase64(),
638
+ });
401
639
 
402
- if (!response.data) {
640
+ if (!response || !response.coordinates) {
403
641
  throw new MatchError("No text on screen matches description");
642
+ }
643
+
644
+ // Perform the action using the located coordinates
645
+ if (action === "hover") {
646
+ await commands.hover({ x: response.coordinates.x, y: response.coordinates.y });
404
647
  } else {
405
- return response.data;
648
+ await click({ x: response.coordinates.x, y: response.coordinates.y, action });
406
649
  }
407
- },
408
- // uses our api to find all images on screen
409
- "hover-image": async (description, action = "click") => {
410
- // take a screenshot
411
- emitter.emit(events.log.narration, theme.dim("thinking..."), true);
412
650
 
413
- let response = await sdk.req(
414
- "hover/image",
415
- {
416
- needle: description,
417
- image: await system.captureScreenBase64(),
418
- intent: action,
419
- displayMultiple: 1,
420
- },
421
- (chunk) => {
422
- if (chunk.type === "closeMatches") {
423
- emitter.emit(events.matches.show, chunk.data);
424
- }
425
- },
651
+ return response;
652
+ },
653
+ /**
654
+ * Hover over an image on screen
655
+ * @param {Object|string} options - Options object or description (for backward compatibility)
656
+ * @param {string} options.description - Description of the image to find
657
+ * @param {string} [options.action='click'] - Action to perform
658
+ */
659
+ "hover-image": async (...args) => {
660
+ let description, action;
661
+
662
+ // Handle both object and positional argument styles
663
+ if (isObjectArgs(args, ['description', 'action'])) {
664
+ ({ description, action = 'click' } = args[0]);
665
+ } else {
666
+ // Legacy positional: hoverImage(description, action)
667
+ [description, action = 'click'] = args;
668
+ }
669
+
670
+ emitter.emit(
671
+ events.log.narration,
672
+ theme.dim(`searching for image: "${description}"...`),
426
673
  );
427
674
 
428
- if (!response?.data) {
675
+ let response = await sdk.req("find", {
676
+ element: description,
677
+ image: await system.captureScreenBase64(),
678
+ });
679
+
680
+ if (!response || !response.coordinates) {
429
681
  throw new MatchError("No image or icon on screen matches description");
682
+ }
683
+
684
+ // Perform the action using the located coordinates
685
+ if (action === "hover") {
686
+ await commands.hover({ x: response.coordinates.x, y: response.coordinates.y });
430
687
  } else {
431
- return response.data;
688
+ await click({ x: response.coordinates.x, y: response.coordinates.y, action });
432
689
  }
690
+
691
+ return response;
433
692
  },
434
- "match-image": async (relativePath, action = "click", invert = false) => {
693
+ /**
694
+ * Match and interact with an image template
695
+ * @param {Object|string} options - Options object or path (for backward compatibility)
696
+ * @param {string} options.path - Path to the image template
697
+ * @param {string} [options.action='click'] - Action to perform
698
+ * @param {boolean} [options.invert=false] - Invert the match
699
+ */
700
+ "match-image": async (...args) => {
701
+ let relativePath, action, invert;
702
+
703
+ // Handle both object and positional argument styles
704
+ if (isObjectArgs(args, ['path', 'action', 'invert'])) {
705
+ ({ path: relativePath, action = 'click', invert = false } = args[0]);
706
+ } else {
707
+ // Legacy positional: matchImage(relativePath, action, invert)
708
+ [relativePath, action = 'click', invert = false] = args;
709
+ }
710
+
711
+ emitter.emit(
712
+ events.log.narration,
713
+ theme.dim(`${action} on image template "${relativePath}"...`),
714
+ );
715
+
435
716
  // Resolve the image path relative to the current file
436
717
  const resolvedPath = resolveRelativePath(relativePath);
437
718
 
@@ -447,42 +728,173 @@ const createCommands = (
447
728
  throw new CommandError(`Image not found: ${resolvedPath}`);
448
729
  } else {
449
730
  if (action === "click") {
450
- await click(result.centerX, result.centerY, action);
731
+ await click({ x: result.centerX, y: result.centerY, action });
451
732
  } else if (action === "hover") {
452
- await hover(result.centerX, result.centerY);
733
+ await hover({ x: result.centerX, y: result.centerY });
453
734
  }
454
735
  }
455
736
 
456
737
  return true;
457
738
  },
458
- // type a string
459
- os: "linux",
460
- type: async (string, delay = 250) => {
461
- await redraw.start();
739
+ /**
740
+ * Type text
741
+ * @param {string|number} text - Text to type
742
+ * @param {Object} [options] - Additional options
743
+ * @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
744
+ * @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
745
+ * @param {Object} [options.redraw] - Redraw detection options
746
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
747
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
748
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
749
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
750
+ */
751
+ "type": async (text, options = {}) => {
752
+ const typeStartTime = Date.now();
753
+ const { delay = 250, secret = false, redraw: redrawOpts, ...elementData } = options;
754
+ const redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...options });
755
+
756
+ // Log masked version if secret, otherwise show actual text
757
+ if (secret) {
758
+ emitter.emit(events.log.narration, theme.dim(`typing secret "****"...`));
759
+ } else {
760
+ emitter.emit(events.log.narration, theme.dim(`typing "${text}"...`));
761
+ }
762
+
763
+ await redraw.start(redrawOptions);
462
764
 
463
- string = string.toString();
765
+ text = text.toString();
464
766
 
465
- await sandbox.send({ os: "linux", type: "write", text: string, delay });
466
- await redraw.wait(5000);
767
+ // Add dashcam timestamp if available
768
+ if (getDashcamElapsedTime) {
769
+ const elapsed = getDashcamElapsedTime();
770
+ if (elapsed !== null) {
771
+ elementData.timestamp = elapsed;
772
+ }
773
+ }
774
+
775
+ // Actually type the text in the sandbox
776
+ await sandbox.send({ type: "write", text, delay, ...elementData });
777
+
778
+ // Track interaction
779
+ const sessionId = sessionInstance?.get();
780
+ if (sessionId) {
781
+ try {
782
+ const typeDuration = Date.now() - typeStartTime;
783
+ await sandbox.send({
784
+ type: "trackInteraction",
785
+ interactionType: "type",
786
+ session: sessionId,
787
+ // Store masked text if secret, otherwise store actual text
788
+ input: { text: secret ? "****" : text, delay },
789
+ timestamp: typeStartTime,
790
+ duration: typeDuration,
791
+ success: true,
792
+ isSecret: secret, // Flag this interaction if it contains a secret
793
+ });
794
+ } catch (err) {
795
+ console.warn("Failed to track type interaction:", err.message);
796
+ }
797
+ }
798
+
799
+ await redraw.wait(5000, redrawOptions);
467
800
  return;
468
801
  },
469
- // press keys
470
- // different than `type`, becasue it can press multiple keys at once
471
- "press-keys": async (inputKeys) => {
472
- await redraw.start();
802
+ /**
803
+ * Press keyboard keys
804
+ * @param {Array} keys - Array of keys to press
805
+ * @param {Object} [options] - Additional options
806
+ * @param {Object} [options.redraw] - Redraw detection options
807
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
808
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
809
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
810
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
811
+ */
812
+ "press-keys": async (keys, options = {}) => {
813
+ const pressKeysStartTime = Date.now();
814
+ const redrawOptions = extractRedrawOptions(options);
815
+ emitter.emit(
816
+ events.log.narration,
817
+ theme.dim(
818
+ `pressing keys: ${Array.isArray(keys) ? keys.join(", ") : keys}...`,
819
+ ),
820
+ );
821
+
822
+ await redraw.start(redrawOptions);
473
823
 
474
824
  // finally, press the keys
475
- await sandbox.send({ os: "linux", type: "press", keys: inputKeys });
825
+ await sandbox.send({ type: "press", keys });
826
+
827
+ // Track interaction
828
+ const sessionId = sessionInstance?.get();
829
+ if (sessionId) {
830
+ try {
831
+ const pressKeysDuration = Date.now() - pressKeysStartTime;
832
+ await sandbox.send({
833
+ type: "trackInteraction",
834
+ interactionType: "pressKeys",
835
+ session: sessionId,
836
+ input: { keys },
837
+ timestamp: pressKeysStartTime,
838
+ duration: pressKeysDuration,
839
+ success: true,
840
+ });
841
+ } catch (err) {
842
+ console.warn("Failed to track pressKeys interaction:", err.message);
843
+ }
844
+ }
476
845
 
477
- await redraw.wait(5000);
846
+ await redraw.wait(5000, redrawOptions);
478
847
 
479
848
  return;
480
849
  },
481
- // simple delay, usually to let ui render or webpage to load
482
- wait: async (timeout = 3000) => {
483
- return await delay(timeout);
850
+ /**
851
+ * Wait for specified time
852
+ * @param {number} [timeout=3000] - Time to wait in milliseconds
853
+ * @param {Object} [options] - Additional options (reserved for future use)
854
+ */
855
+ "wait": async (timeout = 3000, options = {}) => {
856
+ const waitStartTime = Date.now();
857
+ emitter.emit(events.log.narration, theme.dim(`waiting ${timeout}ms...`));
858
+ const result = await delay(timeout);
859
+
860
+ // Track interaction
861
+ const sessionId = sessionInstance?.get();
862
+ if (sessionId) {
863
+ try {
864
+ const waitDuration = Date.now() - waitStartTime;
865
+ await sandbox.send({
866
+ type: "trackInteraction",
867
+ interactionType: "wait",
868
+ session: sessionId,
869
+ input: { timeout },
870
+ timestamp: waitStartTime,
871
+ duration: waitDuration,
872
+ success: true,
873
+ });
874
+ } catch (err) {
875
+ console.warn("Failed to track wait interaction:", err.message);
876
+ }
877
+ }
878
+
879
+ return result;
484
880
  },
485
- "wait-for-image": async (description, timeout = 10000, invert = false) => {
881
+ /**
882
+ * Wait for image to appear on screen
883
+ * @param {Object|string} options - Options object or description (for backward compatibility)
884
+ * @param {string} options.description - Description of the image
885
+ * @param {number} [options.timeout=10000] - Timeout in milliseconds
886
+ */
887
+ "wait-for-image": async (...args) => {
888
+ let description, timeout;
889
+
890
+ // Handle both object and positional argument styles
891
+ if (isObjectArgs(args, ['description', 'timeout'])) {
892
+ ({ description, timeout = 10000 } = args[0]);
893
+ } else {
894
+ // Legacy positional: waitForImage(description, timeout)
895
+ [description, timeout = 10000] = args;
896
+ }
897
+
486
898
  emitter.emit(
487
899
  events.log.narration,
488
900
  theme.dim(
@@ -499,8 +911,6 @@ const createCommands = (
499
911
  passed = await assert(
500
912
  `An image matching the description "${description}" appears on screen.`,
501
913
  false,
502
- false,
503
- invert,
504
914
  );
505
915
 
506
916
  durationPassed = new Date().getTime() - startTime;
@@ -524,20 +934,80 @@ const createCommands = (
524
934
  ),
525
935
  true,
526
936
  );
937
+
938
+ // Track interaction success
939
+ const sessionId = sessionInstance?.get();
940
+ if (sessionId) {
941
+ try {
942
+ const waitForImageDuration = Date.now() - startTime;
943
+ await sandbox.send({
944
+ type: "trackInteraction",
945
+ interactionType: "waitForImage",
946
+ session: sessionId,
947
+ prompt: description,
948
+ input: { timeout },
949
+ timestamp: startTime,
950
+ duration: waitForImageDuration,
951
+ success: true,
952
+ });
953
+ } catch (err) {
954
+ console.warn("Failed to track waitForImage interaction:", err.message);
955
+ }
956
+ }
957
+
527
958
  return;
528
959
  } else {
529
- throw new MatchError(
530
- `Timed out (${niceSeconds(timeout)} seconds) while searching for an image matching the description "${description}"`,
531
- );
960
+ // Track interaction failure
961
+ const sessionId = sessionInstance?.get();
962
+ const errorMsg = `Timed out (${niceSeconds(timeout)} seconds) while searching for an image matching the description "${description}"`;
963
+ if (sessionId) {
964
+ try {
965
+ const waitForImageDuration = Date.now() - startTime;
966
+ await sandbox.send({
967
+ type: "trackInteraction",
968
+ interactionType: "waitForImage",
969
+ session: sessionId,
970
+ prompt: description,
971
+ input: { timeout },
972
+ timestamp: startTime,
973
+ duration: waitForImageDuration,
974
+ success: false,
975
+ error: errorMsg,
976
+ });
977
+ } catch (err) {
978
+ console.warn("Failed to track waitForImage interaction:", err.message);
979
+ }
980
+ }
981
+
982
+ throw new MatchError(errorMsg);
532
983
  }
533
984
  },
534
- "wait-for-text": async (
535
- text,
536
- timeout = 5000,
537
- method = "turbo",
538
- invert = false,
539
- ) => {
540
- await redraw.start();
985
+ /**
986
+ * Wait for text to appear on screen
987
+ * @param {Object|string} options - Options object or text (for backward compatibility)
988
+ * @param {string} options.text - Text to wait for
989
+ * @param {number} [options.timeout=5000] - Timeout in milliseconds
990
+ * @param {Object} [options.redraw] - Redraw detection options
991
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
992
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
993
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
994
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
995
+ */
996
+ "wait-for-text": async (...args) => {
997
+ let text, timeout, redrawOptions;
998
+
999
+ // Handle both object and positional argument styles
1000
+ if (isObjectArgs(args, ['text', 'timeout'])) {
1001
+ const { redraw: redrawOpts, ...rest } = args[0];
1002
+ ({ text, timeout = 5000 } = rest);
1003
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
1004
+ } else {
1005
+ // Legacy positional: waitForText(text, timeout)
1006
+ [text, timeout = 5000] = args;
1007
+ redrawOptions = {};
1008
+ }
1009
+
1010
+ await redraw.start(redrawOptions);
541
1011
 
542
1012
  emitter.emit(
543
1013
  events.log.narration,
@@ -551,25 +1021,13 @@ const createCommands = (
551
1021
  let passed = false;
552
1022
 
553
1023
  while (durationPassed < timeout && !passed) {
554
- const response = await sdk.req(
555
- "assert/text",
556
- {
557
- needle: text,
558
- method: method,
559
- image: await system.captureScreenBase64(),
560
- },
561
- (chunk) => {
562
- if (chunk.type === "closeMatches") {
563
- emitter.emit(events.matches.show, chunk.data);
564
- }
565
- },
566
- );
1024
+ const response = await sdk.req("find", {
1025
+ element: text,
1026
+ image: await system.captureScreenBase64(),
1027
+ });
567
1028
 
568
- passed = response.data;
1029
+ passed = !!(response && response.coordinates);
569
1030
 
570
- if (invert) {
571
- passed = !passed;
572
- }
573
1031
  durationPassed = new Date().getTime() - startTime;
574
1032
 
575
1033
  if (!passed) {
@@ -586,22 +1044,82 @@ const createCommands = (
586
1044
 
587
1045
  if (passed) {
588
1046
  emitter.emit(events.log.narration, theme.dim(`"${text}" found!`), true);
1047
+
1048
+ // Track interaction success
1049
+ const sessionId = sessionInstance?.get();
1050
+ if (sessionId) {
1051
+ try {
1052
+ const waitForTextDuration = Date.now() - startTime;
1053
+ await sandbox.send({
1054
+ type: "trackInteraction",
1055
+ interactionType: "waitForText",
1056
+ session: sessionId,
1057
+ prompt: text,
1058
+ input: { timeout },
1059
+ timestamp: startTime,
1060
+ duration: waitForTextDuration,
1061
+ success: true,
1062
+ });
1063
+ } catch (err) {
1064
+ console.warn("Failed to track waitForText interaction:", err.message);
1065
+ }
1066
+ }
1067
+
589
1068
  return;
590
1069
  } else {
591
- throw new MatchError(
592
- `Timed out (${niceSeconds(timeout)} seconds) while searching for "${text}"`,
593
- );
1070
+ // Track interaction failure
1071
+ const sessionId = sessionInstance?.get();
1072
+ const errorMsg = `Timed out (${niceSeconds(timeout)} seconds) while searching for "${text}"`;
1073
+ if (sessionId) {
1074
+ try {
1075
+ const waitForTextDuration = Date.now() - startTime;
1076
+ await sandbox.send({
1077
+ type: "trackInteraction",
1078
+ interactionType: "waitForText",
1079
+ session: sessionId,
1080
+ prompt: text,
1081
+ input: { timeout },
1082
+ timestamp: startTime,
1083
+ duration: waitForTextDuration,
1084
+ success: false,
1085
+ error: errorMsg,
1086
+ });
1087
+ } catch (err) {
1088
+ console.warn("Failed to track waitForText interaction:", err.message);
1089
+ }
1090
+ }
1091
+
1092
+ throw new MatchError(errorMsg);
594
1093
  }
595
1094
  },
596
- "scroll-until-text": async (
597
- text,
598
- direction = "down",
599
- maxDistance = 10000,
600
- textMatchMethod = "turbo",
601
- method = "keyboard",
602
- invert = false,
603
- ) => {
604
- await redraw.start();
1095
+ /**
1096
+ * Scroll until text is found
1097
+ * @param {Object|string} options - Options object or text (for backward compatibility)
1098
+ * @param {string} options.text - Text to find
1099
+ * @param {string} [options.direction='down'] - Scroll direction
1100
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
1101
+ * @param {boolean} [options.invert=false] - Invert the match
1102
+ * @param {Object} [options.redraw] - Redraw detection options
1103
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
1104
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
1105
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
1106
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
1107
+ */
1108
+ "scroll-until-text": async (...args) => {
1109
+ let text, direction, maxDistance, invert, redrawOptions;
1110
+
1111
+ // Handle both object and positional argument styles
1112
+ if (isObjectArgs(args, ['text', 'direction', 'maxDistance', 'invert'])) {
1113
+ const { redraw: redrawOpts, ...rest } = args[0];
1114
+ ({ text, direction = 'down', maxDistance = 10000, invert = false } = rest);
1115
+ redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
1116
+ } else {
1117
+ // Legacy positional: scrollUntilText(text, direction, maxDistance, invert)
1118
+ [text, direction = 'down', maxDistance = 10000, invert = false] = args;
1119
+ redrawOptions = {};
1120
+ }
1121
+
1122
+ await redraw.start(redrawOptions);
605
1123
 
606
1124
  emitter.emit(
607
1125
  events.log.narration,
@@ -609,44 +1127,17 @@ const createCommands = (
609
1127
  true,
610
1128
  );
611
1129
 
612
- if (method === "keyboard") {
613
- try {
614
- await sandbox.send({
615
- os: "linux",
616
- type: "press",
617
- keys: ["f", "ctrl"],
618
- });
619
- await delay(1000);
620
- await sandbox.send({ os: "linux", type: "write", text });
621
- await redraw.wait(5000);
622
- await sandbox.send({ os: "linux", type: "press", keys: ["escape"] });
623
- } catch {
624
- throw new MatchError(
625
- "Could not find element using browser text search",
626
- );
627
- }
628
- }
629
-
630
1130
  let scrollDistance = 0;
631
1131
  let incrementDistance = 500;
632
1132
  let passed = false;
633
1133
 
634
1134
  while (scrollDistance <= maxDistance && !passed) {
635
- const response = await sdk.req(
636
- "assert/text",
637
- {
638
- needle: text,
639
- method: textMatchMethod,
640
- image: await system.captureScreenBase64(),
641
- },
642
- (chunk) => {
643
- if (chunk.type === "closeMatches") {
644
- emitter.emit(events.matches.show, chunk.data);
645
- }
646
- },
647
- );
1135
+ const response = await sdk.req("find", {
1136
+ element: text,
1137
+ image: await system.captureScreenBase64(),
1138
+ });
648
1139
 
649
- passed = response.data;
1140
+ passed = !!(response && response.coordinates);
650
1141
 
651
1142
  if (invert) {
652
1143
  passed = !passed;
@@ -660,7 +1151,7 @@ const createCommands = (
660
1151
  ),
661
1152
  true,
662
1153
  );
663
- await scroll(direction, incrementDistance, method);
1154
+ await scroll({ direction, amount: incrementDistance });
664
1155
  scrollDistance = scrollDistance + incrementDistance;
665
1156
  }
666
1157
  }
@@ -674,21 +1165,34 @@ const createCommands = (
674
1165
  );
675
1166
  }
676
1167
  },
677
- "scroll-until-image": async (
678
- description,
679
- direction = "down",
680
- maxDistance = 10000,
681
- method = "keyboard",
682
- path,
683
- invert = false,
684
- ) => {
685
- const needle = description || path;
1168
+ /**
1169
+ * Scroll until image is found
1170
+ * @param {Object|string} options - Options object or description (for backward compatibility)
1171
+ * @param {string} [options.description] - Description of the image
1172
+ * @param {string} [options.direction='down'] - Scroll direction
1173
+ * @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
1174
+ * @param {string} [options.method='mouse'] - Scroll method
1175
+ * @param {string} [options.path] - Path to image template
1176
+ * @param {boolean} [options.invert=false] - Invert the match
1177
+ */
1178
+ "scroll-until-image": async (...args) => {
1179
+ let description, direction, maxDistance, method, imagePath, invert;
1180
+
1181
+ // Handle both object and positional argument styles
1182
+ if (isObjectArgs(args, ['description', 'direction', 'maxDistance', 'method', 'path', 'invert'])) {
1183
+ ({ description, direction = 'down', maxDistance = 10000, method = 'mouse', path: imagePath, invert = false } = args[0]);
1184
+ } else {
1185
+ // Legacy positional: scrollUntilImage(description, direction, maxDistance, method, path, invert)
1186
+ [description, direction = 'down', maxDistance = 10000, method = 'mouse', imagePath, invert = false] = args;
1187
+ }
1188
+
1189
+ const needle = description || imagePath;
686
1190
 
687
1191
  if (!needle) {
688
1192
  throw new CommandError("No description or path provided");
689
1193
  }
690
1194
 
691
- if (description && path) {
1195
+ if (description && imagePath) {
692
1196
  throw new CommandError(
693
1197
  "Only one of description or path can be provided",
694
1198
  );
@@ -714,9 +1218,9 @@ const createCommands = (
714
1218
  );
715
1219
  }
716
1220
 
717
- if (path) {
1221
+ if (imagePath) {
718
1222
  // Don't throw if not found. We only want to know if it's found or not.
719
- passed = await commands["match-image"](path, null).catch(
1223
+ passed = await commands["match-image"]({ path: imagePath }).catch(
720
1224
  console.warn,
721
1225
  );
722
1226
  }
@@ -727,7 +1231,7 @@ const createCommands = (
727
1231
  theme.dim(`scrolling ${direction} ${incrementDistance} pixels...`),
728
1232
  true,
729
1233
  );
730
- await scroll(direction, incrementDistance, method);
1234
+ await scroll({ direction, amount: incrementDistance });
731
1235
  scrollDistance = scrollDistance + incrementDistance;
732
1236
  }
733
1237
  }
@@ -745,31 +1249,122 @@ const createCommands = (
745
1249
  );
746
1250
  }
747
1251
  },
748
- // run applescript to focus an application by name
749
- "focus-application": async (name) => {
750
- await redraw.start();
1252
+ /**
1253
+ * Focus an application by name
1254
+ * @param {string} name - Application name
1255
+ * @param {Object} [options] - Additional options
1256
+ * @param {Object} [options.redraw] - Redraw detection options
1257
+ * @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
1258
+ * @param {boolean} [options.redraw.screenRedraw=true] - Enable/disable screen redraw detection
1259
+ * @param {boolean} [options.redraw.networkMonitor=true] - Enable/disable network monitoring
1260
+ * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
1261
+ */
1262
+ "focus-application": async (name, options = {}) => {
1263
+ const redrawOptions = extractRedrawOptions(options);
1264
+ await redraw.start(redrawOptions);
751
1265
 
752
1266
  await sandbox.send({
753
- os: "linux",
754
1267
  type: "commands.focus-application",
755
1268
  name,
756
1269
  });
757
- await redraw.wait(1000);
1270
+ await redraw.wait(1000, redrawOptions);
758
1271
  return "The application was focused.";
759
1272
  },
760
- remember: async (description) => {
761
- let result = await sdk.req("remember", {
762
- image: await system.captureScreenBase64(),
763
- description,
764
- });
765
- return result.data;
1273
+ /**
1274
+ * Extract and remember information from the screen using AI
1275
+ * @param {Object|string} options - Options object or description (for backward compatibility)
1276
+ * @param {string} options.description - What to remember
1277
+ */
1278
+ "remember": async (...args) => {
1279
+ const rememberStartTime = Date.now();
1280
+ let description;
1281
+
1282
+ // Handle both object and positional argument styles
1283
+ if (isObjectArgs(args, ['description'])) {
1284
+ ({ description } = args[0]);
1285
+ } else {
1286
+ // Legacy positional: remember(description)
1287
+ [description] = args;
1288
+ }
1289
+
1290
+ try {
1291
+ let result = await sdk.req("remember", {
1292
+ image: await system.captureScreenBase64(),
1293
+ description,
1294
+ });
1295
+
1296
+ // Track interaction success
1297
+ const sessionId = sessionInstance?.get();
1298
+ if (sessionId) {
1299
+ try {
1300
+ const rememberDuration = Date.now() - rememberStartTime;
1301
+ await sandbox.send({
1302
+ type: "trackInteraction",
1303
+ interactionType: "remember",
1304
+ session: sessionId,
1305
+ prompt: description,
1306
+ timestamp: rememberStartTime,
1307
+ duration: rememberDuration,
1308
+ success: true,
1309
+ });
1310
+ } catch (err) {
1311
+ console.warn("Failed to track remember interaction:", err.message);
1312
+ }
1313
+ }
1314
+
1315
+ return result.data;
1316
+ } catch (error) {
1317
+ // Track interaction failure
1318
+ const sessionId = sessionInstance?.get();
1319
+ if (sessionId) {
1320
+ try {
1321
+ const rememberDuration = Date.now() - rememberStartTime;
1322
+ await sandbox.send({
1323
+ type: "trackInteraction",
1324
+ interactionType: "remember",
1325
+ session: sessionId,
1326
+ prompt: description,
1327
+ timestamp: rememberStartTime,
1328
+ duration: rememberDuration,
1329
+ success: false,
1330
+ error: error.message,
1331
+ });
1332
+ } catch (err) {
1333
+ console.warn("Failed to track remember interaction:", err.message);
1334
+ }
1335
+ }
1336
+ throw error;
1337
+ }
766
1338
  },
767
- assert: async (assertion, async = false, invert = false) => {
768
- let response = await assert(assertion, true, async, invert);
1339
+ /**
1340
+ * Make an AI-powered assertion
1341
+ * @param {string} assertion - Assertion to check
1342
+ * @param {Object} [options] - Additional options (reserved for future use)
1343
+ */
1344
+ "assert": async (assertion, options = {}) => {
1345
+ let response = await assert(assertion, true);
769
1346
 
770
1347
  return response;
771
1348
  },
772
- exec: async (language = "pwsh", code, timeout, silent = false) => {
1349
+ /**
1350
+ * Execute code in the sandbox
1351
+ * @param {Object|string} options - Options object or language (for backward compatibility)
1352
+ * @param {string} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
1353
+ * @param {string} options.code - Code to execute
1354
+ * @param {number} [options.timeout] - Timeout in milliseconds
1355
+ * @param {boolean} [options.silent=false] - Suppress output
1356
+ */
1357
+ "exec": async (...args) => {
1358
+ let language, code, timeout, silent;
1359
+
1360
+ // Handle both object and positional argument styles
1361
+ if (isObjectArgs(args, ['language', 'code', 'timeout', 'silent'])) {
1362
+ ({ language = 'pwsh', code, timeout, silent = false } = args[0]);
1363
+ } else {
1364
+ // Legacy positional: exec(language, code, timeout, silent)
1365
+ [language = 'pwsh', code, timeout, silent = false] = args;
1366
+ }
1367
+
773
1368
  emitter.emit(events.log.narration, theme.dim(`calling exec...`), true);
774
1369
 
775
1370
  emitter.emit(events.log.log, code);
@@ -777,17 +1372,36 @@ const createCommands = (
777
1372
  let plat = system.platform();
778
1373
 
779
1374
  if (language == "pwsh" || language == "sh") {
1375
+ if (language === "pwsh" && sandbox.os === "linux") {
1376
+ emitter.emit(
1377
+ events.log.log,
1378
+ theme.yellow(
1379
+ `⚠️ Warning: You are using 'pwsh' exec command on a Linux sandbox. This may fail. Consider using 'bash' or 'sh' for Linux environments.`,
1380
+ ),
1381
+ true,
1382
+ );
1383
+ }
1384
+
1385
+ if (language === "sh" && sandbox.os === "windows") {
1386
+ emitter.emit(
1387
+ events.log.log,
1388
+ theme.yellow(
1389
+ `⚠️ Warning: You are using 'sh' exec command on a Windows sandbox. This will fail. Automatically switching to 'pwsh' for Windows environments.`,
1390
+ ),
1391
+ true,
1392
+ );
1393
+ // Automatically switch to pwsh for Windows
1394
+ language = "pwsh";
1395
+ }
1396
+
780
1397
  let result = null;
781
1398
 
782
1399
  result = await sandbox.send({
783
- os: "linux",
784
1400
  type: "commands.run",
785
1401
  command: code,
786
1402
  timeout,
787
1403
  });
788
1404
 
789
- console.log("Exec result:", result);
790
-
791
1405
  if (result.out && result.out.returncode !== 0) {
792
1406
  throw new MatchError(
793
1407
  `Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,