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.
- package/.github/workflows/acceptance-linux.yml +75 -0
- package/.github/workflows/acceptance-sdk-tests.yml +133 -0
- package/.vscode/settings.json +5 -1
- package/AGENTS.md +550 -0
- package/CODEOWNERS +0 -1
- package/README.md +126 -0
- package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
- package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
- package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
- package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
- package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
- package/_testdriver/lifecycle/prerun.yaml +15 -0
- package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
- package/agent/index.js +300 -85
- package/agent/interface.js +15 -0
- package/agent/lib/cache.js +142 -0
- package/agent/lib/commander.js +1 -39
- package/agent/lib/commands.js +910 -296
- package/agent/lib/redraw.js +129 -41
- package/agent/lib/sandbox.js +29 -6
- package/agent/lib/sdk.js +22 -0
- package/agent/lib/system.js +0 -3
- package/agent/lib/validation.js +1 -7
- package/debug-locate-response.js +82 -0
- package/debugger/index.html +15 -4
- package/docs/ARCHITECTURE.md +424 -0
- package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
- package/docs/MIGRATION.md +425 -0
- package/docs/PRESETS.md +210 -0
- package/docs/QUICK_START_TEST_RECORDING.md +215 -0
- package/docs/SDK_AWESOME_LOGS.md +468 -0
- package/docs/TEST_RECORDING.md +388 -0
- package/docs/docs.json +286 -152
- package/docs/guide/best-practices-polling.mdx +154 -0
- package/docs/sdk-browser-rendering.md +167 -0
- package/docs/v6/getting-started/self-hosting.mdx +407 -0
- package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
- package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
- package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
- package/docs/v6/overview/comparison.mdx +101 -0
- package/docs/v7/README.md +135 -0
- package/docs/v7/api/ai.mdx +205 -0
- package/docs/v7/api/assert.mdx +285 -0
- package/docs/v7/api/assertions.mdx +403 -0
- package/docs/v7/api/click.mdx +287 -0
- package/docs/v7/api/client.mdx +322 -0
- package/docs/v7/api/dashcam.mdx +497 -0
- package/docs/v7/api/doubleClick.mdx +102 -0
- package/docs/v7/api/elements.mdx +479 -0
- package/docs/v7/api/exec.mdx +346 -0
- package/docs/v7/api/find.mdx +316 -0
- package/docs/v7/api/focusApplication.mdx +294 -0
- package/docs/v7/api/hover.mdx +279 -0
- package/docs/v7/api/mouseDown.mdx +161 -0
- package/docs/v7/api/mouseUp.mdx +164 -0
- package/docs/v7/api/pressKeys.mdx +349 -0
- package/docs/v7/api/rightClick.mdx +123 -0
- package/docs/v7/api/sandbox.mdx +404 -0
- package/docs/v7/api/scroll.mdx +300 -0
- package/docs/v7/api/type.mdx +314 -0
- package/docs/v7/commands/assert.mdx +45 -0
- package/docs/v7/commands/exec.mdx +282 -0
- package/docs/v7/commands/focus-application.mdx +44 -0
- package/docs/v7/commands/hover-image.mdx +69 -0
- package/docs/v7/commands/hover-text.mdx +47 -0
- package/docs/v7/commands/if.mdx +53 -0
- package/docs/v7/commands/match-image.mdx +67 -0
- package/docs/v7/commands/press-keys.mdx +87 -0
- package/docs/v7/commands/remember.mdx +49 -0
- package/docs/v7/commands/run.mdx +44 -0
- package/docs/v7/commands/scroll-until-image.mdx +66 -0
- package/docs/v7/commands/scroll-until-text.mdx +60 -0
- package/docs/v7/commands/scroll.mdx +69 -0
- package/docs/v7/commands/type.mdx +45 -0
- package/docs/v7/commands/wait-for-image.mdx +54 -0
- package/docs/v7/commands/wait-for-text.mdx +48 -0
- package/docs/v7/commands/wait.mdx +45 -0
- package/docs/v7/getting-started/configuration.mdx +380 -0
- package/docs/v7/getting-started/quickstart.mdx +332 -0
- package/docs/v7/guides/best-practices.mdx +486 -0
- package/docs/v7/guides/caching-ai.mdx +215 -0
- package/docs/v7/guides/caching-selectors.mdx +292 -0
- package/docs/v7/guides/caching.mdx +366 -0
- package/docs/v7/guides/ci-cd/azure.mdx +587 -0
- package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
- package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
- package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
- package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
- package/docs/v7/guides/ci-cd/travis.mdx +438 -0
- package/docs/v7/guides/debugging.mdx +349 -0
- package/docs/v7/guides/faq.mdx +393 -0
- package/docs/v7/guides/migration.mdx +562 -0
- package/docs/v7/guides/performance.mdx +517 -0
- package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
- package/docs/v7/guides/troubleshooting.mdx +526 -0
- package/docs/v7/guides/vitest-plugin.mdx +477 -0
- package/docs/v7/guides/vitest.mdx +535 -0
- package/docs/v7/platforms/linux.mdx +308 -0
- package/docs/v7/platforms/macos.mdx +433 -0
- package/docs/v7/platforms/windows.mdx +430 -0
- package/docs/v7/playwright.mdx +342 -0
- package/docs/v7/presets/chrome-extension.mdx +223 -0
- package/docs/v7/presets/chrome.mdx +287 -0
- package/docs/v7/presets/electron.mdx +435 -0
- package/docs/v7/presets/vscode.mdx +398 -0
- package/docs/v7/presets/webapp.mdx +396 -0
- package/docs/v7/progressive-apis/CORE.md +459 -0
- package/docs/v7/progressive-apis/HOOKS.md +360 -0
- package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
- package/docs/v7/progressive-apis/PROVISION.md +266 -0
- package/eslint.config.js +19 -1
- package/interfaces/cli/lib/base.js +10 -4
- package/interfaces/logger.js +2 -1
- package/interfaces/shared-test-state.mjs +69 -0
- package/interfaces/vitest-plugin.mjs +830 -0
- package/package.json +29 -5
- package/schema.json +8 -29
- package/scripts/view-test-results.mjs +96 -0
- package/sdk-log-formatter.js +714 -0
- package/sdk.d.ts +1028 -0
- package/sdk.js +2567 -0
- package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
- package/setup/aws/cloudformation.yaml +9 -2
- package/src/core/Dashcam.js +469 -0
- package/src/core/index.d.ts +150 -0
- package/src/core/index.js +12 -0
- package/src/presets/index.mjs +331 -0
- package/src/vitest/extended.mjs +108 -0
- package/src/vitest/hooks.d.ts +119 -0
- package/src/vitest/hooks.mjs +298 -0
- package/src/vitest/index.mjs +64 -0
- package/src/vitest/lifecycle.mjs +277 -0
- package/src/vitest/utils.mjs +150 -0
- package/test/dashcam.test.js +137 -0
- package/test/mcp-example-test.yaml +27 -0
- package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
- package/testdriver/acceptance-sdk/README.md +128 -0
- package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
- package/testdriver/acceptance-sdk/assert.test.mjs +26 -0
- package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
- package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
- package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +58 -0
- package/testdriver/acceptance-sdk/element-not-found.test.mjs +25 -0
- package/testdriver/acceptance-sdk/exec-js.test.mjs +43 -0
- package/testdriver/acceptance-sdk/exec-output.test.mjs +59 -0
- package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +57 -0
- package/testdriver/acceptance-sdk/focus-window.test.mjs +36 -0
- package/testdriver/acceptance-sdk/formatted-logging.test.mjs +26 -0
- package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
- package/testdriver/acceptance-sdk/hover-image.test.mjs +34 -0
- package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +38 -0
- package/testdriver/acceptance-sdk/hover-text.test.mjs +27 -0
- package/testdriver/acceptance-sdk/match-image.test.mjs +36 -0
- package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
- package/testdriver/acceptance-sdk/press-keys.test.mjs +50 -0
- package/testdriver/acceptance-sdk/prompt.test.mjs +33 -0
- package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +38 -0
- package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +39 -0
- package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +28 -0
- package/testdriver/acceptance-sdk/scroll.test.mjs +41 -0
- package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
- package/testdriver/acceptance-sdk/setup/testHelpers.mjs +420 -0
- package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
- package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
- package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
- package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
- package/testdriver/acceptance-sdk/type.test.mjs +45 -0
- package/verify-element-api.js +89 -0
- package/verify-types.js +0 -0
- package/vitest.config.example.js +19 -0
- package/vitest.config.mjs +66 -0
- package/vitest.config.mjs.bak +44 -0
- package/.github/workflows/acceptance-v6.yml +0 -169
- package/.vscode/mcp.json +0 -9
- package/docs/overview/comparison.mdx +0 -82
- package/testdriver/lifecycle/prerun.yaml +0 -17
- /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
- /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
- /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
- /package/docs/{account → v6/account}/dashboard.mdx +0 -0
- /package/docs/{account → v6/account}/enterprise.mdx +0 -0
- /package/docs/{account → v6/account}/pricing.mdx +0 -0
- /package/docs/{account → v6/account}/projects.mdx +0 -0
- /package/docs/{account → v6/account}/team.mdx +0 -0
- /package/docs/{action → v6/action}/ami.mdx +0 -0
- /package/docs/{action → v6/action}/performance.mdx +0 -0
- /package/docs/{action → v6/action}/secrets.mdx +0 -0
- /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
- /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
- /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
- /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
- /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
- /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
- /package/docs/{cli → v6/cli}/overview.mdx +0 -0
- /package/docs/{commands → v6/commands}/assert.mdx +0 -0
- /package/docs/{commands → v6/commands}/exec.mdx +0 -0
- /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
- /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
- /package/docs/{commands → v6/commands}/if.mdx +0 -0
- /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
- /package/docs/{commands → v6/commands}/remember.mdx +0 -0
- /package/docs/{commands → v6/commands}/run.mdx +0 -0
- /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
- /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
- /package/docs/{commands → v6/commands}/type.mdx +0 -0
- /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
- /package/docs/{commands → v6/commands}/wait.mdx +0 -0
- /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
- /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
- /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
- /package/docs/{features → v6/features}/generation.mdx +0 -0
- /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
- /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
- /package/docs/{features → v6/features}/selectorless.mdx +0 -0
- /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
- /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
- /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
- /package/docs/{guide → v6/guide}/code.mdx +0 -0
- /package/docs/{guide → v6/guide}/locating.mdx +0 -0
- /package/docs/{guide → v6/guide}/protips.mdx +0 -0
- /package/docs/{guide → v6/guide}/variables.mdx +0 -0
- /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
- /package/docs/{importing → v6/importing}/csv.mdx +0 -0
- /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
- /package/docs/{importing → v6/importing}/jira.mdx +0 -0
- /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
- /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
- /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
- /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
- /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
- /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
- /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
- /package/docs/{overview → v6/overview}/faq.mdx +0 -0
- /package/docs/{overview → v6/overview}/performance.mdx +0 -0
- /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
- /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
- /package/docs/{security → v6/security}/action.mdx +0 -0
- /package/docs/{security → v6/security}/agent.mdx +0 -0
- /package/docs/{security → v6/security}/platform.mdx +0 -0
- /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
- /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
package/agent/lib/commands.js
CHANGED
|
@@ -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
|
|
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,
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
372
|
+
let button = "left";
|
|
373
|
+
let double = false;
|
|
298
374
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
375
|
+
if (action === "right-click") {
|
|
376
|
+
button = "right";
|
|
377
|
+
}
|
|
378
|
+
if (action === "double-click") {
|
|
379
|
+
double = true;
|
|
380
|
+
}
|
|
305
381
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
382
|
+
emitter.emit(
|
|
383
|
+
events.log.narration,
|
|
384
|
+
theme.dim(`${action} ${button} clicking at ${x}, ${y}...`),
|
|
385
|
+
true,
|
|
386
|
+
);
|
|
311
387
|
|
|
312
|
-
|
|
313
|
-
|
|
388
|
+
x = parseInt(x);
|
|
389
|
+
y = parseInt(y);
|
|
314
390
|
|
|
315
|
-
|
|
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
|
-
|
|
399
|
+
await sandbox.send({ type: "moveMouse", x, y, ...elementData });
|
|
318
400
|
|
|
319
|
-
|
|
401
|
+
emitter.emit(events.mouseMove, { x, y });
|
|
320
402
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
type: "
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
};
|
|
521
|
+
await redraw.start(redrawOptions);
|
|
347
522
|
|
|
348
|
-
|
|
349
|
-
|
|
523
|
+
x = parseInt(x);
|
|
524
|
+
y = parseInt(y);
|
|
350
525
|
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
+
await redraw.wait(2500, redrawOptions);
|
|
357
560
|
|
|
358
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
+
await click({ x: response.coordinates.x, y: response.coordinates.y, action });
|
|
432
689
|
}
|
|
690
|
+
|
|
691
|
+
return response;
|
|
433
692
|
},
|
|
434
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
765
|
+
text = text.toString();
|
|
464
766
|
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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({
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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.
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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 &&
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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}`,
|