testdriverai 6.2.1 → 7.0.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 (264) 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/MIGRATION.md +389 -0
  5. package/PLUGIN_MIGRATION.md +222 -0
  6. package/PROMPT_CACHE.md +200 -0
  7. package/SDK_LOGGING.md +222 -0
  8. package/SDK_MIGRATION.md +474 -0
  9. package/SDK_README.md +1122 -0
  10. package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
  11. package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
  12. package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
  13. package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
  14. package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
  15. package/_testdriver/lifecycle/prerun.yaml +15 -0
  16. package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
  17. package/agent/index.js +258 -68
  18. package/agent/interface.js +15 -0
  19. package/agent/lib/cache.js +142 -0
  20. package/agent/lib/commander.js +1 -39
  21. package/agent/lib/commands.js +143 -188
  22. package/agent/lib/redraw.js +6 -3
  23. package/agent/lib/sandbox.js +19 -5
  24. package/agent/lib/sdk.js +1 -0
  25. package/agent/lib/system.js +0 -3
  26. package/agent/lib/validation.js +1 -7
  27. package/debug-locate-response.js +82 -0
  28. package/debug-screenshot-1763401388589.png +0 -0
  29. package/debugger/index.html +16 -5
  30. package/docs/ARCHITECTURE.md +424 -0
  31. package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
  32. package/docs/QUICK_START_TEST_RECORDING.md +215 -0
  33. package/docs/SDK_AWESOME_LOGS.md +468 -0
  34. package/docs/TEST_RECORDING.md +388 -0
  35. package/docs/docs.json +232 -152
  36. package/docs/sdk-browser-rendering.md +167 -0
  37. package/docs/v6/getting-started/self-hosting.mdx +407 -0
  38. package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
  39. package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
  40. package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
  41. package/docs/v6/overview/comparison.mdx +101 -0
  42. package/docs/v7/README.md +135 -0
  43. package/docs/v7/api/ai.mdx +205 -0
  44. package/docs/v7/api/assert.mdx +285 -0
  45. package/docs/v7/api/assertions.mdx +403 -0
  46. package/docs/v7/api/click.mdx +287 -0
  47. package/docs/v7/api/client.mdx +322 -0
  48. package/docs/v7/api/elements.mdx +479 -0
  49. package/docs/v7/api/exec.mdx +346 -0
  50. package/docs/v7/api/find.mdx +316 -0
  51. package/docs/v7/api/focusApplication.mdx +294 -0
  52. package/docs/v7/api/hover.mdx +279 -0
  53. package/docs/v7/api/pressKeys.mdx +349 -0
  54. package/docs/v7/api/sandbox.mdx +404 -0
  55. package/docs/v7/api/scroll.mdx +300 -0
  56. package/docs/v7/api/type.mdx +314 -0
  57. package/docs/v7/commands/assert.mdx +45 -0
  58. package/docs/v7/commands/exec.mdx +282 -0
  59. package/docs/v7/commands/focus-application.mdx +44 -0
  60. package/docs/v7/commands/hover-image.mdx +69 -0
  61. package/docs/v7/commands/hover-text.mdx +47 -0
  62. package/docs/v7/commands/if.mdx +53 -0
  63. package/docs/v7/commands/match-image.mdx +67 -0
  64. package/docs/v7/commands/press-keys.mdx +87 -0
  65. package/docs/v7/commands/remember.mdx +49 -0
  66. package/docs/v7/commands/run.mdx +44 -0
  67. package/docs/v7/commands/scroll-until-image.mdx +66 -0
  68. package/docs/v7/commands/scroll-until-text.mdx +60 -0
  69. package/docs/v7/commands/scroll.mdx +69 -0
  70. package/docs/v7/commands/type.mdx +45 -0
  71. package/docs/v7/commands/wait-for-image.mdx +54 -0
  72. package/docs/v7/commands/wait-for-text.mdx +48 -0
  73. package/docs/v7/commands/wait.mdx +45 -0
  74. package/docs/v7/getting-started/quickstart.mdx +199 -0
  75. package/docs/v7/guides/migration.mdx +562 -0
  76. package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
  77. package/docs/v7/playwright.mdx +342 -0
  78. package/eslint.config.js +19 -1
  79. package/examples/run-tests-with-recording.sh +70 -0
  80. package/examples/screenshot-example.js +63 -0
  81. package/examples/sdk-awesome-logs-demo.js +177 -0
  82. package/examples/sdk-cache-thresholds.js +96 -0
  83. package/examples/sdk-element-properties.js +155 -0
  84. package/examples/sdk-simple-example.js +65 -0
  85. package/examples/test-recording-example.test.js +166 -0
  86. package/interfaces/cli/lib/base.js +10 -4
  87. package/interfaces/logger.js +2 -1
  88. package/interfaces/shared-test-state.mjs +69 -0
  89. package/interfaces/vitest-plugin.mjs +744 -0
  90. package/mcp-server/AI_GUIDELINES.md +57 -0
  91. package/package.json +18 -5
  92. package/schema.json +8 -29
  93. package/scripts/view-test-results.mjs +96 -0
  94. package/sdk-log-formatter.js +714 -0
  95. package/sdk.d.ts +735 -0
  96. package/sdk.js +1906 -0
  97. package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
  98. package/setup/aws/cloudformation.yaml +9 -2
  99. package/test/mcp-example-test.yaml +27 -0
  100. package/test-find-api.js +73 -0
  101. package/test-prompt-cache.js +96 -0
  102. package/test-sandbox-render.js +28 -0
  103. package/test-sdk-methods.js +15 -0
  104. package/test-sdk-refactor.js +53 -0
  105. package/test-stack-trace.mjs +57 -0
  106. package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
  107. package/testdriver/acceptance-sdk/README.md +128 -0
  108. package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
  109. package/testdriver/acceptance-sdk/assert.test.mjs +44 -0
  110. package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +70 -0
  111. package/testdriver/acceptance-sdk/element-not-found.test.mjs +38 -0
  112. package/testdriver/acceptance-sdk/exec-js.test.mjs +55 -0
  113. package/testdriver/acceptance-sdk/exec-output.test.mjs +71 -0
  114. package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +69 -0
  115. package/testdriver/acceptance-sdk/focus-window.test.mjs +48 -0
  116. package/testdriver/acceptance-sdk/formatted-logging.test.mjs +41 -0
  117. package/testdriver/acceptance-sdk/hover-image.test.mjs +43 -0
  118. package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +50 -0
  119. package/testdriver/acceptance-sdk/hover-text.test.mjs +41 -0
  120. package/testdriver/acceptance-sdk/match-image.test.mjs +48 -0
  121. package/testdriver/acceptance-sdk/press-keys.test.mjs +64 -0
  122. package/testdriver/acceptance-sdk/prompt.test.mjs +45 -0
  123. package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +52 -0
  124. package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +51 -0
  125. package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +42 -0
  126. package/testdriver/acceptance-sdk/scroll.test.mjs +50 -0
  127. package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
  128. package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +239 -0
  129. package/testdriver/acceptance-sdk/setup/testHelpers.mjs +648 -0
  130. package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
  131. package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
  132. package/testdriver/acceptance-sdk/type.test.mjs +84 -0
  133. package/verify-element-api.js +89 -0
  134. package/verify-types.js +0 -0
  135. package/vitest.config.example.js +19 -0
  136. package/vitest.config.mjs +65 -0
  137. package/vitest.config.mjs.bak +44 -0
  138. package/.github/workflows/acceptance-v6.yml +0 -169
  139. package/docs/overview/comparison.mdx +0 -82
  140. package/testdriver/lifecycle/prerun.yaml +0 -17
  141. /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
  142. /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
  143. /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
  144. /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
  145. /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
  146. /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
  147. /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
  148. /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
  149. /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
  150. /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
  151. /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
  152. /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
  153. /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
  154. /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
  155. /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
  156. /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
  157. /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
  158. /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
  159. /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
  160. /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
  161. /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
  162. /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
  163. /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
  164. /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
  165. /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
  166. /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
  167. /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
  168. /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
  169. /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
  170. /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
  171. /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
  172. /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
  173. /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
  174. /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
  175. /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
  176. /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
  177. /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
  178. /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
  179. /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
  180. /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
  181. /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
  182. /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
  183. /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
  184. /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
  185. /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
  186. /package/docs/{account → v6/account}/dashboard.mdx +0 -0
  187. /package/docs/{account → v6/account}/enterprise.mdx +0 -0
  188. /package/docs/{account → v6/account}/pricing.mdx +0 -0
  189. /package/docs/{account → v6/account}/projects.mdx +0 -0
  190. /package/docs/{account → v6/account}/team.mdx +0 -0
  191. /package/docs/{action → v6/action}/ami.mdx +0 -0
  192. /package/docs/{action → v6/action}/performance.mdx +0 -0
  193. /package/docs/{action → v6/action}/secrets.mdx +0 -0
  194. /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
  195. /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
  196. /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
  197. /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
  198. /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
  199. /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
  200. /package/docs/{cli → v6/cli}/overview.mdx +0 -0
  201. /package/docs/{commands → v6/commands}/assert.mdx +0 -0
  202. /package/docs/{commands → v6/commands}/exec.mdx +0 -0
  203. /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
  204. /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
  205. /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
  206. /package/docs/{commands → v6/commands}/if.mdx +0 -0
  207. /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
  208. /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
  209. /package/docs/{commands → v6/commands}/remember.mdx +0 -0
  210. /package/docs/{commands → v6/commands}/run.mdx +0 -0
  211. /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
  212. /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
  213. /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
  214. /package/docs/{commands → v6/commands}/type.mdx +0 -0
  215. /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
  216. /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
  217. /package/docs/{commands → v6/commands}/wait.mdx +0 -0
  218. /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
  219. /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
  220. /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
  221. /package/docs/{features → v6/features}/generation.mdx +0 -0
  222. /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
  223. /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
  224. /package/docs/{features → v6/features}/selectorless.mdx +0 -0
  225. /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
  226. /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
  227. /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
  228. /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
  229. /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
  230. /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
  231. /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
  232. /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
  233. /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
  234. /package/docs/{guide → v6/guide}/code.mdx +0 -0
  235. /package/docs/{guide → v6/guide}/locating.mdx +0 -0
  236. /package/docs/{guide → v6/guide}/protips.mdx +0 -0
  237. /package/docs/{guide → v6/guide}/variables.mdx +0 -0
  238. /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
  239. /package/docs/{importing → v6/importing}/csv.mdx +0 -0
  240. /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
  241. /package/docs/{importing → v6/importing}/jira.mdx +0 -0
  242. /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
  243. /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
  244. /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
  245. /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
  246. /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
  247. /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
  248. /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
  249. /package/docs/{overview → v6/overview}/faq.mdx +0 -0
  250. /package/docs/{overview → v6/overview}/performance.mdx +0 -0
  251. /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
  252. /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
  253. /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
  254. /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
  255. /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
  256. /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
  257. /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
  258. /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
  259. /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
  260. /package/docs/{security → v6/security}/action.mdx +0 -0
  261. /package/docs/{security → v6/security}/agent.mdx +0 -0
  262. /package/docs/{security → v6/security}/platform.mdx +0 -0
  263. /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
  264. /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
@@ -44,11 +44,12 @@ const createCommands = (
44
44
  config,
45
45
  sessionInstance,
46
46
  getCurrentFilePath,
47
+ redrawThreshold = 0.1,
47
48
  ) => {
48
49
  // Create SDK instance with emitter, config, and session
49
50
  const sdk = createSDK(emitter, config, sessionInstance);
50
51
  // Create redraw instance with the system
51
- const redraw = createRedraw(emitter, system, sandbox);
52
+ const redraw = createRedraw(emitter, system, sandbox, redrawThreshold);
52
53
 
53
54
  // Helper method to resolve file paths relative to the current file
54
55
  const resolveRelativePath = (relativePath) => {
@@ -73,6 +74,7 @@ const createCommands = (
73
74
  return Math.round(ms / 1000);
74
75
  };
75
76
  const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
77
+
76
78
  const findImageOnScreen = async (
77
79
  relativePath,
78
80
  haystack,
@@ -170,103 +172,64 @@ const createCommands = (
170
172
  return result;
171
173
  };
172
174
 
173
- const assert = async (
174
- assertion,
175
- shouldThrow = false,
176
- async = false,
177
- invert = false,
178
- ) => {
179
- if (async) {
180
- shouldThrow = true;
181
- }
182
-
175
+ const assert = async (assertion, shouldThrow = false) => {
183
176
  const handleAssertResponse = (response) => {
184
177
  emitter.emit(events.log.log, response);
185
178
 
186
179
  let valid = response.indexOf("The task passed") > -1;
187
180
 
188
- if (invert) {
189
- valid = !valid;
190
- }
191
-
192
181
  if (valid) {
193
182
  return true;
194
183
  } else {
195
184
  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
- );
185
+ // Is fatal, otherwise it just changes the assertion to be true
186
+ const errorMessage = `AI Assertion failed: ${assertion}\n${response}`;
187
+ throw new MatchError(errorMessage, true);
201
188
  } else {
202
189
  return false;
203
190
  }
204
191
  }
205
192
  };
206
193
 
207
- emitter.emit(events.log.narration, `thinking...`);
194
+ // Log asserting action
195
+ const { formatter } = require("../../sdk-log-formatter.js");
196
+ const assertingMessage = formatter.formatAsserting(assertion);
197
+ emitter.emit(events.log.log, assertingMessage);
208
198
 
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);
217
- });
199
+ emitter.emit(events.log.narration, `thinking...`);
218
200
 
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);
226
- }
201
+ let response = await sdk.req("assert", {
202
+ expect: assertion,
203
+ image: await system.captureScreenBase64(),
204
+ });
205
+ return handleAssertResponse(response.data);
227
206
  };
228
- const scroll = async (direction = "down", amount = 300, method = "mouse") => {
207
+ const scroll = async (direction = "down", amount = 300) => {
208
+ emitter.emit(
209
+ events.log.narration,
210
+ theme.dim(`scrolling ${direction} ${amount}px...`),
211
+ );
212
+
229
213
  await redraw.start();
230
214
 
231
215
  amount = parseInt(amount, 10);
232
216
 
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
- }
239
-
240
217
  const before = await system.captureScreenBase64();
241
218
  switch (direction) {
242
219
  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
- }
220
+ await sandbox.send({
221
+ type: "scroll",
222
+ amount,
223
+ direction,
224
+ });
253
225
  await redraw.wait(2500);
254
226
  break;
255
227
  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
- }
228
+ await sandbox.send({
229
+ type: "scroll",
230
+ amount,
231
+ direction,
232
+ });
270
233
  await redraw.wait(2500);
271
234
  break;
272
235
  case "left":
@@ -312,7 +275,7 @@ const createCommands = (
312
275
  x = parseInt(x);
313
276
  y = parseInt(y);
314
277
 
315
- await sandbox.send({ os: "linux", type: "moveMouse", x, y });
278
+ await sandbox.send({ type: "moveMouse", x, y });
316
279
 
317
280
  emitter.emit(events.mouseMove, { x, y });
318
281
 
@@ -320,18 +283,17 @@ const createCommands = (
320
283
 
321
284
  if (action !== "hover") {
322
285
  if (action === "click" || action === "left-click") {
323
- await sandbox.send({ os: "linux", type: "leftClick" });
286
+ await sandbox.send({ type: "leftClick" });
324
287
  } else if (action === "right-click") {
325
- await sandbox.send({ os: "linux", type: "rightClick" });
288
+ await sandbox.send({ type: "rightClick" });
326
289
  } else if (action === "middle-click") {
327
- await sandbox.send({ os: "linux", type: "middleClick" });
290
+ await sandbox.send({ type: "middleClick" });
328
291
  } 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") {
292
+ await sandbox.send({ type: "doubleClick" });
293
+ } else if (action === "mouseDown") {
294
+ await sandbox.send({ type: "mousePress", button: "left" });
295
+ } else if (action === "mouseUp") {
333
296
  await sandbox.send({
334
- os: "linux",
335
297
  type: "mouseRelease",
336
298
  button: "left",
337
299
  });
@@ -346,12 +308,14 @@ const createCommands = (
346
308
  };
347
309
 
348
310
  const hover = async (x, y) => {
311
+ emitter.emit(events.log.narration, theme.dim(`hovering at ${x}, ${y}...`));
312
+
349
313
  await redraw.start();
350
314
 
351
315
  x = parseInt(x);
352
316
  y = parseInt(y);
353
317
 
354
- await sandbox.send({ os: "linux", type: "moveMouse", x, y });
318
+ await sandbox.send({ type: "moveMouse", x, y });
355
319
 
356
320
  await redraw.wait(2500);
357
321
 
@@ -370,9 +334,15 @@ const createCommands = (
370
334
  text,
371
335
  description = null,
372
336
  action = "click",
373
- method = "turbo",
374
337
  timeout = 5000, // we pass this to the subsequent wait-for-text block
375
338
  ) => {
339
+ emitter.emit(
340
+ events.log.narration,
341
+ theme.dim(
342
+ `searching for "${text}"${description ? ` (${description})` : ""}...`,
343
+ ),
344
+ );
345
+
376
346
  text = text ? text.toString() : null;
377
347
 
378
348
  // wait for the text to appear on screen
@@ -382,56 +352,61 @@ const createCommands = (
382
352
 
383
353
  emitter.emit(events.log.narration, theme.dim("thinking..."), true);
384
354
 
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
- );
355
+ // Combine text and description into element parameter
356
+ let element = text;
357
+ if (description) {
358
+ element = `"${text}" with description ${description}`;
359
+ }
401
360
 
402
- if (!response.data) {
361
+ let response = await sdk.req("find", {
362
+ element,
363
+ image: await system.captureScreenBase64(),
364
+ });
365
+
366
+ if (!response || !response.coordinates) {
403
367
  throw new MatchError("No text on screen matches description");
368
+ }
369
+
370
+ // Perform the action using the located coordinates
371
+ if (action === "hover") {
372
+ await commands.hover(response.coordinates.x, response.coordinates.y);
404
373
  } else {
405
- return response.data;
374
+ await click(response.coordinates.x, response.coordinates.y, action);
406
375
  }
376
+
377
+ return response;
407
378
  },
408
379
  // uses our api to find all images on screen
409
380
  "hover-image": async (description, action = "click") => {
410
- // take a screenshot
411
- emitter.emit(events.log.narration, theme.dim("thinking..."), true);
412
-
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
- },
381
+ emitter.emit(
382
+ events.log.narration,
383
+ theme.dim(`searching for image: "${description}"...`),
426
384
  );
427
385
 
428
- if (!response?.data) {
386
+ let response = await sdk.req("find", {
387
+ element: description,
388
+ image: await system.captureScreenBase64(),
389
+ });
390
+
391
+ if (!response || !response.coordinates) {
429
392
  throw new MatchError("No image or icon on screen matches description");
393
+ }
394
+
395
+ // Perform the action using the located coordinates
396
+ if (action === "hover") {
397
+ await commands.hover(response.coordinates.x, response.coordinates.y);
430
398
  } else {
431
- return response.data;
399
+ await click(response.coordinates.x, response.coordinates.y, action);
432
400
  }
401
+
402
+ return response;
433
403
  },
434
404
  "match-image": async (relativePath, action = "click", invert = false) => {
405
+ emitter.emit(
406
+ events.log.narration,
407
+ theme.dim(`${action} on image template "${relativePath}"...`),
408
+ );
409
+
435
410
  // Resolve the image path relative to the current file
436
411
  const resolvedPath = resolveRelativePath(relativePath);
437
412
 
@@ -456,23 +431,32 @@ const createCommands = (
456
431
  return true;
457
432
  },
458
433
  // type a string
459
- os: "linux",
434
+
460
435
  type: async (string, delay = 250) => {
436
+ emitter.emit(events.log.narration, theme.dim(`typing "${string}"...`));
437
+
461
438
  await redraw.start();
462
439
 
463
440
  string = string.toString();
464
441
 
465
- await sandbox.send({ os: "linux", type: "write", text: string, delay });
442
+ await sandbox.send({ type: "write", text: string, delay });
466
443
  await redraw.wait(5000);
467
444
  return;
468
445
  },
469
446
  // press keys
470
447
  // different than `type`, becasue it can press multiple keys at once
471
448
  "press-keys": async (inputKeys) => {
449
+ emitter.emit(
450
+ events.log.narration,
451
+ theme.dim(
452
+ `pressing keys: ${Array.isArray(inputKeys) ? inputKeys.join(", ") : inputKeys}...`,
453
+ ),
454
+ );
455
+
472
456
  await redraw.start();
473
457
 
474
458
  // finally, press the keys
475
- await sandbox.send({ os: "linux", type: "press", keys: inputKeys });
459
+ await sandbox.send({ type: "press", keys: inputKeys });
476
460
 
477
461
  await redraw.wait(5000);
478
462
 
@@ -480,9 +464,10 @@ const createCommands = (
480
464
  },
481
465
  // simple delay, usually to let ui render or webpage to load
482
466
  wait: async (timeout = 3000) => {
467
+ emitter.emit(events.log.narration, theme.dim(`waiting ${timeout}ms...`));
483
468
  return await delay(timeout);
484
469
  },
485
- "wait-for-image": async (description, timeout = 10000, invert = false) => {
470
+ "wait-for-image": async (description, timeout = 10000) => {
486
471
  emitter.emit(
487
472
  events.log.narration,
488
473
  theme.dim(
@@ -499,8 +484,6 @@ const createCommands = (
499
484
  passed = await assert(
500
485
  `An image matching the description "${description}" appears on screen.`,
501
486
  false,
502
- false,
503
- invert,
504
487
  );
505
488
 
506
489
  durationPassed = new Date().getTime() - startTime;
@@ -531,12 +514,7 @@ const createCommands = (
531
514
  );
532
515
  }
533
516
  },
534
- "wait-for-text": async (
535
- text,
536
- timeout = 5000,
537
- method = "turbo",
538
- invert = false,
539
- ) => {
517
+ "wait-for-text": async (text, timeout = 5000) => {
540
518
  await redraw.start();
541
519
 
542
520
  emitter.emit(
@@ -551,25 +529,13 @@ const createCommands = (
551
529
  let passed = false;
552
530
 
553
531
  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
- );
532
+ const response = await sdk.req("find", {
533
+ element: text,
534
+ image: await system.captureScreenBase64(),
535
+ });
567
536
 
568
- passed = response.data;
537
+ passed = !!(response && response.coordinates);
569
538
 
570
- if (invert) {
571
- passed = !passed;
572
- }
573
539
  durationPassed = new Date().getTime() - startTime;
574
540
 
575
541
  if (!passed) {
@@ -597,8 +563,6 @@ const createCommands = (
597
563
  text,
598
564
  direction = "down",
599
565
  maxDistance = 10000,
600
- textMatchMethod = "turbo",
601
- method = "keyboard",
602
566
  invert = false,
603
567
  ) => {
604
568
  await redraw.start();
@@ -609,44 +573,17 @@ const createCommands = (
609
573
  true,
610
574
  );
611
575
 
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
576
  let scrollDistance = 0;
631
577
  let incrementDistance = 500;
632
578
  let passed = false;
633
579
 
634
580
  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
- );
581
+ const response = await sdk.req("find", {
582
+ element: text,
583
+ image: await system.captureScreenBase64(),
584
+ });
648
585
 
649
- passed = response.data;
586
+ passed = !!(response && response.coordinates);
650
587
 
651
588
  if (invert) {
652
589
  passed = !passed;
@@ -660,7 +597,7 @@ const createCommands = (
660
597
  ),
661
598
  true,
662
599
  );
663
- await scroll(direction, incrementDistance, method);
600
+ await scroll(direction, incrementDistance);
664
601
  scrollDistance = scrollDistance + incrementDistance;
665
602
  }
666
603
  }
@@ -678,7 +615,7 @@ const createCommands = (
678
615
  description,
679
616
  direction = "down",
680
617
  maxDistance = 10000,
681
- method = "keyboard",
618
+ method = "mouse",
682
619
  path,
683
620
  invert = false,
684
621
  ) => {
@@ -750,7 +687,6 @@ const createCommands = (
750
687
  await redraw.start();
751
688
 
752
689
  await sandbox.send({
753
- os: "linux",
754
690
  type: "commands.focus-application",
755
691
  name,
756
692
  });
@@ -764,8 +700,8 @@ const createCommands = (
764
700
  });
765
701
  return result.data;
766
702
  },
767
- assert: async (assertion, async = false, invert = false) => {
768
- let response = await assert(assertion, true, async, invert);
703
+ assert: async (assertion) => {
704
+ let response = await assert(assertion, true);
769
705
 
770
706
  return response;
771
707
  },
@@ -777,17 +713,36 @@ const createCommands = (
777
713
  let plat = system.platform();
778
714
 
779
715
  if (language == "pwsh" || language == "sh") {
716
+ if (language === "pwsh" && sandbox.os === "linux") {
717
+ emitter.emit(
718
+ events.log.log,
719
+ theme.yellow(
720
+ `⚠️ Warning: You are using 'pwsh' exec command on a Linux sandbox. This may fail. Consider using 'bash' or 'sh' for Linux environments.`,
721
+ ),
722
+ true,
723
+ );
724
+ }
725
+
726
+ if (language === "sh" && sandbox.os === "windows") {
727
+ emitter.emit(
728
+ events.log.log,
729
+ theme.yellow(
730
+ `⚠️ Warning: You are using 'sh' exec command on a Windows sandbox. This will fail. Automatically switching to 'pwsh' for Windows environments.`,
731
+ ),
732
+ true,
733
+ );
734
+ // Automatically switch to pwsh for Windows
735
+ language = "pwsh";
736
+ }
737
+
780
738
  let result = null;
781
739
 
782
740
  result = await sandbox.send({
783
- os: "linux",
784
741
  type: "commands.run",
785
742
  command: code,
786
743
  timeout,
787
744
  });
788
745
 
789
- console.log("Exec result:", result);
790
-
791
746
  if (result.out && result.out.returncode !== 0) {
792
747
  throw new MatchError(
793
748
  `Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,
@@ -4,8 +4,12 @@ const { events } = require("../events");
4
4
  const theme = require("./theme");
5
5
 
6
6
  // Factory function that creates redraw functionality with the provided system instance
7
- const createRedraw = (emitter, system, sandbox) => {
8
- const redrawThresholdPercent = 0.1;
7
+ const createRedraw = (
8
+ emitter,
9
+ system,
10
+ sandbox,
11
+ redrawThresholdPercent = 0.1,
12
+ ) => {
9
13
  const networkUpdateInterval = 15000;
10
14
 
11
15
  let lastTxBytes = null;
@@ -69,7 +73,6 @@ const createRedraw = (emitter, system, sandbox) => {
69
73
  async function updateNetwork() {
70
74
  if (sandbox && sandbox.instanceSocketConnected) {
71
75
  let network = await sandbox.send({
72
- os: "linux",
73
76
  type: "system.network",
74
77
  });
75
78
  parseNetworkStats(
@@ -14,6 +14,7 @@ const createSandbox = (emitter, analytics) => {
14
14
  this.instance = null;
15
15
  this.messageId = 0;
16
16
  this.uniqueId = Math.random().toString(36).substring(7);
17
+ this.os = null; // Store OS value to send with every message
17
18
  }
18
19
 
19
20
  send(message) {
@@ -24,6 +25,16 @@ const createSandbox = (emitter, analytics) => {
24
25
  this.messageId++;
25
26
  message.requestId = `${this.uniqueId}-${this.messageId}`;
26
27
 
28
+ // If os is set in the message, store it for future messages
29
+ if (message.os) {
30
+ this.os = message.os;
31
+ }
32
+
33
+ // Add os to every message if it's been set
34
+ if (this.os && !message.os) {
35
+ message.os = this.os;
36
+ }
37
+
27
38
  // Start timing for this message
28
39
  const timingKey = `sandbox-${message.type}`;
29
40
  marky.mark(timingKey);
@@ -51,7 +62,6 @@ const createSandbox = (emitter, analytics) => {
51
62
  async auth(apiKey) {
52
63
  let reply = await this.send({
53
64
  type: "authenticate",
54
- os: "linux",
55
65
  apiKey,
56
66
  });
57
67
 
@@ -65,7 +75,6 @@ const createSandbox = (emitter, analytics) => {
65
75
  async connect(sandboxId, persist = false) {
66
76
  let reply = await this.send({
67
77
  type: "connect",
68
- os: "linux",
69
78
  persist,
70
79
  sandboxId,
71
80
  });
@@ -73,9 +82,12 @@ const createSandbox = (emitter, analytics) => {
73
82
  if (reply.success) {
74
83
  this.instanceSocketConnected = true;
75
84
  emitter.emit(events.sandbox.connected);
85
+ // Return the full reply (includes url and sandbox)
86
+ return reply;
87
+ } else {
88
+ // Throw error to trigger fallback to creating new sandbox
89
+ throw new Error(reply.errorMessage || "Failed to connect to sandbox");
76
90
  }
77
-
78
- return reply.sandbox;
79
91
  }
80
92
 
81
93
  async boot(apiRoot) {
@@ -124,7 +136,9 @@ const createSandbox = (emitter, analytics) => {
124
136
 
125
137
  if (message.error) {
126
138
  emitter.emit(events.error.sandbox, message.errorMessage);
127
- this.ps[message.requestId].reject(JSON.stringify(message));
139
+ const error = new Error(message.errorMessage || "Sandbox error");
140
+ error.responseData = message;
141
+ this.ps[message.requestId].reject(error);
128
142
  } else {
129
143
  emitter.emit(events.sandbox.received);
130
144
 
package/agent/lib/sdk.js CHANGED
@@ -123,6 +123,7 @@ const createSDK = (emitter, config, sessionInstance) => {
123
123
  ...(token && { Authorization: `Bearer ${token}` }), // Add the authorization bearer token only if token is set
124
124
  },
125
125
  responseType: typeof onChunk === "function" ? "stream" : "json",
126
+ timeout: 60000, // 60 second timeout to prevent hanging requests
126
127
  data: {
127
128
  ...data,
128
129
  session: sessionInstance.get(),
@@ -8,7 +8,6 @@ const { events } = require("../events.js");
8
8
  const createSystem = (emitter, sandbox, config) => {
9
9
  const screenshot = async (options) => {
10
10
  let { base64 } = await sandbox.send({
11
- os: "linux",
12
11
  type: "system.screenshot",
13
12
  });
14
13
 
@@ -113,7 +112,6 @@ const createSystem = (emitter, sandbox, config) => {
113
112
  const activeWin = async () => {
114
113
  // Get Mouse Position from command line
115
114
  let result = await sandbox.send({
116
- os: "linux",
117
115
  type: "system.get-active-window",
118
116
  });
119
117
 
@@ -123,7 +121,6 @@ const createSystem = (emitter, sandbox, config) => {
123
121
  const getMousePosition = async () => {
124
122
  // Get Mouse Position from command line
125
123
  let result = await sandbox.send({
126
- os: "linux",
127
124
  type: "system.get-mouse-position",
128
125
  });
129
126