@testdriverai/agent 7.8.0-test.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +4 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +1 -0
  5. package/CHANGELOG.md +953 -0
  6. package/README.md +81 -0
  7. package/agent/events.js +135 -0
  8. package/agent/index.js +2450 -0
  9. package/agent/interface.js +35 -0
  10. package/agent/lib/analytics.js +22 -0
  11. package/agent/lib/censorship.js +75 -0
  12. package/agent/lib/commander.js +246 -0
  13. package/agent/lib/commands.js +1684 -0
  14. package/agent/lib/config.js +60 -0
  15. package/agent/lib/generator.js +91 -0
  16. package/agent/lib/http.js +144 -0
  17. package/agent/lib/logger.js +56 -0
  18. package/agent/lib/outputs.js +29 -0
  19. package/agent/lib/parser.js +209 -0
  20. package/agent/lib/redraw.js +386 -0
  21. package/agent/lib/resources/cursor-2.png +0 -0
  22. package/agent/lib/sandbox.js +1104 -0
  23. package/agent/lib/sdk.js +633 -0
  24. package/agent/lib/session.js +25 -0
  25. package/agent/lib/source-mapper.js +342 -0
  26. package/agent/lib/subimage/index.js +77 -0
  27. package/agent/lib/subimage/opencv.js +69 -0
  28. package/agent/lib/system.js +204 -0
  29. package/agent/lib/theme.js +14 -0
  30. package/agent/lib/valid-version.js +21 -0
  31. package/agent/lib/validation.js +169 -0
  32. package/ai/.claude-plugin/plugin.json +9 -0
  33. package/ai/agents/testdriver.md +638 -0
  34. package/ai/skills/testdriver-ai/SKILL.md +204 -0
  35. package/ai/skills/testdriver-assert/SKILL.md +315 -0
  36. package/ai/skills/testdriver-aws-setup/SKILL.md +448 -0
  37. package/ai/skills/testdriver-cache/SKILL.md +221 -0
  38. package/ai/skills/testdriver-caching/SKILL.md +124 -0
  39. package/ai/skills/testdriver-captcha/SKILL.md +158 -0
  40. package/ai/skills/testdriver-ci-cd/SKILL.md +602 -0
  41. package/ai/skills/testdriver-click/SKILL.md +286 -0
  42. package/ai/skills/testdriver-client/SKILL.md +477 -0
  43. package/ai/skills/testdriver-cloud/SKILL.md +119 -0
  44. package/ai/skills/testdriver-customizing-devices/SKILL.md +319 -0
  45. package/ai/skills/testdriver-dashcam/SKILL.md +418 -0
  46. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +401 -0
  47. package/ai/skills/testdriver-device-config/SKILL.md +317 -0
  48. package/ai/skills/testdriver-double-click/SKILL.md +102 -0
  49. package/ai/skills/testdriver-elements/SKILL.md +605 -0
  50. package/ai/skills/testdriver-enterprise/SKILL.md +114 -0
  51. package/ai/skills/testdriver-errors/SKILL.md +246 -0
  52. package/ai/skills/testdriver-events/SKILL.md +356 -0
  53. package/ai/skills/testdriver-examples/SKILL.md +7 -0
  54. package/ai/skills/testdriver-exec/SKILL.md +317 -0
  55. package/ai/skills/testdriver-find/SKILL.md +829 -0
  56. package/ai/skills/testdriver-focus-application/SKILL.md +293 -0
  57. package/ai/skills/testdriver-generating-tests/SKILL.md +36 -0
  58. package/ai/skills/testdriver-hover/SKILL.md +278 -0
  59. package/ai/skills/testdriver-locating-elements/SKILL.md +71 -0
  60. package/ai/skills/testdriver-making-assertions/SKILL.md +32 -0
  61. package/ai/skills/testdriver-mcp/SKILL.md +7 -0
  62. package/ai/skills/testdriver-mcp-workflow/SKILL.md +410 -0
  63. package/ai/skills/testdriver-mouse-down/SKILL.md +161 -0
  64. package/ai/skills/testdriver-mouse-up/SKILL.md +164 -0
  65. package/ai/skills/testdriver-parse/SKILL.md +236 -0
  66. package/ai/skills/testdriver-performing-actions/SKILL.md +54 -0
  67. package/ai/skills/testdriver-press-keys/SKILL.md +348 -0
  68. package/ai/skills/testdriver-provision/SKILL.md +331 -0
  69. package/ai/skills/testdriver-quickstart/SKILL.md +144 -0
  70. package/ai/skills/testdriver-redraw/SKILL.md +214 -0
  71. package/ai/skills/testdriver-reusable-code/SKILL.md +249 -0
  72. package/ai/skills/testdriver-right-click/SKILL.md +123 -0
  73. package/ai/skills/testdriver-running-tests/SKILL.md +185 -0
  74. package/ai/skills/testdriver-screenshot/SKILL.md +248 -0
  75. package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
  76. package/ai/skills/testdriver-scroll/SKILL.md +335 -0
  77. package/ai/skills/testdriver-secrets/SKILL.md +115 -0
  78. package/ai/skills/testdriver-self-hosted/SKILL.md +65 -0
  79. package/ai/skills/testdriver-test-writer/SKILL.md +448 -0
  80. package/ai/skills/testdriver-testdriver/SKILL.md +628 -0
  81. package/ai/skills/testdriver-testdriver-mechanic/SKILL.md +165 -0
  82. package/ai/skills/testdriver-type/SKILL.md +357 -0
  83. package/ai/skills/testdriver-variables/SKILL.md +111 -0
  84. package/ai/skills/testdriver-wait/SKILL.md +50 -0
  85. package/ai/skills/testdriver-waiting-for-elements/SKILL.md +90 -0
  86. package/ai/skills/testdriver-what-is-testdriver/SKILL.md +54 -0
  87. package/bin/testdriverai.js +22 -0
  88. package/debugger/bg.png +0 -0
  89. package/debugger/icon.png +0 -0
  90. package/debugger/index.html +469 -0
  91. package/debugger/td.png +0 -0
  92. package/debugger/tray-buffered.png +0 -0
  93. package/debugger/tray.png +0 -0
  94. package/docs/GITHUB_COMMENTS.md +330 -0
  95. package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
  96. package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
  97. package/docs/TEST-GITHUB-COMMENTS.md +129 -0
  98. package/docs/_data/examples-manifest.json +177 -0
  99. package/docs/_data/examples-manifest.schema.json +41 -0
  100. package/docs/_scripts/extract-example-urls.js +165 -0
  101. package/docs/_scripts/generate-examples.js +560 -0
  102. package/docs/_scripts/generate-skills.js +154 -0
  103. package/docs/_scripts/link-replacer.js +164 -0
  104. package/docs/_scripts/upload-docs-to-openai.js +284 -0
  105. package/docs/changelog.mdx +161 -0
  106. package/docs/claude-mcp-plugin.mdx +160 -0
  107. package/docs/docs.json +442 -0
  108. package/docs/github-integration-setup.md +266 -0
  109. package/docs/guide/best-practices-polling.mdx +174 -0
  110. package/docs/images/content/account/newprojectsettings.png +0 -0
  111. package/docs/images/content/account/projectpage.png +0 -0
  112. package/docs/images/content/account/projectreplays.png +0 -0
  113. package/docs/images/content/account/team-manage.png +0 -0
  114. package/docs/images/content/account/teampage.png +0 -0
  115. package/docs/images/content/extension/cursor.svg +1 -0
  116. package/docs/images/content/extension/vscode.svg +57 -0
  117. package/docs/images/content/extension/windsurf.svg +3 -0
  118. package/docs/images/content/parse/output.png +0 -0
  119. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  120. package/docs/images/content/side-by-side.png +0 -0
  121. package/docs/images/content/vscode/ide-full.png +0 -0
  122. package/docs/images/content/vscode/running.png +0 -0
  123. package/docs/images/content/vscode/v7-chat.png +0 -0
  124. package/docs/images/content/vscode/v7-choose-agent.png +0 -0
  125. package/docs/images/content/vscode/v7-full.png +0 -0
  126. package/docs/images/content/vscode/v7-onboarding.png +0 -0
  127. package/docs/images/content/vscode/vscode-2-assert.png +0 -0
  128. package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
  129. package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
  130. package/docs/images/content/vscode/vscode-file-creation.png +0 -0
  131. package/docs/images/content/vscode/vscode-install.png +0 -0
  132. package/docs/images/content/vscode/vscode-overview.png +0 -0
  133. package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
  134. package/docs/images/content/vscode/vscode-stopchat.png +0 -0
  135. package/docs/images/content/vscode/vscode-stoptest.png +0 -0
  136. package/docs/images/content/vscode/vscode-tdservice.png +0 -0
  137. package/docs/images/content/vscode/vscode-test-output.png +0 -0
  138. package/docs/images/content/vscode/vscode-testhistory.png +0 -0
  139. package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
  140. package/docs/images/content/vscode/vscode-testpane.png +0 -0
  141. package/docs/images/template/dark.png +0 -0
  142. package/docs/images/template/icon.png +0 -0
  143. package/docs/images/template/light.png +0 -0
  144. package/docs/snippets/calendar-link.mdx +4 -0
  145. package/docs/snippets/gitignore-warning.mdx +7 -0
  146. package/docs/snippets/lifecycle-warning.mdx +6 -0
  147. package/docs/snippets/test-prereqs.mdx +12 -0
  148. package/docs/snippets/tests/assert-replay.mdx +7 -0
  149. package/docs/snippets/tests/assert-yaml.mdx +8 -0
  150. package/docs/snippets/tests/exec-js-replay.mdx +7 -0
  151. package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
  152. package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
  153. package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
  154. package/docs/snippets/tests/hover-image-replay.mdx +7 -0
  155. package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
  156. package/docs/snippets/tests/hover-text-replay.mdx +7 -0
  157. package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
  158. package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
  159. package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
  160. package/docs/snippets/tests/match-image-replay.mdx +7 -0
  161. package/docs/snippets/tests/match-image-yaml.mdx +17 -0
  162. package/docs/snippets/tests/press-keys-replay.mdx +7 -0
  163. package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
  164. package/docs/snippets/tests/remember-replay.mdx +7 -0
  165. package/docs/snippets/tests/remember-yaml.mdx +28 -0
  166. package/docs/snippets/tests/scroll-replay.mdx +7 -0
  167. package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
  168. package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
  169. package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
  170. package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
  171. package/docs/snippets/tests/scroll-yaml.mdx +30 -0
  172. package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
  173. package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
  174. package/docs/snippets/tests/type-replay.mdx +7 -0
  175. package/docs/snippets/tests/type-yaml.mdx +28 -0
  176. package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
  177. package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
  178. package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
  179. package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
  180. package/docs/snippets/tests/wait-replay.mdx +7 -0
  181. package/docs/snippets/tests/wait-yaml.mdx +13 -0
  182. package/docs/styles.css +65 -0
  183. package/docs/v6/account/dashboard.mdx +16 -0
  184. package/docs/v6/account/enterprise.mdx +110 -0
  185. package/docs/v6/account/pricing.mdx +33 -0
  186. package/docs/v6/account/projects.mdx +33 -0
  187. package/docs/v6/account/team.mdx +35 -0
  188. package/docs/v6/action/ami.mdx +109 -0
  189. package/docs/v6/action/performance.mdx +105 -0
  190. package/docs/v6/action/secrets.mdx +93 -0
  191. package/docs/v6/apps/chrome-extensions.mdx +48 -0
  192. package/docs/v6/apps/desktop-apps.mdx +93 -0
  193. package/docs/v6/apps/mobile-apps.mdx +26 -0
  194. package/docs/v6/apps/static-websites.mdx +54 -0
  195. package/docs/v6/apps/tauri-apps.mdx +361 -0
  196. package/docs/v6/bugs/jira.mdx +232 -0
  197. package/docs/v6/cli/overview.mdx +66 -0
  198. package/docs/v6/commands/assert.mdx +45 -0
  199. package/docs/v6/commands/exec.mdx +276 -0
  200. package/docs/v6/commands/focus-application.mdx +44 -0
  201. package/docs/v6/commands/hover-image.mdx +69 -0
  202. package/docs/v6/commands/hover-text.mdx +47 -0
  203. package/docs/v6/commands/if.mdx +53 -0
  204. package/docs/v6/commands/match-image.mdx +67 -0
  205. package/docs/v6/commands/press-keys.mdx +87 -0
  206. package/docs/v6/commands/remember.mdx +49 -0
  207. package/docs/v6/commands/run.mdx +44 -0
  208. package/docs/v6/commands/scroll-until-image.mdx +66 -0
  209. package/docs/v6/commands/scroll-until-text.mdx +60 -0
  210. package/docs/v6/commands/scroll.mdx +69 -0
  211. package/docs/v6/commands/type.mdx +45 -0
  212. package/docs/v6/commands/wait-for-image.mdx +54 -0
  213. package/docs/v6/commands/wait-for-text.mdx +48 -0
  214. package/docs/v6/commands/wait.mdx +45 -0
  215. package/docs/v6/exporting/junit.mdx +218 -0
  216. package/docs/v6/exporting/playwright.mdx +197 -0
  217. package/docs/v6/features/auto-healing.mdx +144 -0
  218. package/docs/v6/features/generation.mdx +116 -0
  219. package/docs/v6/features/parallel-testing.mdx +151 -0
  220. package/docs/v6/features/reusable-snippets.mdx +131 -0
  221. package/docs/v6/features/selectorless.mdx +80 -0
  222. package/docs/v6/features/visual-assertions.mdx +139 -0
  223. package/docs/v6/getting-started/ci.mdx +146 -0
  224. package/docs/v6/getting-started/cli.mdx +91 -0
  225. package/docs/v6/getting-started/editing.mdx +100 -0
  226. package/docs/v6/getting-started/playwright.mdx +342 -0
  227. package/docs/v6/getting-started/running.mdx +48 -0
  228. package/docs/v6/getting-started/self-hosting.mdx +408 -0
  229. package/docs/v6/getting-started/vscode.mdx +88 -0
  230. package/docs/v6/guide/assertions.mdx +189 -0
  231. package/docs/v6/guide/authentication.mdx +136 -0
  232. package/docs/v6/guide/code.mdx +65 -0
  233. package/docs/v6/guide/dashcam.mdx +118 -0
  234. package/docs/v6/guide/environment-variables.mdx +26 -0
  235. package/docs/v6/guide/lifecycle.mdx +242 -0
  236. package/docs/v6/guide/locating.mdx +141 -0
  237. package/docs/v6/guide/protips.mdx +43 -0
  238. package/docs/v6/guide/variables.mdx +143 -0
  239. package/docs/v6/guide/waiting.mdx +130 -0
  240. package/docs/v6/importing/csv.mdx +196 -0
  241. package/docs/v6/importing/gherkin.mdx +143 -0
  242. package/docs/v6/importing/jira.mdx +164 -0
  243. package/docs/v6/importing/testrail.mdx +162 -0
  244. package/docs/v6/integrations/electron.mdx +146 -0
  245. package/docs/v6/integrations/netlify.mdx +100 -0
  246. package/docs/v6/integrations/vercel.mdx +125 -0
  247. package/docs/v6/interactive/explore.mdx +99 -0
  248. package/docs/v6/interactive/run.mdx +52 -0
  249. package/docs/v6/interactive/save.mdx +63 -0
  250. package/docs/v6/overview/comparison.mdx +101 -0
  251. package/docs/v6/overview/faq.mdx +162 -0
  252. package/docs/v6/overview/performance.mdx +52 -0
  253. package/docs/v6/overview/quickstart.mdx +137 -0
  254. package/docs/v6/overview/what-is-testdriver.mdx +85 -0
  255. package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
  256. package/docs/v6/scenarios/cookie-banner.mdx +32 -0
  257. package/docs/v6/scenarios/file-upload.mdx +33 -0
  258. package/docs/v6/scenarios/form-filling.mdx +32 -0
  259. package/docs/v6/scenarios/log-in.mdx +75 -0
  260. package/docs/v6/scenarios/pdf-generation.mdx +25 -0
  261. package/docs/v6/scenarios/spell-check.mdx +22 -0
  262. package/docs/v6/security/action.mdx +84 -0
  263. package/docs/v6/security/agent.mdx +73 -0
  264. package/docs/v6/security/platform.mdx +77 -0
  265. package/docs/v6/tutorials/advanced-test.mdx +81 -0
  266. package/docs/v6/tutorials/basic-test.mdx +45 -0
  267. package/docs/v7/_drafts/agents.mdx +843 -0
  268. package/docs/v7/_drafts/architecture.mdx +399 -0
  269. package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
  270. package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -0
  271. package/docs/v7/_drafts/best-practices.mdx +486 -0
  272. package/docs/v7/_drafts/caching-ai.mdx +215 -0
  273. package/docs/v7/_drafts/caching-selectors.mdx +424 -0
  274. package/docs/v7/_drafts/caching.mdx +366 -0
  275. package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
  276. package/docs/v7/_drafts/commands/assert.mdx +45 -0
  277. package/docs/v7/_drafts/commands/exec.mdx +276 -0
  278. package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
  279. package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
  280. package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
  281. package/docs/v7/_drafts/commands/if.mdx +53 -0
  282. package/docs/v7/_drafts/commands/match-image.mdx +67 -0
  283. package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
  284. package/docs/v7/_drafts/commands/remember.mdx +49 -0
  285. package/docs/v7/_drafts/commands/run.mdx +44 -0
  286. package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
  287. package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
  288. package/docs/v7/_drafts/commands/scroll.mdx +69 -0
  289. package/docs/v7/_drafts/commands/type.mdx +45 -0
  290. package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
  291. package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
  292. package/docs/v7/_drafts/commands/wait.mdx +45 -0
  293. package/docs/v7/_drafts/configuration.mdx +378 -0
  294. package/docs/v7/_drafts/contributing.mdx +174 -0
  295. package/docs/v7/_drafts/core.mdx +458 -0
  296. package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
  297. package/docs/v7/_drafts/debugging.mdx +349 -0
  298. package/docs/v7/_drafts/error-handling.mdx +501 -0
  299. package/docs/v7/_drafts/faq.mdx +393 -0
  300. package/docs/v7/_drafts/hooks.mdx +360 -0
  301. package/docs/v7/_drafts/init-command.mdx +95 -0
  302. package/docs/v7/_drafts/installation.mdx +420 -0
  303. package/docs/v7/_drafts/migration.mdx +562 -0
  304. package/docs/v7/_drafts/observable.mdx +604 -0
  305. package/docs/v7/_drafts/playwright.mdx +342 -0
  306. package/docs/v7/_drafts/plugin-migration.mdx +220 -0
  307. package/docs/v7/_drafts/powerful.mdx +419 -0
  308. package/docs/v7/_drafts/presets.mdx +210 -0
  309. package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
  310. package/docs/v7/_drafts/prompt-cache.mdx +200 -0
  311. package/docs/v7/_drafts/provision.mdx +390 -0
  312. package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
  313. package/docs/v7/_drafts/readme.mdx +135 -0
  314. package/docs/v7/_drafts/reports.mdx +414 -0
  315. package/docs/v7/_drafts/scalable.mdx +763 -0
  316. package/docs/v7/_drafts/screenshot.mdx +155 -0
  317. package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
  318. package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
  319. package/docs/v7/_drafts/sdk-migration.mdx +474 -0
  320. package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
  321. package/docs/v7/_drafts/self-hosting.mdx +369 -0
  322. package/docs/v7/_drafts/test-recording.mdx +382 -0
  323. package/docs/v7/_drafts/troubleshooting.mdx +526 -0
  324. package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
  325. package/docs/v7/_drafts/vitest.mdx +535 -0
  326. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  327. package/docs/v7/ai.mdx +205 -0
  328. package/docs/v7/assert.mdx +316 -0
  329. package/docs/v7/aws-setup.mdx +449 -0
  330. package/docs/v7/cache.mdx +223 -0
  331. package/docs/v7/caching.mdx +128 -0
  332. package/docs/v7/captcha.mdx +159 -0
  333. package/docs/v7/ci-cd.mdx +603 -0
  334. package/docs/v7/click.mdx +287 -0
  335. package/docs/v7/client.mdx +478 -0
  336. package/docs/v7/copilot/auto-healing.mdx +265 -0
  337. package/docs/v7/copilot/creating-tests.mdx +156 -0
  338. package/docs/v7/copilot/github.mdx +143 -0
  339. package/docs/v7/copilot/running-tests.mdx +149 -0
  340. package/docs/v7/copilot/setup.mdx +143 -0
  341. package/docs/v7/customizing-devices.mdx +319 -0
  342. package/docs/v7/dashcam.mdx +419 -0
  343. package/docs/v7/debugging-with-screenshots.mdx +402 -0
  344. package/docs/v7/device-config.mdx +317 -0
  345. package/docs/v7/double-click.mdx +102 -0
  346. package/docs/v7/elements.mdx +606 -0
  347. package/docs/v7/enterprise.mdx +9 -0
  348. package/docs/v7/errors.mdx +248 -0
  349. package/docs/v7/events.mdx +358 -0
  350. package/docs/v7/examples/ai.mdx +72 -0
  351. package/docs/v7/examples/assert.mdx +72 -0
  352. package/docs/v7/examples/captcha-api.mdx +92 -0
  353. package/docs/v7/examples/chrome-extension.mdx +132 -0
  354. package/docs/v7/examples/drag-and-drop.mdx +100 -0
  355. package/docs/v7/examples/element-not-found.mdx +67 -0
  356. package/docs/v7/examples/exec-output.mdx +85 -0
  357. package/docs/v7/examples/exec-pwsh.mdx +83 -0
  358. package/docs/v7/examples/focus-window.mdx +62 -0
  359. package/docs/v7/examples/hover-image.mdx +94 -0
  360. package/docs/v7/examples/hover-text.mdx +69 -0
  361. package/docs/v7/examples/installer.mdx +91 -0
  362. package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
  363. package/docs/v7/examples/match-image.mdx +96 -0
  364. package/docs/v7/examples/press-keys.mdx +92 -0
  365. package/docs/v7/examples/scroll-keyboard.mdx +79 -0
  366. package/docs/v7/examples/scroll-until-image.mdx +81 -0
  367. package/docs/v7/examples/scroll-until-text.mdx +109 -0
  368. package/docs/v7/examples/scroll.mdx +81 -0
  369. package/docs/v7/examples/type.mdx +92 -0
  370. package/docs/v7/examples/windows-installer.mdx +89 -0
  371. package/docs/v7/exec.mdx +318 -0
  372. package/docs/v7/find.mdx +830 -0
  373. package/docs/v7/focus-application.mdx +294 -0
  374. package/docs/v7/generating-tests.mdx +36 -0
  375. package/docs/v7/hosted.mdx +158 -0
  376. package/docs/v7/hover.mdx +279 -0
  377. package/docs/v7/locating-elements.mdx +71 -0
  378. package/docs/v7/making-assertions.mdx +32 -0
  379. package/docs/v7/mcp.mdx +9 -0
  380. package/docs/v7/mouse-down.mdx +161 -0
  381. package/docs/v7/mouse-up.mdx +164 -0
  382. package/docs/v7/parse.mdx +237 -0
  383. package/docs/v7/performing-actions.mdx +54 -0
  384. package/docs/v7/press-keys.mdx +349 -0
  385. package/docs/v7/provision.mdx +333 -0
  386. package/docs/v7/quickstart.mdx +173 -0
  387. package/docs/v7/redraw.mdx +216 -0
  388. package/docs/v7/reusable-code.mdx +249 -0
  389. package/docs/v7/right-click.mdx +123 -0
  390. package/docs/v7/running-tests.mdx +185 -0
  391. package/docs/v7/screenshot.mdx +249 -0
  392. package/docs/v7/screenshots.mdx +186 -0
  393. package/docs/v7/scroll.mdx +336 -0
  394. package/docs/v7/secrets.mdx +115 -0
  395. package/docs/v7/self-hosted.mdx +149 -0
  396. package/docs/v7/type.mdx +358 -0
  397. package/docs/v7/variables.mdx +111 -0
  398. package/docs/v7/wait.mdx +52 -0
  399. package/docs/v7/waiting-for-elements.mdx +90 -0
  400. package/docs/v7/what-is-testdriver.mdx +54 -0
  401. package/eslint.config.js +67 -0
  402. package/examples/ai.test.mjs +31 -0
  403. package/examples/assert.test.mjs +47 -0
  404. package/examples/chrome-extension.test.mjs +97 -0
  405. package/examples/config.mjs +5 -0
  406. package/examples/element-not-found.test.mjs +27 -0
  407. package/examples/exec-output.test.mjs +60 -0
  408. package/examples/exec-pwsh.test.mjs +58 -0
  409. package/examples/findall-coffee-icons.test.mjs +42 -0
  410. package/examples/focus-window.test.mjs +37 -0
  411. package/examples/formatted-logging.test.mjs +27 -0
  412. package/examples/hover-image.test.mjs +53 -0
  413. package/examples/hover-text-with-description.test.mjs +57 -0
  414. package/examples/hover-text.test.mjs +28 -0
  415. package/examples/installer.test.mjs +50 -0
  416. package/examples/launch-vscode-linux.test.mjs +55 -0
  417. package/examples/match-image.test.mjs +55 -0
  418. package/examples/parse.test.mjs +19 -0
  419. package/examples/press-keys.test.mjs +44 -0
  420. package/examples/prompt.test.mjs +34 -0
  421. package/examples/scroll-keyboard.test.mjs +38 -0
  422. package/examples/scroll-until-image.test.mjs +40 -0
  423. package/examples/scroll.test.mjs +42 -0
  424. package/examples/type.test.mjs +46 -0
  425. package/examples/windows-installer.test.mjs +54 -0
  426. package/index.js +2 -0
  427. package/interfaces/cli/commands/init.js +438 -0
  428. package/interfaces/cli/commands/setup.js +382 -0
  429. package/interfaces/cli/lib/base.js +285 -0
  430. package/interfaces/cli.js +20 -0
  431. package/interfaces/junit-reporter.js +290 -0
  432. package/interfaces/logger.js +388 -0
  433. package/interfaces/readline.js +234 -0
  434. package/interfaces/shared-test-state.mjs +64 -0
  435. package/interfaces/vitest-plugin.d.ts +115 -0
  436. package/interfaces/vitest-plugin.mjs +1698 -0
  437. package/lib/captcha/solver.js +358 -0
  438. package/lib/core/Dashcam.js +533 -0
  439. package/lib/core/index.d.ts +172 -0
  440. package/lib/core/index.js +12 -0
  441. package/lib/environments.json +18 -0
  442. package/lib/github-comment-formatter.js +263 -0
  443. package/lib/github-comment.mjs +452 -0
  444. package/lib/init-project.js +575 -0
  445. package/lib/presets/index.mjs +331 -0
  446. package/lib/resolve-channel.js +46 -0
  447. package/lib/sentry.js +417 -0
  448. package/lib/vitest/hooks.d.ts +57 -0
  449. package/lib/vitest/hooks.mjs +674 -0
  450. package/lib/vitest/setup-aws.mjs +247 -0
  451. package/lib/vitest/setup-self-hosted.mjs +151 -0
  452. package/lib/vitest/setup.mjs +46 -0
  453. package/manual/captcha-api.test.mjs +51 -0
  454. package/manual/drag-and-drop.test.mjs +59 -0
  455. package/manual/flake-diffthreshold-001.test.mjs +9 -0
  456. package/manual/flake-diffthreshold-01.test.mjs +9 -0
  457. package/manual/flake-diffthreshold-05.test.mjs +9 -0
  458. package/manual/flake-noredraw-cache.test.mjs +9 -0
  459. package/manual/flake-noredraw-nocache.test.mjs +9 -0
  460. package/manual/flake-redraw-cache.test.mjs +9 -0
  461. package/manual/flake-redraw-nocache.test.mjs +9 -0
  462. package/manual/flake-rocket-match.test.mjs +30 -0
  463. package/manual/flake-shared.mjs +51 -0
  464. package/manual/no-provision.test.mjs +31 -0
  465. package/manual/packer-hover-image.test.mjs +176 -0
  466. package/manual/scroll-until-text.test.mjs +68 -0
  467. package/manual/test-init-command.js +223 -0
  468. package/mcp-server/README.md +322 -0
  469. package/mcp-server/dist/codegen.d.ts +9 -0
  470. package/mcp-server/dist/codegen.js +165 -0
  471. package/mcp-server/dist/mcp-app.html +114 -0
  472. package/mcp-server/dist/package.json +1 -0
  473. package/mcp-server/dist/provision-types.d.ts +290 -0
  474. package/mcp-server/dist/provision-types.js +174 -0
  475. package/mcp-server/dist/server.d.ts +6 -0
  476. package/mcp-server/dist/server.mjs +1925 -0
  477. package/mcp-server/dist/session.d.ts +85 -0
  478. package/mcp-server/dist/session.js +152 -0
  479. package/mcp-server/mcp-app.html +28 -0
  480. package/mcp-server/mcp-config.example.json +19 -0
  481. package/mcp-server/package-lock.json +4027 -0
  482. package/mcp-server/package.json +31 -0
  483. package/mcp-server/src/codegen.ts +189 -0
  484. package/mcp-server/src/mcp-app.css +360 -0
  485. package/mcp-server/src/mcp-app.ts +547 -0
  486. package/mcp-server/src/provision-types.ts +209 -0
  487. package/mcp-server/src/server.ts +2391 -0
  488. package/mcp-server/src/session.ts +194 -0
  489. package/mcp-server/tsconfig.json +16 -0
  490. package/mcp-server/vite.config.ts +23 -0
  491. package/package.json +158 -0
  492. package/schema.json +1046 -0
  493. package/scripts/generate-skills.js +94 -0
  494. package/sdk-log-formatter.js +1157 -0
  495. package/sdk.d.ts +1486 -0
  496. package/sdk.js +4336 -0
  497. package/setup/aws/cloudformation.yaml +463 -0
  498. package/setup/aws/disable-defender.sh +42 -0
  499. package/setup/aws/install-dev-runner.sh +79 -0
  500. package/setup/aws/spawn-runner.sh +289 -0
  501. package/test/captcha-solver.test.mjs +152 -0
  502. package/test/chrome-remote-debugging.test.mjs +66 -0
  503. package/test/duckduckgo/experiment.test.mjs +28 -0
  504. package/test/duckduckgo/setup.test.mjs +29 -0
  505. package/test/manual/debug-locate-response.js +82 -0
  506. package/test/manual/reconnect-provision.test.mjs +49 -0
  507. package/test/manual/test-console-logs.test.mjs +42 -0
  508. package/test/manual/test-find-api.js +73 -0
  509. package/test/manual/test-init.sh +54 -0
  510. package/test/manual/test-prompt-cache.js +97 -0
  511. package/test/manual/test-provision-auth.mjs +22 -0
  512. package/test/manual/test-sandbox-render.js +29 -0
  513. package/test/manual/test-sdk-methods.js +15 -0
  514. package/test/manual/test-sdk-refactor.js +53 -0
  515. package/test/manual/test-stack-trace.mjs +57 -0
  516. package/test/manual/verify-element-api.js +89 -0
  517. package/test/manual/verify-types.js +0 -0
  518. package/test/manual-unawaited-promise.test.mjs +31 -0
  519. package/vitest.config.mjs +58 -0
  520. package/vitest.runner.config.mjs +33 -0
  521. package/vscode-extension/.vscodeignore +12 -0
  522. package/vscode-extension/README.md +94 -0
  523. package/vscode-extension/media/icon.png +0 -0
  524. package/vscode-extension/package-lock.json +4126 -0
  525. package/vscode-extension/package.json +86 -0
  526. package/vscode-extension/src/extension.ts +829 -0
  527. package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
  528. package/vscode-extension/tsconfig.json +16 -0
@@ -0,0 +1,1925 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TestDriver MCP Server
4
+ * Enables AI agents to iteratively build tests with visual feedback
5
+ */
6
+ // Configure logger to use stderr to avoid corrupting MCP JSON-RPC stream on stdout
7
+ process.env.TD_STDIO = "stderr";
8
+ // Enable debug mode to preserve croppedImage in SDK responses (needed for MCP App visuals)
9
+ process.env.TD_DEBUG = "true";
10
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
11
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import * as Sentry from "@sentry/node";
14
+ import * as fs from "fs";
15
+ import * as os from "os";
16
+ import * as path from "path";
17
+ import { fileURLToPath, pathToFileURL } from "url";
18
+ import { z } from "zod";
19
+ import { generateActionCode } from "./codegen.js";
20
+ import { getProvisionOptions, SessionStartInputSchema } from "./provision-types.js";
21
+ import { sessionManager } from "./session.js";
22
+ // =============================================================================
23
+ // Sentry
24
+ // =============================================================================
25
+ // Read version from main package.json (../../package.json from mcp-server/dist/)
26
+ const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
+ const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
28
+ const version = packageJson.version || "1.0.0";
29
+ // Derive release channel from package version prerelease tag (e.g. "7.6.0-test.5" → "test")
30
+ import semver from "semver";
31
+ const KNOWN_CHANNELS = new Set(["dev", "test", "canary", "latest"]);
32
+ function resolveReleaseChannel(ver) {
33
+ if (process.env.TD_CHANNEL && KNOWN_CHANNELS.has(process.env.TD_CHANNEL))
34
+ return process.env.TD_CHANNEL;
35
+ const pre = semver.prerelease(ver);
36
+ if (pre && pre.length > 0 && KNOWN_CHANNELS.has(String(pre[0])))
37
+ return String(pre[0]);
38
+ return "latest";
39
+ }
40
+ const releaseChannel = resolveReleaseChannel(version);
41
+ const isSentryEnabled = () => {
42
+ if (process.env.TD_TELEMETRY === "false") {
43
+ return false;
44
+ }
45
+ return true;
46
+ };
47
+ if (isSentryEnabled()) {
48
+ console.error("Analytics enabled. Set TD_TELEMETRY=false to disable.");
49
+ Sentry.init({
50
+ dsn: process.env.SENTRY_DSN ||
51
+ "https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
52
+ environment: releaseChannel,
53
+ release: version,
54
+ sampleRate: 1.0,
55
+ tracesSampleRate: 1.0,
56
+ sendDefaultPii: true,
57
+ integrations: [Sentry.httpIntegration(), Sentry.nodeContextIntegration()],
58
+ initialScope: {
59
+ tags: {
60
+ platform: os.platform(),
61
+ arch: os.arch(),
62
+ nodeVersion: process.version,
63
+ },
64
+ },
65
+ // Filter out expected test/element failures - only report actual exceptions and crashes
66
+ beforeSend(event, hint) {
67
+ const error = hint.originalException;
68
+ if (error && typeof error === "object" && "message" in error) {
69
+ const msg = error.message;
70
+ // Don't send user-initiated exits
71
+ if (msg.includes("User cancelled")) {
72
+ return null;
73
+ }
74
+ // Don't send expected test/element failures - these are normal test outcomes, not crashes
75
+ if (msg.includes("Element not found") ||
76
+ msg.includes("No elements found") ||
77
+ msg.includes("No element found") ||
78
+ msg.includes("Assertion failed") ||
79
+ msg.includes("assertion failed")) {
80
+ return null;
81
+ }
82
+ }
83
+ // Filter out TestFailure errors (test failures, not crashes)
84
+ if (error && typeof error === "object" && "name" in error && error.name === "TestFailure") {
85
+ return null;
86
+ }
87
+ // Filter out ElementNotFoundError - expected test outcome, not a crash
88
+ if (error && typeof error === "object" && "name" in error && error.name === "ElementNotFoundError") {
89
+ return null;
90
+ }
91
+ return event;
92
+ },
93
+ });
94
+ }
95
+ function captureException(error, context = {}) {
96
+ if (!isSentryEnabled())
97
+ return;
98
+ Sentry.withScope((scope) => {
99
+ if (context.tags) {
100
+ Object.entries(context.tags).forEach(([key, value]) => {
101
+ scope.setTag(key, value);
102
+ });
103
+ }
104
+ if (context.extra) {
105
+ Object.entries(context.extra).forEach(([key, value]) => {
106
+ scope.setExtra(key, value);
107
+ });
108
+ }
109
+ Sentry.captureException(error);
110
+ });
111
+ }
112
+ function setSessionContext(sessionId, sandboxId) {
113
+ if (!isSentryEnabled())
114
+ return;
115
+ Sentry.setTag("session", sessionId);
116
+ if (sandboxId) {
117
+ Sentry.setTag("sandbox", sandboxId);
118
+ }
119
+ Sentry.setContext("session", {
120
+ sessionId,
121
+ sandboxId,
122
+ });
123
+ }
124
+ async function flushSentry(timeout = 2000) {
125
+ if (!isSentryEnabled())
126
+ return;
127
+ await Sentry.flush(timeout);
128
+ }
129
+ // =============================================================================
130
+ // Logging
131
+ // =============================================================================
132
+ const LOG_LEVELS = {
133
+ DEBUG: 0,
134
+ INFO: 1,
135
+ WARN: 2,
136
+ ERROR: 3,
137
+ };
138
+ // Set via TD_LOG_LEVEL env var (default: INFO)
139
+ const currentLogLevel = LOG_LEVELS[process.env.TD_LOG_LEVEL?.toUpperCase() || "INFO"] ?? LOG_LEVELS.INFO;
140
+ function log(level, message, data) {
141
+ if (LOG_LEVELS[level] < currentLogLevel)
142
+ return;
143
+ const timestamp = new Date().toISOString();
144
+ const prefix = `[${timestamp}] [${level}]`;
145
+ const dataStr = data ? ` ${JSON.stringify(data)}` : "";
146
+ console.error(`${prefix} ${message}${dataStr}`);
147
+ }
148
+ const logger = {
149
+ debug: (msg, data) => log("DEBUG", msg, data),
150
+ info: (msg, data) => log("INFO", msg, data),
151
+ warn: (msg, data) => log("WARN", msg, data),
152
+ error: (msg, data) => log("ERROR", msg, data),
153
+ };
154
+ // Get directory for UI files - works both from source and compiled
155
+ const __filename = fileURLToPath(import.meta.url);
156
+ const __dirname = path.dirname(__filename);
157
+ const DIST_DIR = __filename.endsWith(".ts")
158
+ ? path.join(__dirname, "..", "dist")
159
+ : __dirname;
160
+ // Resource URI for the screenshot result UI
161
+ const RESOURCE_URI = "ui://testdriver/mcp-app.html";
162
+ // Resource URI base for serving screenshot blobs (with dynamic IDs)
163
+ const SCREENSHOT_RESOURCE_BASE = "screenshot://testdriver/screenshot";
164
+ const CROPPED_IMAGE_RESOURCE_BASE = "screenshot://testdriver/cropped";
165
+ // SDK instance (will be initialized on session start)
166
+ let sdk = null;
167
+ // Last screenshot base64 for check comparisons
168
+ let lastScreenshotBase64 = null;
169
+ // Map of image ID -> image data
170
+ const imageStore = new Map();
171
+ // Counter for generating unique image IDs
172
+ let imageIdCounter = 0;
173
+ // Maximum number of images to store (to prevent memory leaks)
174
+ const MAX_STORED_IMAGES = 100;
175
+ /**
176
+ * Store an image and return its unique resource URI
177
+ */
178
+ function storeImage(data, type) {
179
+ const id = `${type}-${++imageIdCounter}`;
180
+ // Clean up old images if we exceed the limit
181
+ if (imageStore.size >= MAX_STORED_IMAGES) {
182
+ // Remove oldest images (first entries in the map)
183
+ const entriesToRemove = Math.floor(MAX_STORED_IMAGES / 4);
184
+ const keys = Array.from(imageStore.keys()).slice(0, entriesToRemove);
185
+ for (const key of keys) {
186
+ imageStore.delete(key);
187
+ }
188
+ logger.debug("storeImage: Cleaned up old images", { removed: entriesToRemove, remaining: imageStore.size });
189
+ }
190
+ imageStore.set(id, {
191
+ data,
192
+ type,
193
+ timestamp: Date.now(),
194
+ });
195
+ logger.debug("storeImage: Stored image", { id, type, dataLength: data.length });
196
+ const base = type === "screenshot" ? SCREENSHOT_RESOURCE_BASE : CROPPED_IMAGE_RESOURCE_BASE;
197
+ return `${base}/${id}`;
198
+ }
199
+ /**
200
+ * Get an image by its ID
201
+ */
202
+ function getStoredImage(id) {
203
+ return imageStore.get(id);
204
+ }
205
+ /**
206
+ * Get session info for structured content
207
+ */
208
+ function getSessionData(session) {
209
+ if (!session)
210
+ return { id: null, expiresIn: 0 };
211
+ return {
212
+ id: session.sessionId,
213
+ expiresIn: sessionManager.getTimeRemaining(session.sessionId),
214
+ };
215
+ }
216
+ /**
217
+ * Check if session is ready for use - returns error result if not
218
+ * This helper provides clear, actionable error messages for the AI
219
+ *
220
+ * Auto-extends the session on each successful check to prevent expiry during active use
221
+ */
222
+ function requireActiveSession() {
223
+ const session = sessionManager.getCurrentSession();
224
+ // No session ever created
225
+ if (!sdk || !session) {
226
+ return {
227
+ valid: false,
228
+ error: createToolResult(false, "ERROR: No active session. You must call session_start first to create a sandbox before using any other tools.", {
229
+ error: "NO_SESSION",
230
+ action: "session_start",
231
+ message: "No sandbox session exists. Call session_start to create one."
232
+ })
233
+ };
234
+ }
235
+ // Session exists but has expired
236
+ if (!sessionManager.isSessionValid(session.sessionId)) {
237
+ // Clear the SDK reference since the sandbox is no longer available
238
+ sdk = null;
239
+ return {
240
+ valid: false,
241
+ error: createToolResult(false, "ERROR: Session has expired or timed out. The sandbox is no longer available. You must call session_start again to create a new sandbox session before continuing.", {
242
+ error: "SESSION_EXPIRED",
243
+ action: "session_start",
244
+ message: "The previous sandbox session has expired. Call session_start to create a new one.",
245
+ expiredSessionId: session.sessionId
246
+ })
247
+ };
248
+ }
249
+ // Auto-extend session on each command to prevent expiry during active use
250
+ // This resets the expiry timer back to the original keepAlive duration
251
+ sessionManager.refreshSession(session.sessionId);
252
+ return { valid: true };
253
+ }
254
+ /**
255
+ * Create tool result with structured content for MCP App
256
+ * Images: imageUrl (data URL) goes to structuredContent for UI to display
257
+ * The croppedImage from find() is small (~10KB) so it's acceptable as data URL
258
+ *
259
+ * If generatedCode is provided, it's appended to the text response with instructions
260
+ * for the agent to write it to the test file.
261
+ */
262
+ function createToolResult(success, textContent, structuredData, generatedCode) {
263
+ // Build text content - append generated code if provided with directive instructions
264
+ let fullText = textContent;
265
+ if (generatedCode && success) {
266
+ // Get the test file from the current session
267
+ const session = sessionManager.getCurrentSession();
268
+ const testFile = session?.testFile;
269
+ if (testFile) {
270
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to ${testFile}:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
271
+ }
272
+ else {
273
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to the test file:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
274
+ }
275
+ }
276
+ const content = [{ type: "text", text: fullText }];
277
+ logger.debug("createToolResult", {
278
+ success,
279
+ action: structuredData.action,
280
+ hasImage: !!structuredData.imageUrl,
281
+ duration: structuredData.duration,
282
+ hasGeneratedCode: !!generatedCode
283
+ });
284
+ // structuredContent goes to UI (includes imageUrl for display)
285
+ // Always include success flag so UI can display correct status indicator
286
+ // Include generatedCode and testFile in structured data so agents can programmatically handle it
287
+ const session = sessionManager.getCurrentSession();
288
+ return {
289
+ content,
290
+ structuredContent: {
291
+ ...structuredData,
292
+ success,
293
+ generatedCode: generatedCode && success ? generatedCode : undefined,
294
+ testFile: session?.testFile || undefined,
295
+ },
296
+ };
297
+ }
298
+ // Create MCP server wrapped with Sentry for automatic tracing
299
+ const server = isSentryEnabled()
300
+ ? Sentry.wrapMcpServerWithSentry(new McpServer({
301
+ name: "testdriver",
302
+ version: version,
303
+ }))
304
+ : new McpServer({
305
+ name: "testdriver",
306
+ version: version,
307
+ });
308
+ // Element reference storage (for click/hover after find)
309
+ // Stores actual Element instances - no raw coordinates as input
310
+ const elementRefs = new Map();
311
+ // =============================================================================
312
+ // Register UI Resource
313
+ // =============================================================================
314
+ registerAppResource(server, RESOURCE_URI, RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE, description: "TestDriver Screenshot Viewer UI" }, async () => {
315
+ const htmlPath = path.join(DIST_DIR, "mcp-app.html");
316
+ if (!fs.existsSync(htmlPath)) {
317
+ throw new Error(`UI file not found: ${htmlPath}`);
318
+ }
319
+ const html = fs.readFileSync(htmlPath, "utf-8");
320
+ return {
321
+ contents: [{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html }],
322
+ };
323
+ });
324
+ // Register screenshot resource template for serving binary blobs by ID
325
+ server.registerResource("Screenshot", new ResourceTemplate(`${SCREENSHOT_RESOURCE_BASE}/{imageId}`, { list: undefined }), {
326
+ description: "Screenshot from TestDriver session served as base64 blob",
327
+ mimeType: "image/png",
328
+ }, async (uri, variables) => {
329
+ const imageId = variables.imageId;
330
+ const image = getStoredImage(imageId);
331
+ if (!image) {
332
+ throw new Error(`Screenshot not found: ${imageId}. It may have been cleaned up.`);
333
+ }
334
+ logger.debug("screenshot resource: Serving screenshot blob", {
335
+ imageId,
336
+ blobLength: image.data.length
337
+ });
338
+ return {
339
+ contents: [{
340
+ uri: uri.href,
341
+ mimeType: "image/png",
342
+ blob: image.data,
343
+ }],
344
+ };
345
+ });
346
+ // Register cropped image resource template for serving find operation results by ID
347
+ server.registerResource("CroppedImage", new ResourceTemplate(`${CROPPED_IMAGE_RESOURCE_BASE}/{imageId}`, { list: undefined }), {
348
+ description: "Cropped image from find operations served as base64 blob",
349
+ mimeType: "image/png",
350
+ }, async (uri, variables) => {
351
+ const imageId = variables.imageId;
352
+ const image = getStoredImage(imageId);
353
+ if (!image) {
354
+ throw new Error(`Cropped image not found: ${imageId}. It may have been cleaned up.`);
355
+ }
356
+ logger.debug("cropped image resource: Serving cropped image blob", {
357
+ imageId,
358
+ blobLength: image.data.length
359
+ });
360
+ return {
361
+ contents: [{
362
+ uri: uri.href,
363
+ mimeType: "image/png",
364
+ blob: image.data,
365
+ }],
366
+ };
367
+ });
368
+ // =============================================================================
369
+ // Tools
370
+ // =============================================================================
371
+ // Session Start
372
+ registerAppTool(server, "session_start", {
373
+ title: "Session Start",
374
+ description: `Start a new TestDriver session and provision a sandbox with browser or app.
375
+
376
+ ⚠️ IMPORTANT - Test File Parameter:
377
+ When 'testFile' is provided, you MUST append the generated code to that file after EVERY successful action.
378
+ Each tool response includes "ACTION REQUIRED: Append this code..." - you must write that code to the test file immediately.
379
+
380
+ Provision types:
381
+ - chrome: Launch Chrome browser (default). Use 'url' for starting page.
382
+ - chromeExtension: Launch Chrome with an extension. Use 'extensionPath' or 'extensionId'.
383
+ - vscode: Launch VS Code. Use 'workspace' and optional 'extensions'.
384
+ - installer: Download and install an app. Use 'installerUrl' (required).
385
+ - electron: Launch an Electron app. Use 'appPath' (required).
386
+
387
+ Self-hosted mode:
388
+ - Provide 'ip' parameter to connect directly to a self-hosted Windows instance
389
+ - Set 'os' to 'windows' when connecting to Windows instances
390
+ - The IP can be from an AWS EC2 instance spawned via CloudFormation
391
+ - See https://docs.testdriver.ai/v7/aws-setup for AWS setup guide
392
+
393
+ Debug mode (connect to existing sandbox):
394
+ - Provide 'sandboxId' to connect to an existing sandbox (e.g., from a failed test with debugOnFailure: true)
395
+ - Skips provisioning - connects to sandbox in its current state
396
+ - Use this to interactively debug failed tests without re-running from scratch`,
397
+ inputSchema: SessionStartInputSchema,
398
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
399
+ }, async (params) => {
400
+ const startTime = Date.now();
401
+ logger.info("session_start: Starting", {
402
+ type: params.type,
403
+ url: params.url,
404
+ os: params.os,
405
+ reconnect: params.reconnect,
406
+ sandboxId: params.sandboxId,
407
+ });
408
+ try {
409
+ // Validate required fields for specific provision types (unless connecting to existing sandbox)
410
+ if (!params.sandboxId) {
411
+ if (params.type === "installer" && !params.installerUrl) {
412
+ return createToolResult(false, "installer type requires 'installerUrl' parameter", { error: "Missing required parameter: installerUrl" });
413
+ }
414
+ if (params.type === "electron" && !params.appPath) {
415
+ return createToolResult(false, "electron type requires 'appPath' parameter", { error: "Missing required parameter: appPath" });
416
+ }
417
+ }
418
+ // Create new session
419
+ const newSession = sessionManager.createSession({
420
+ os: params.os,
421
+ keepAlive: params.keepAlive,
422
+ testFile: params.testFile,
423
+ });
424
+ logger.debug("session_start: Session created", { sessionId: newSession.sessionId });
425
+ // Determine API root
426
+ const apiRoot = params.apiRoot || process.env.TD_API_ROOT || "https://api.testdriver.ai";
427
+ logger.debug("session_start: Using API root", { apiRoot });
428
+ // Initialize SDK
429
+ logger.debug("session_start: Initializing SDK");
430
+ const TestDriverSDK = (await import("../../sdk.js")).default;
431
+ // Determine preview mode from environment variable
432
+ // TD_PREVIEW can be "ide", "browser", or "none"
433
+ // Default to "ide" so the live preview shows within the IDE
434
+ const previewMode = process.env.TD_PREVIEW || "ide";
435
+ logger.debug("session_start: Preview mode", { preview: previewMode });
436
+ // Get IP from params or environment (for self-hosted instances)
437
+ const instanceIp = params.ip || process.env.TD_IP;
438
+ // Get API key - check multiple sources for GitHub Copilot coding agent compatibility
439
+ // 1. TD_API_KEY (standard environment variable)
440
+ // 2. COPILOT_MCP_TD_API_KEY (fallback for GitHub Copilot coding agent)
441
+ const apiKey = process.env.TD_API_KEY || process.env.COPILOT_MCP_TD_API_KEY || "";
442
+ if (!apiKey) {
443
+ logger.error("session_start: No API key found", {
444
+ hasTD_API_KEY: !!process.env.TD_API_KEY,
445
+ hasCOPILOT_MCP_TD_API_KEY: !!process.env.COPILOT_MCP_TD_API_KEY,
446
+ availableEnvVars: Object.keys(process.env).filter(k => k.includes('TD') || k.includes('COPILOT_MCP'))
447
+ });
448
+ return createToolResult(false, "No API key found. Please set TD_API_KEY or COPILOT_MCP_TD_API_KEY environment variable.", {
449
+ error: "Missing API key",
450
+ hint: "For GitHub Copilot coding agent, create a Copilot environment secret named COPILOT_MCP_TD_API_KEY"
451
+ });
452
+ }
453
+ logger.debug("session_start: API key found", {
454
+ source: process.env.TD_API_KEY ? "TD_API_KEY" : "COPILOT_MCP_TD_API_KEY",
455
+ keyPrefix: apiKey.substring(0, 7) + "..."
456
+ });
457
+ sdk = new TestDriverSDK(apiKey, {
458
+ os: params.os,
459
+ logging: false,
460
+ apiRoot,
461
+ preview: previewMode,
462
+ ip: instanceIp,
463
+ });
464
+ // Handle sandboxId mode - connect to existing sandbox (debug-on-failure mode)
465
+ if (params.sandboxId) {
466
+ logger.info("session_start: Connecting to existing sandbox (debug mode)", { sandboxId: params.sandboxId });
467
+ await sdk.connect({
468
+ sandboxId: params.sandboxId,
469
+ keepAlive: params.keepAlive,
470
+ });
471
+ // Get sandbox ID
472
+ const instance = sdk.getInstance();
473
+ logger.info("session_start: Connected to existing sandbox", { instanceId: instance?.instanceId });
474
+ sessionManager.activateSession(newSession.sessionId, instance?.instanceId || params.sandboxId);
475
+ // Set Sentry context for error tracking
476
+ setSessionContext(newSession.sessionId, instance?.instanceId);
477
+ // Capture screenshot of current state
478
+ logger.debug("session_start: Capturing screenshot of existing sandbox");
479
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
480
+ let screenshotResourceUri;
481
+ if (screenshotBase64) {
482
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
483
+ lastScreenshotBase64 = screenshotBase64;
484
+ }
485
+ const duration = Date.now() - startTime;
486
+ logger.info("session_start: Connected to existing sandbox", { duration, sessionId: newSession.sessionId, sandboxId: params.sandboxId });
487
+ return createToolResult(true, `Connected to existing sandbox (debug mode)
488
+ Session: ${newSession.sessionId}
489
+ Sandbox: ${params.sandboxId}
490
+ Expires in: ${Math.round(params.keepAlive / 1000)}s
491
+
492
+ You are now connected to the sandbox in its current state. Use find, click, type, etc. to interact.`, {
493
+ action: "session_start",
494
+ sessionId: newSession.sessionId,
495
+ sandboxId: params.sandboxId,
496
+ debugMode: true,
497
+ screenshotResourceUri,
498
+ duration
499
+ }, "// Connected to existing sandbox - no provision code needed");
500
+ }
501
+ // Connect to sandbox
502
+ if (instanceIp) {
503
+ logger.info("session_start: Connecting to self-hosted instance...", { ip: instanceIp });
504
+ }
505
+ else {
506
+ logger.info("session_start: Connecting to cloud sandbox...");
507
+ }
508
+ await sdk.connect({
509
+ reconnect: params.reconnect,
510
+ keepAlive: params.keepAlive,
511
+ ip: instanceIp,
512
+ });
513
+ // Get sandbox ID
514
+ const instance = sdk.getInstance();
515
+ logger.info("session_start: Connected to sandbox", { instanceId: instance?.instanceId });
516
+ sessionManager.activateSession(newSession.sessionId, instance?.instanceId || "unknown");
517
+ // Set Sentry context for error tracking
518
+ setSessionContext(newSession.sessionId, instance?.instanceId);
519
+ // Get provision-specific options
520
+ const provisionOptions = getProvisionOptions(params);
521
+ let provisionCmd = "";
522
+ // Provision based on type
523
+ switch (params.type) {
524
+ case "chrome": {
525
+ const chromeOpts = provisionOptions;
526
+ logger.info("session_start: Provisioning Chrome", { url: chromeOpts.url });
527
+ await sdk.provision.chrome(chromeOpts);
528
+ provisionCmd = "provision.chrome";
529
+ logger.debug("session_start: Chrome provisioned");
530
+ break;
531
+ }
532
+ case "chromeExtension": {
533
+ const extOpts = provisionOptions;
534
+ logger.info("session_start: Provisioning Chrome Extension", { extensionPath: extOpts.extensionPath, extensionId: extOpts.extensionId });
535
+ await sdk.provision.chromeExtension(extOpts);
536
+ provisionCmd = "provision.chromeExtension";
537
+ logger.debug("session_start: Chrome Extension provisioned");
538
+ break;
539
+ }
540
+ case "vscode": {
541
+ const vscodeOpts = provisionOptions;
542
+ logger.info("session_start: Provisioning VS Code", { workspace: vscodeOpts.workspace });
543
+ await sdk.provision.vscode(vscodeOpts);
544
+ provisionCmd = "provision.vscode";
545
+ logger.debug("session_start: VS Code provisioned");
546
+ break;
547
+ }
548
+ case "installer": {
549
+ const installerOpts = provisionOptions;
550
+ logger.info("session_start: Provisioning installer", { url: installerOpts.url });
551
+ await sdk.provision.installer(installerOpts);
552
+ provisionCmd = "provision.installer";
553
+ logger.debug("session_start: Installer provisioned");
554
+ break;
555
+ }
556
+ case "electron": {
557
+ const electronOpts = provisionOptions;
558
+ logger.info("session_start: Provisioning Electron", { appPath: electronOpts.appPath });
559
+ await sdk.provision.electron(electronOpts);
560
+ provisionCmd = "provision.electron";
561
+ logger.debug("session_start: Electron app provisioned");
562
+ break;
563
+ }
564
+ }
565
+ // Capture initial screenshot after provisioning
566
+ logger.debug("session_start: Capturing initial screenshot");
567
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
568
+ let screenshotResourceUri;
569
+ if (screenshotBase64) {
570
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
571
+ lastScreenshotBase64 = screenshotBase64;
572
+ }
573
+ const duration = Date.now() - startTime;
574
+ logger.info("session_start: Completed", { duration, sessionId: newSession.sessionId, selfHosted: !!instanceIp });
575
+ // Generate the code for this provision action
576
+ const generatedCode = generateActionCode(provisionCmd, provisionOptions);
577
+ // Build debugger URL for the session
578
+ const debuggerUrl = instance?.debuggerUrl || (instanceIp ? `http://${instanceIp}:9222` : null);
579
+ const connectionType = instanceIp ? `Self-hosted (${instanceIp})` : "Cloud";
580
+ return createToolResult(true, `Session started: ${newSession.sessionId}\nConnection: ${connectionType}\nType: ${params.type}\nSandbox: ${instance?.instanceId}\nExpires in: ${Math.round(params.keepAlive / 1000)}s
581
+
582
+ IMPORTANT - If creating a new test project, use these EXACT dependencies in package.json:
583
+ {
584
+ "type": "module",
585
+ "devDependencies": {
586
+ "testdriverai": "beta",
587
+ "vitest": "^4.0.0"
588
+ },
589
+ "scripts": {
590
+ "test": "vitest"
591
+ }
592
+ }`, {
593
+ action: "session_start",
594
+ sessionId: newSession.sessionId,
595
+ provisionType: params.type,
596
+ selfHosted: !!instanceIp,
597
+ instanceIp: instanceIp || undefined,
598
+ debuggerUrl,
599
+ screenshotResourceUri,
600
+ duration
601
+ }, generatedCode);
602
+ }
603
+ catch (error) {
604
+ logger.error("session_start: Failed", { error: String(error) });
605
+ captureException(error, { tags: { tool: "session_start" }, extra: { params } });
606
+ throw error;
607
+ }
608
+ });
609
+ // Session Status
610
+ server.registerTool("session_status", {
611
+ description: "Check the current session status and time remaining",
612
+ inputSchema: z.object({}),
613
+ }, async () => {
614
+ const startTime = Date.now();
615
+ logger.info("session_status: Checking");
616
+ const session = sessionManager.getCurrentSession();
617
+ if (!session) {
618
+ logger.warn("session_status: No active session");
619
+ return createToolResult(false, "No active session", { error: "No active session. Call session_start first." });
620
+ }
621
+ const summary = sessionManager.getSessionSummary(session.sessionId);
622
+ const duration = Date.now() - startTime;
623
+ logger.info("session_status: Completed", {
624
+ sessionId: session.sessionId,
625
+ status: session.status,
626
+ timeRemaining: summary?.timeRemaining,
627
+ duration
628
+ });
629
+ return createToolResult(true, `Session: ${session.sessionId}\nStatus: ${session.status}\nTime remaining: ${Math.round((summary?.timeRemaining || 0) / 1000)}s`, { action: "session_status", ...summary, sessionId: session.sessionId, status: session.status, duration });
630
+ });
631
+ // Session Extend
632
+ server.registerTool("session_extend", {
633
+ description: "Extend the session keepAlive time",
634
+ inputSchema: z.object({
635
+ additionalMs: z.number().default(60000).describe("Additional time in ms"),
636
+ }),
637
+ }, async (params) => {
638
+ logger.info("session_extend: Extending", { additionalMs: params.additionalMs });
639
+ const session = sessionManager.getCurrentSession();
640
+ if (!session) {
641
+ logger.warn("session_extend: No active session");
642
+ return { content: [{ type: "text", text: "No active session" }] };
643
+ }
644
+ sessionManager.extendSession(session.sessionId, params.additionalMs);
645
+ const newExpiry = sessionManager.getTimeRemaining(session.sessionId);
646
+ logger.info("session_extend: Extended", { sessionId: session.sessionId, newExpiry });
647
+ return {
648
+ content: [
649
+ {
650
+ type: "text",
651
+ text: `Session extended by ${params.additionalMs / 1000}s. New expiry: ${Math.round(newExpiry / 1000)}s`,
652
+ },
653
+ ],
654
+ };
655
+ });
656
+ // Find Element
657
+ registerAppTool(server, "find", {
658
+ title: "Find Element",
659
+ description: "Find an element on screen by natural language description",
660
+ inputSchema: z.object({
661
+ description: z.string().describe("Natural language description of the element"),
662
+ timeout: z.number().optional().describe("Timeout in ms for polling"),
663
+ }),
664
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
665
+ }, async (params) => {
666
+ const startTime = Date.now();
667
+ logger.info("find: Starting", { description: params.description, timeout: params.timeout });
668
+ const sessionCheck = requireActiveSession();
669
+ if (!sessionCheck.valid) {
670
+ logger.warn("find: No active session");
671
+ return sessionCheck.error;
672
+ }
673
+ try {
674
+ logger.debug("find: Calling SDK find");
675
+ const element = await sdk.find(params.description, params.timeout ? { timeout: params.timeout } : undefined);
676
+ const found = element.found();
677
+ const coords = element.getCoordinates();
678
+ // Store element ref for later use (stores actual Element instance)
679
+ const elementRef = `el-${Date.now()}`;
680
+ if (found && coords) {
681
+ elementRefs.set(elementRef, {
682
+ element: element, // Store the actual Element instance
683
+ description: params.description,
684
+ coords: {
685
+ x: coords.x,
686
+ y: coords.y,
687
+ centerX: coords.centerX,
688
+ centerY: coords.centerY,
689
+ },
690
+ });
691
+ logger.info("find: Element found", {
692
+ description: params.description,
693
+ coords: { x: coords.centerX, y: coords.centerY },
694
+ confidence: element.confidence,
695
+ elementRef
696
+ });
697
+ }
698
+ else {
699
+ logger.warn("find: Element not found", { description: params.description });
700
+ }
701
+ // Return raw SDK response directly
702
+ const rawResponse = element._response || {};
703
+ const duration = Date.now() - startTime;
704
+ // Store cropped image for resource serving (instead of inline data URL)
705
+ let croppedImageResourceUri;
706
+ let screenshotResourceUri;
707
+ const croppedImage = rawResponse.croppedImage;
708
+ if (croppedImage) {
709
+ const imageData = croppedImage.startsWith('data:')
710
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
711
+ : croppedImage;
712
+ croppedImageResourceUri = storeImage(imageData, "cropped");
713
+ // Remove croppedImage from response to avoid context bloat
714
+ delete rawResponse.croppedImage;
715
+ }
716
+ else if (!found) {
717
+ // Element not found and no cropped image - capture a fresh screenshot
718
+ // so the user can see what's currently visible on screen
719
+ try {
720
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
721
+ if (screenshotBase64) {
722
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
723
+ logger.debug("find: Captured screenshot for not-found state");
724
+ }
725
+ }
726
+ catch (e) {
727
+ logger.warn("find: Failed to capture screenshot for not-found state", { error: String(e) });
728
+ }
729
+ }
730
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
731
+ delete rawResponse.extractedText;
732
+ delete rawResponse.pixelDiffImage;
733
+ // Generate code for this find action
734
+ const generatedCode = found ? generateActionCode("find", { description: params.description }) : undefined;
735
+ // Build element info for display (cropped image is always centered on element)
736
+ const elementInfo = found ? {
737
+ description: params.description,
738
+ centerX: coords?.centerX,
739
+ centerY: coords?.centerY,
740
+ confidence: element.confidence,
741
+ ref: elementRef,
742
+ } : undefined;
743
+ return createToolResult(found, found
744
+ ? `Found: "${params.description}" at (${rawResponse.coordinates?.x}, ${rawResponse.coordinates?.y})\nRef: ${elementRef}`
745
+ : `Element not found: "${params.description}"`, {
746
+ ...rawResponse,
747
+ action: "find",
748
+ element: elementInfo,
749
+ ref: elementRef,
750
+ croppedImageResourceUri,
751
+ screenshotResourceUri,
752
+ duration,
753
+ }, generatedCode);
754
+ }
755
+ catch (error) {
756
+ logger.error("find: Failed", { error: String(error), description: params.description });
757
+ captureException(error, { tags: { tool: "find" }, extra: { description: params.description } });
758
+ throw error;
759
+ }
760
+ });
761
+ // Find All Elements
762
+ registerAppTool(server, "findall", {
763
+ title: "Find All Elements",
764
+ description: "Find all elements on screen matching a natural language description. Returns an array of element references.",
765
+ inputSchema: z.object({
766
+ description: z.string().describe("Natural language description of the elements to find"),
767
+ timeout: z.number().optional().describe("Timeout in ms for polling"),
768
+ }),
769
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
770
+ }, async (params) => {
771
+ const startTime = Date.now();
772
+ logger.info("findall: Starting", { description: params.description, timeout: params.timeout });
773
+ const sessionCheck = requireActiveSession();
774
+ if (!sessionCheck.valid) {
775
+ logger.warn("findall: No active session");
776
+ return sessionCheck.error;
777
+ }
778
+ try {
779
+ logger.debug("findall: Calling SDK findAll");
780
+ const elements = await sdk.findAll(params.description, params.timeout ? { timeout: params.timeout } : undefined);
781
+ const count = elements.length;
782
+ // Store element refs for later use
783
+ const refs = [];
784
+ const elementInfos = [];
785
+ for (let i = 0; i < elements.length; i++) {
786
+ const element = elements[i];
787
+ const coords = element.getCoordinates();
788
+ const elementRef = `el-${Date.now()}-${i}`;
789
+ if (coords) {
790
+ elementRefs.set(elementRef, {
791
+ element: element,
792
+ description: `${params.description} [${i}]`,
793
+ coords: {
794
+ x: coords.x,
795
+ y: coords.y,
796
+ centerX: coords.centerX,
797
+ centerY: coords.centerY,
798
+ },
799
+ });
800
+ refs.push(elementRef);
801
+ elementInfos.push({
802
+ ref: elementRef,
803
+ x: coords.x,
804
+ y: coords.y,
805
+ centerX: coords.centerX,
806
+ centerY: coords.centerY,
807
+ confidence: element.confidence,
808
+ });
809
+ }
810
+ }
811
+ logger.info("findall: Elements found", {
812
+ description: params.description,
813
+ count,
814
+ refs
815
+ });
816
+ // Get the first element's response for the image (shows all highlights)
817
+ const rawResponse = elements[0]?._response || {};
818
+ const duration = Date.now() - startTime;
819
+ // Store cropped image for resource serving (instead of inline data URL)
820
+ let croppedImageResourceUri;
821
+ let screenshotResourceUri;
822
+ const croppedImage = rawResponse.croppedImage;
823
+ if (croppedImage) {
824
+ const imageData = croppedImage.startsWith('data:')
825
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
826
+ : croppedImage;
827
+ croppedImageResourceUri = storeImage(imageData, "cropped");
828
+ // Remove croppedImage from response to avoid context bloat
829
+ delete rawResponse.croppedImage;
830
+ }
831
+ else if (count === 0) {
832
+ // No elements found and no cropped image - capture a fresh screenshot
833
+ // so the user can see what's currently visible on screen
834
+ try {
835
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
836
+ if (screenshotBase64) {
837
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
838
+ logger.debug("findall: Captured screenshot for not-found state");
839
+ }
840
+ }
841
+ catch (e) {
842
+ logger.warn("findall: Failed to capture screenshot for not-found state", { error: String(e) });
843
+ }
844
+ }
845
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
846
+ delete rawResponse.extractedText;
847
+ delete rawResponse.pixelDiffImage;
848
+ // Generate code for this findall action
849
+ const generatedCode = count > 0 ? generateActionCode("findall", { description: params.description }) : undefined;
850
+ // Build refs list for text output
851
+ const refsList = refs.map((ref, i) => ` [${i}] ${ref}`).join('\n');
852
+ return createToolResult(count > 0, count > 0
853
+ ? `Found ${count} elements matching "${params.description}":\n${refsList}`
854
+ : `No elements found matching: "${params.description}"`, {
855
+ ...rawResponse,
856
+ count,
857
+ refs,
858
+ elements: elementInfos,
859
+ croppedImageResourceUri,
860
+ screenshotResourceUri,
861
+ duration,
862
+ }, generatedCode);
863
+ }
864
+ catch (error) {
865
+ logger.error("findall: Failed", { error: String(error), description: params.description });
866
+ captureException(error, { tags: { tool: "findall" }, extra: { description: params.description } });
867
+ throw error;
868
+ }
869
+ });
870
+ // Click
871
+ registerAppTool(server, "click", {
872
+ title: "Click Element",
873
+ description: "Click on a previously found element. Use 'find' first to locate the element.",
874
+ inputSchema: z.object({
875
+ elementRef: z.string().describe("Reference to previously found element (required). Get this from a 'find' call."),
876
+ action: z.enum(["click", "double-click", "right-click"]).default("click"),
877
+ }),
878
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
879
+ }, async (params) => {
880
+ const startTime = Date.now();
881
+ logger.info("click: Starting", { elementRef: params.elementRef, action: params.action });
882
+ const sessionCheck = requireActiveSession();
883
+ if (!sessionCheck.valid) {
884
+ logger.warn("click: No active session");
885
+ return sessionCheck.error;
886
+ }
887
+ // Look up the element reference
888
+ const ref = elementRefs.get(params.elementRef);
889
+ if (!ref) {
890
+ logger.warn("click: Element reference not found", { elementRef: params.elementRef });
891
+ return createToolResult(false, `Element reference "${params.elementRef}" not found. Use 'find' first to locate the element.`, { error: "Element reference not found" });
892
+ }
893
+ const { element, description, coords } = ref;
894
+ try {
895
+ logger.debug("click: Executing click on element", { description, action: params.action });
896
+ // Use the Element's click method instead of raw coordinates
897
+ if (params.action === "click") {
898
+ await element.click();
899
+ }
900
+ else if (params.action === "double-click") {
901
+ await element.doubleClick();
902
+ }
903
+ else if (params.action === "right-click") {
904
+ await element.rightClick();
905
+ }
906
+ // Capture screenshot after click to show result
907
+ logger.debug("click: Capturing screenshot after click");
908
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
909
+ let screenshotResourceUri;
910
+ if (screenshotBase64) {
911
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
912
+ lastScreenshotBase64 = screenshotBase64;
913
+ }
914
+ const rawResponse = element._response || {};
915
+ // Remove large data from response to reduce context bloat
916
+ delete rawResponse.croppedImage;
917
+ delete rawResponse.extractedText;
918
+ delete rawResponse.pixelDiffImage;
919
+ const duration = Date.now() - startTime;
920
+ logger.info("click: Completed", { description, duration });
921
+ // Generate code for this click action
922
+ const generatedCode = generateActionCode("click", { action: params.action });
923
+ return createToolResult(true, `Clicked on "${description}"`, {
924
+ ...rawResponse,
925
+ action: "click",
926
+ clickAction: params.action,
927
+ clickPosition: coords,
928
+ screenshotResourceUri,
929
+ duration
930
+ }, generatedCode);
931
+ }
932
+ catch (error) {
933
+ logger.error("click: Failed", { error: String(error), description });
934
+ captureException(error, { tags: { tool: "click" }, extra: { elementRef: params.elementRef, action: params.action } });
935
+ throw error;
936
+ }
937
+ });
938
+ // Hover
939
+ registerAppTool(server, "hover", {
940
+ title: "Hover Element",
941
+ description: "Hover over a previously found element. Use 'find' first to locate the element.",
942
+ inputSchema: z.object({
943
+ elementRef: z.string().describe("Reference to previously found element (required). Get this from a 'find' call."),
944
+ }),
945
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
946
+ }, async (params) => {
947
+ const startTime = Date.now();
948
+ logger.info("hover: Starting", { elementRef: params.elementRef });
949
+ const sessionCheck = requireActiveSession();
950
+ if (!sessionCheck.valid) {
951
+ logger.warn("hover: No active session");
952
+ return sessionCheck.error;
953
+ }
954
+ // Look up the element reference
955
+ const ref = elementRefs.get(params.elementRef);
956
+ if (!ref) {
957
+ logger.warn("hover: Element reference not found", { elementRef: params.elementRef });
958
+ return createToolResult(false, `Element reference "${params.elementRef}" not found. Use 'find' first to locate the element.`, { error: "Element reference not found" });
959
+ }
960
+ const { element, description, coords } = ref;
961
+ try {
962
+ logger.debug("hover: Executing hover on element", { description });
963
+ await element.hover();
964
+ // Capture screenshot after hover to show result
965
+ logger.debug("hover: Capturing screenshot after hover");
966
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
967
+ let screenshotResourceUri;
968
+ if (screenshotBase64) {
969
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
970
+ lastScreenshotBase64 = screenshotBase64;
971
+ }
972
+ const rawResponse = element._response || {};
973
+ // Remove large data from response to reduce context bloat
974
+ delete rawResponse.croppedImage;
975
+ delete rawResponse.extractedText;
976
+ delete rawResponse.pixelDiffImage;
977
+ const duration = Date.now() - startTime;
978
+ logger.info("hover: Completed", { description, duration });
979
+ // Generate code for this hover action
980
+ const generatedCode = generateActionCode("hover", {});
981
+ return createToolResult(true, `Hovered over "${description}"`, {
982
+ ...rawResponse,
983
+ action: "hover",
984
+ screenshotResourceUri,
985
+ duration
986
+ }, generatedCode);
987
+ }
988
+ catch (error) {
989
+ logger.error("hover: Failed", { error: String(error), description });
990
+ captureException(error, { tags: { tool: "hover" }, extra: { elementRef: params.elementRef } });
991
+ throw error;
992
+ }
993
+ });
994
+ // Wait
995
+ server.registerTool("wait", {
996
+ description: "Wait for a specified amount of time",
997
+ inputSchema: z.object({
998
+ timeout: z.number().default(3000).describe("Time to wait in milliseconds (default: 3000)"),
999
+ }),
1000
+ }, async (params) => {
1001
+ const startTime = Date.now();
1002
+ logger.info("wait: Starting", { timeout: params.timeout });
1003
+ const sessionCheck = requireActiveSession();
1004
+ if (!sessionCheck.valid) {
1005
+ logger.warn("wait: No active session");
1006
+ return sessionCheck.error;
1007
+ }
1008
+ try {
1009
+ logger.debug("wait: Waiting", { timeout: params.timeout });
1010
+ await sdk.wait(params.timeout);
1011
+ const duration = Date.now() - startTime;
1012
+ logger.info("wait: Completed", { timeout: params.timeout, duration });
1013
+ // Generate code for this wait action
1014
+ const generatedCode = generateActionCode("wait", { timeout: params.timeout });
1015
+ return createToolResult(true, `Waited for ${params.timeout}ms`, { action: "wait", timeout: params.timeout, duration }, generatedCode);
1016
+ }
1017
+ catch (error) {
1018
+ logger.error("wait: Failed", { error: String(error) });
1019
+ captureException(error, { tags: { tool: "wait" }, extra: { timeout: params.timeout } });
1020
+ throw error;
1021
+ }
1022
+ });
1023
+ // Focus Application
1024
+ server.registerTool("focus_application", {
1025
+ description: "Bring an application window to the foreground",
1026
+ inputSchema: z.object({
1027
+ name: z.string().describe("Name of the application to focus (e.g., 'Google Chrome', 'Visual Studio Code')"),
1028
+ }),
1029
+ }, async (params) => {
1030
+ const startTime = Date.now();
1031
+ logger.info("focus_application: Starting", { name: params.name });
1032
+ const sessionCheck = requireActiveSession();
1033
+ if (!sessionCheck.valid) {
1034
+ logger.warn("focus_application: No active session");
1035
+ return sessionCheck.error;
1036
+ }
1037
+ try {
1038
+ logger.debug("focus_application: Focusing", { name: params.name });
1039
+ await sdk.focusApplication(params.name);
1040
+ const duration = Date.now() - startTime;
1041
+ logger.info("focus_application: Completed", { name: params.name, duration });
1042
+ // Generate code for this focus action
1043
+ const generatedCode = generateActionCode("focus_application", { name: params.name });
1044
+ return createToolResult(true, `Focused application: "${params.name}"`, { action: "focus", name: params.name, duration }, generatedCode);
1045
+ }
1046
+ catch (error) {
1047
+ logger.error("focus_application: Failed", { error: String(error), name: params.name });
1048
+ captureException(error, { tags: { tool: "focus_application" }, extra: { name: params.name } });
1049
+ throw error;
1050
+ }
1051
+ });
1052
+ // Find and Click
1053
+ registerAppTool(server, "find_and_click", {
1054
+ title: "Find and Click",
1055
+ description: "Find an element and click it in one action",
1056
+ inputSchema: z.object({
1057
+ description: z.string().describe("Natural language description of element"),
1058
+ action: z.enum(["click", "double-click", "right-click"]).default("click"),
1059
+ }),
1060
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1061
+ }, async (params) => {
1062
+ const startTime = Date.now();
1063
+ logger.info("find_and_click: Starting", { description: params.description, action: params.action });
1064
+ const sessionCheck = requireActiveSession();
1065
+ if (!sessionCheck.valid) {
1066
+ logger.warn("find_and_click: No active session");
1067
+ return sessionCheck.error;
1068
+ }
1069
+ try {
1070
+ logger.debug("find_and_click: Finding element");
1071
+ const element = await sdk.find(params.description);
1072
+ const found = element.found();
1073
+ if (!found) {
1074
+ logger.warn("find_and_click: Element not found", { description: params.description });
1075
+ // Capture screenshot to show current state even when element not found
1076
+ const rawResponse = element._response || {};
1077
+ const duration = Date.now() - startTime;
1078
+ // Store cropped image (screenshot) for resource serving
1079
+ let croppedImageResourceUri;
1080
+ let screenshotResourceUri;
1081
+ const croppedImage = rawResponse.croppedImage;
1082
+ if (croppedImage) {
1083
+ const imageData = croppedImage.startsWith('data:')
1084
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
1085
+ : croppedImage;
1086
+ croppedImageResourceUri = storeImage(imageData, "screenshot");
1087
+ delete rawResponse.croppedImage;
1088
+ }
1089
+ else {
1090
+ // No cropped image - capture a fresh screenshot so the user can see
1091
+ // what's currently visible on screen when element was not found
1092
+ try {
1093
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1094
+ if (screenshotBase64) {
1095
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1096
+ logger.debug("find_and_click: Captured screenshot for not-found state");
1097
+ }
1098
+ }
1099
+ catch (e) {
1100
+ logger.warn("find_and_click: Failed to capture screenshot for not-found state", { error: String(e) });
1101
+ }
1102
+ }
1103
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
1104
+ delete rawResponse.extractedText;
1105
+ delete rawResponse.pixelDiffImage;
1106
+ return createToolResult(false, `Element not found: "${params.description}"`, {
1107
+ ...rawResponse,
1108
+ action: "find_and_click",
1109
+ error: "Element not found",
1110
+ croppedImageResourceUri,
1111
+ screenshotResourceUri,
1112
+ duration
1113
+ });
1114
+ }
1115
+ const coords = element.getCoordinates();
1116
+ // Store element ref for later use (in case user wants to interact again)
1117
+ const elementRef = `el-${Date.now()}`;
1118
+ if (coords) {
1119
+ elementRefs.set(elementRef, {
1120
+ element: element,
1121
+ description: params.description,
1122
+ coords: {
1123
+ x: coords.x,
1124
+ y: coords.y,
1125
+ centerX: coords.centerX,
1126
+ centerY: coords.centerY,
1127
+ },
1128
+ });
1129
+ }
1130
+ logger.debug("find_and_click: Element found, clicking", { action: params.action, elementRef });
1131
+ if (params.action === "click") {
1132
+ await element.click();
1133
+ }
1134
+ else if (params.action === "double-click") {
1135
+ await element.doubleClick();
1136
+ }
1137
+ else if (params.action === "right-click") {
1138
+ await element.rightClick();
1139
+ }
1140
+ // Return raw SDK response directly
1141
+ const rawResponse = element._response || {};
1142
+ const duration = Date.now() - startTime;
1143
+ // Store cropped image for resource serving (instead of inline data URL)
1144
+ let croppedImageResourceUri;
1145
+ const croppedImage = rawResponse.croppedImage;
1146
+ if (croppedImage) {
1147
+ const imageData = croppedImage.startsWith('data:')
1148
+ ? croppedImage.replace(/^data:image\/\w+;base64,/, '')
1149
+ : croppedImage;
1150
+ croppedImageResourceUri = storeImage(imageData, "cropped");
1151
+ // Remove croppedImage from response to avoid context bloat
1152
+ delete rawResponse.croppedImage;
1153
+ }
1154
+ // Remove extractedText and pixelDiffImage from response to reduce context bloat
1155
+ delete rawResponse.extractedText;
1156
+ delete rawResponse.pixelDiffImage;
1157
+ // Generate code for this find_and_click action
1158
+ const generatedCode = generateActionCode("find_and_click", { description: params.description, action: params.action });
1159
+ // Build element info for display (match find action format)
1160
+ const elementInfo = coords ? {
1161
+ description: params.description,
1162
+ centerX: coords.centerX,
1163
+ centerY: coords.centerY,
1164
+ confidence: element.confidence,
1165
+ ref: elementRef,
1166
+ } : undefined;
1167
+ return createToolResult(true, `Found and clicked: "${params.description}" at (${rawResponse.coordinates?.x}, ${rawResponse.coordinates?.y})\nRef: ${elementRef}`, {
1168
+ ...rawResponse,
1169
+ action: "find_and_click",
1170
+ element: elementInfo,
1171
+ ref: elementRef,
1172
+ clickAction: params.action,
1173
+ clickPosition: coords ? { x: coords.centerX, y: coords.centerY } : undefined,
1174
+ croppedImageResourceUri,
1175
+ duration,
1176
+ }, generatedCode);
1177
+ }
1178
+ catch (error) {
1179
+ logger.error("find_and_click: Failed", { error: String(error), description: params.description });
1180
+ captureException(error, { tags: { tool: "find_and_click" }, extra: { description: params.description, action: params.action } });
1181
+ throw error;
1182
+ }
1183
+ });
1184
+ // Type
1185
+ server.registerTool("type", {
1186
+ description: "Type text into the currently focused field",
1187
+ inputSchema: z.object({
1188
+ text: z.string().describe("Text to type"),
1189
+ secret: z.boolean().default(false).describe("Whether this is sensitive data"),
1190
+ delay: z.number().optional().describe("Delay between keystrokes in ms"),
1191
+ }),
1192
+ }, async (params) => {
1193
+ const startTime = Date.now();
1194
+ logger.info("type: Starting", { textLength: params.text.length, secret: params.secret });
1195
+ const sessionCheck = requireActiveSession();
1196
+ if (!sessionCheck.valid) {
1197
+ logger.warn("type: No active session");
1198
+ return sessionCheck.error;
1199
+ }
1200
+ try {
1201
+ logger.debug("type: Typing text");
1202
+ await sdk.type(params.text, { secret: params.secret, delay: params.delay });
1203
+ const duration = Date.now() - startTime;
1204
+ logger.info("type: Completed", { duration });
1205
+ // Generate code for this type action
1206
+ const generatedCode = generateActionCode("type", { text: params.text, secret: params.secret });
1207
+ return createToolResult(true, `Typed: ${params.secret ? "[secret text]" : `"${params.text}"`}`, { action: "type", text: params.secret ? "[SECRET]" : params.text, duration }, generatedCode);
1208
+ }
1209
+ catch (error) {
1210
+ logger.error("type: Failed", { error: String(error) });
1211
+ captureException(error, { tags: { tool: "type" }, extra: { textLength: params.text.length, secret: params.secret } });
1212
+ throw error;
1213
+ }
1214
+ });
1215
+ // Press Keys
1216
+ server.registerTool("press_keys", {
1217
+ description: "Press keyboard keys or shortcuts",
1218
+ inputSchema: z.object({
1219
+ keys: z.array(z.string()).describe("Array of keys to press (e.g., ['ctrl', 'a'])"),
1220
+ }),
1221
+ }, async (params) => {
1222
+ const startTime = Date.now();
1223
+ logger.info("press_keys: Starting", { keys: params.keys });
1224
+ const sessionCheck = requireActiveSession();
1225
+ if (!sessionCheck.valid) {
1226
+ logger.warn("press_keys: No active session");
1227
+ return sessionCheck.error;
1228
+ }
1229
+ try {
1230
+ logger.debug("press_keys: Pressing keys");
1231
+ await sdk.pressKeys(params.keys);
1232
+ const duration = Date.now() - startTime;
1233
+ logger.info("press_keys: Completed", { keys: params.keys, duration });
1234
+ // Generate code for this press_keys action
1235
+ const generatedCode = generateActionCode("press_keys", { keys: params.keys });
1236
+ return createToolResult(true, `Pressed keys: ${params.keys.join(" + ")}`, { action: "press_keys", keys: params.keys, duration }, generatedCode);
1237
+ }
1238
+ catch (error) {
1239
+ logger.error("press_keys: Failed", { error: String(error), keys: params.keys });
1240
+ captureException(error, { tags: { tool: "press_keys" }, extra: { keys: params.keys } });
1241
+ throw error;
1242
+ }
1243
+ });
1244
+ // Scroll
1245
+ server.registerTool("scroll", {
1246
+ description: "Scroll the page or element",
1247
+ inputSchema: z.object({
1248
+ direction: z.enum(["up", "down", "left", "right"]).default("down"),
1249
+ amount: z.number().optional().describe("Amount to scroll in pixels"),
1250
+ }),
1251
+ }, async (params) => {
1252
+ const startTime = Date.now();
1253
+ logger.info("scroll: Starting", { direction: params.direction, amount: params.amount });
1254
+ const sessionCheck = requireActiveSession();
1255
+ if (!sessionCheck.valid) {
1256
+ logger.warn("scroll: No active session");
1257
+ return sessionCheck.error;
1258
+ }
1259
+ try {
1260
+ logger.debug("scroll: Scrolling");
1261
+ await sdk.scroll(params.direction, params.amount ? { amount: params.amount } : undefined);
1262
+ const duration = Date.now() - startTime;
1263
+ logger.info("scroll: Completed", { direction: params.direction, duration });
1264
+ // Generate code for this scroll action
1265
+ const generatedCode = generateActionCode("scroll", { direction: params.direction, amount: params.amount });
1266
+ return createToolResult(true, `Scrolled ${params.direction}${params.amount ? ` by ${params.amount}px` : ""}`, { action: "scroll", scrollDirection: params.direction, direction: params.direction, amount: params.amount, duration }, generatedCode);
1267
+ }
1268
+ catch (error) {
1269
+ logger.error("scroll: Failed", { error: String(error), direction: params.direction });
1270
+ captureException(error, { tags: { tool: "scroll" }, extra: { direction: params.direction, amount: params.amount } });
1271
+ throw error;
1272
+ }
1273
+ });
1274
+ // Assert - generates code for test files
1275
+ server.registerTool("assert", {
1276
+ description: `Make an AI-powered assertion about the current screen state. GENERATES CODE for the test file.
1277
+
1278
+ Use this when you want a verification step recorded in the generated test. This will add code like:
1279
+ const assertResult = await testdriver.assert("your assertion");
1280
+ expect(assertResult).toBeTruthy();
1281
+
1282
+ Unlike 'check' which is for your understanding during development, 'assert' creates verification code that runs in CI/CD.`,
1283
+ inputSchema: z.object({
1284
+ assertion: z.string().describe("Natural language assertion to verify"),
1285
+ }),
1286
+ }, async (params) => {
1287
+ const startTime = Date.now();
1288
+ logger.info("assert: Starting", { assertion: params.assertion });
1289
+ const sessionCheck = requireActiveSession();
1290
+ if (!sessionCheck.valid) {
1291
+ logger.warn("assert: No active session");
1292
+ return sessionCheck.error;
1293
+ }
1294
+ try {
1295
+ logger.debug("assert: Running assertion");
1296
+ const result = await sdk.assert(params.assertion);
1297
+ const duration = Date.now() - startTime;
1298
+ logger.info("assert: Completed", { assertion: params.assertion, passed: result, duration });
1299
+ // Generate code for this assert action
1300
+ const generatedCode = generateActionCode("assert", { assertion: params.assertion });
1301
+ return createToolResult(result, result ? `✓ Assertion passed: "${params.assertion}"` : `✗ Assertion failed: "${params.assertion}"`, { action: "assert", assertion: params.assertion, passed: result, success: result, duration }, generatedCode);
1302
+ }
1303
+ catch (error) {
1304
+ logger.error("assert: Failed", { error: String(error), assertion: params.assertion });
1305
+ captureException(error, { tags: { tool: "assert" }, extra: { assertion: params.assertion } });
1306
+ throw error;
1307
+ }
1308
+ });
1309
+ // Check - AI uses this to understand the screen state (DOES NOT generate code)
1310
+ registerAppTool(server, "check", {
1311
+ title: "Check Screen State",
1312
+ description: `👁️ THIS IS HOW YOU SEE THE SCREEN. Use this tool whenever you need to understand what's currently displayed.
1313
+
1314
+ This tool captures a screenshot and returns AI analysis to YOU. Use it to:
1315
+ - See what's on the screen right now
1316
+ - Verify if your last action worked
1317
+ - Understand the current application state
1318
+ - Check if elements are visible or if navigation completed
1319
+
1320
+ Examples:
1321
+ - "What is currently on the screen?"
1322
+ - "Did the button click work?"
1323
+ - "Is the login form visible?"
1324
+ - "Did the page navigate to the dashboard?"
1325
+
1326
+ ⚠️ Do NOT use 'screenshot' to see the screen - that only shows the user, not you.
1327
+
1328
+ Note: This tool does NOT generate test code. Use 'assert' when you want to add a verification step to the test file.
1329
+
1330
+ You can optionally provide a reference image URI to compare against a previous state.`,
1331
+ inputSchema: z.object({
1332
+ task: z.string().describe("The task or condition to verify (e.g., 'Did the login succeed?', 'Is the modal visible?')"),
1333
+ referenceImageUri: z.string().optional().describe("Optional screenshot resource URI (e.g., 'screenshot://testdriver/screenshot/screenshot-1') to compare against instead of the automatically captured 'before' screenshot. Use a screenshotResourceUri from a previous action."),
1334
+ }),
1335
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1336
+ }, async (params) => {
1337
+ const startTime = Date.now();
1338
+ logger.info("check: Starting", { task: params.task, hasReferenceImageUri: !!params.referenceImageUri });
1339
+ const sessionCheck = requireActiveSession();
1340
+ if (!sessionCheck.valid) {
1341
+ logger.warn("check: No active session");
1342
+ return sessionCheck.error;
1343
+ }
1344
+ try {
1345
+ // Capture current screenshot
1346
+ logger.debug("check: Capturing current screenshot");
1347
+ const currentScreenshot = await sdk.agent.system.captureScreenBase64(1, false, true);
1348
+ // Use provided reference image URI, last screenshot as "before" state, or current if no previous screenshot
1349
+ let beforeScreenshot;
1350
+ if (params.referenceImageUri) {
1351
+ // Extract image ID from URI (e.g., "screenshot://testdriver/screenshot/screenshot-1" -> "screenshot-1")
1352
+ const uriParts = params.referenceImageUri.split('/');
1353
+ const imageId = uriParts[uriParts.length - 1];
1354
+ logger.info("check: Looking up reference image", {
1355
+ referenceImageUri: params.referenceImageUri,
1356
+ extractedImageId: imageId,
1357
+ imageStoreSize: imageStore.size,
1358
+ availableKeys: Array.from(imageStore.keys())
1359
+ });
1360
+ const storedImage = getStoredImage(imageId);
1361
+ if (storedImage) {
1362
+ logger.info("check: Found reference image", {
1363
+ imageId,
1364
+ dataLength: storedImage.data?.length,
1365
+ type: storedImage.type,
1366
+ hasData: !!storedImage.data
1367
+ });
1368
+ beforeScreenshot = storedImage.data;
1369
+ }
1370
+ else {
1371
+ logger.warn("check: Reference image NOT found in store, falling back to last screenshot", {
1372
+ referenceImageUri: params.referenceImageUri,
1373
+ imageId,
1374
+ imageStoreSize: imageStore.size,
1375
+ availableKeys: Array.from(imageStore.keys())
1376
+ });
1377
+ beforeScreenshot = lastScreenshotBase64 || currentScreenshot;
1378
+ }
1379
+ }
1380
+ else {
1381
+ beforeScreenshot = lastScreenshotBase64 || currentScreenshot;
1382
+ }
1383
+ // Update last screenshot for next check
1384
+ lastScreenshotBase64 = currentScreenshot;
1385
+ // Get system state
1386
+ const mousePosition = await sdk.agent.system.getMousePosition();
1387
+ const activeWindow = await sdk.agent.system.activeWin();
1388
+ // Call the check endpoint
1389
+ logger.info("check: Calling check API endpoint", {
1390
+ hasLastScreenshot: beforeScreenshot !== currentScreenshot,
1391
+ usingReferenceImageUri: !!params.referenceImageUri,
1392
+ beforeScreenshotLength: beforeScreenshot?.length || 0,
1393
+ currentScreenshotLength: currentScreenshot?.length || 0,
1394
+ beforeScreenshotPreview: beforeScreenshot?.substring(0, 50),
1395
+ currentScreenshotPreview: currentScreenshot?.substring(0, 50)
1396
+ });
1397
+ const response = await sdk.agent.sdk.req("check", {
1398
+ tasks: [params.task],
1399
+ images: [beforeScreenshot, currentScreenshot],
1400
+ mousePosition,
1401
+ activeWindow,
1402
+ });
1403
+ const aiResponse = response.data;
1404
+ // Store screenshot for resource serving
1405
+ let screenshotResourceUri;
1406
+ if (currentScreenshot) {
1407
+ screenshotResourceUri = storeImage(currentScreenshot, "screenshot");
1408
+ }
1409
+ // Determine if the check passed based on the AI response
1410
+ // The AI typically returns markdown with its analysis
1411
+ // We consider it "complete" if the response doesn't contain code blocks (indicating more work needed)
1412
+ const hasCodeBlocks = aiResponse && (aiResponse.includes("```yml") ||
1413
+ aiResponse.includes("```yaml") ||
1414
+ aiResponse.includes("- command:"));
1415
+ const isComplete = !hasCodeBlocks;
1416
+ const duration = Date.now() - startTime;
1417
+ logger.info("check: Completed", { task: params.task, complete: isComplete, duration });
1418
+ // Note: check doesn't generate code - it's for AI understanding, not test recording
1419
+ return createToolResult(isComplete, isComplete
1420
+ ? `✓ Task appears complete: "${params.task}"\n\nAI Analysis:\n${aiResponse}`
1421
+ : `⚠ Task may not be complete: "${params.task}"\n\nAI Analysis:\n${aiResponse}`, {
1422
+ action: "check",
1423
+ task: params.task,
1424
+ complete: isComplete,
1425
+ success: isComplete,
1426
+ aiResponse,
1427
+ screenshotResourceUri,
1428
+ duration
1429
+ });
1430
+ }
1431
+ catch (error) {
1432
+ logger.error("check: Failed", { error: String(error), task: params.task });
1433
+ captureException(error, { tags: { tool: "check" }, extra: { task: params.task } });
1434
+ throw error;
1435
+ }
1436
+ });
1437
+ // Exec
1438
+ server.registerTool("exec", {
1439
+ description: "Execute shell or PowerShell commands in the sandbox",
1440
+ inputSchema: z.object({
1441
+ language: z.enum(["sh", "pwsh"]).default("sh"),
1442
+ code: z.string().describe("Code to execute"),
1443
+ timeout: z.number().default(30000).describe("Timeout in ms"),
1444
+ }),
1445
+ }, async (params) => {
1446
+ const startTime = Date.now();
1447
+ logger.info("exec: Starting", { language: params.language, codeLength: params.code.length, timeout: params.timeout });
1448
+ const sessionCheck = requireActiveSession();
1449
+ if (!sessionCheck.valid) {
1450
+ logger.warn("exec: No active session");
1451
+ return sessionCheck.error;
1452
+ }
1453
+ try {
1454
+ logger.debug("exec: Executing code", { language: params.language });
1455
+ const output = await sdk.exec(params.language, params.code, params.timeout);
1456
+ const duration = Date.now() - startTime;
1457
+ logger.info("exec: Completed", { language: params.language, outputLength: output?.length || 0, duration });
1458
+ // Generate code for this exec action
1459
+ const generatedCode = generateActionCode("exec", { language: params.language, code: params.code, timeout: params.timeout });
1460
+ return createToolResult(true, `Executed ${params.language} code:\n${output || "(no output)"}`, { action: "exec", language: params.language, output, duration }, generatedCode);
1461
+ }
1462
+ catch (error) {
1463
+ logger.error("exec: Failed", { error: String(error), language: params.language });
1464
+ captureException(error, { tags: { tool: "exec" }, extra: { language: params.language, codeLength: params.code.length } });
1465
+ throw error;
1466
+ }
1467
+ });
1468
+ function parseScreenshotFilename(filename) {
1469
+ // Match pattern: 001-click-before-L42-submit-button.png or 001-click-error-L42-submit-button.png
1470
+ const match = filename.match(/^(\d+)-([a-z]+)-(before|after|error)-L(\d+)-(.+)\.png$/i);
1471
+ if (match) {
1472
+ return {
1473
+ sequence: parseInt(match[1], 10),
1474
+ action: match[2].toLowerCase(),
1475
+ phase: match[3].toLowerCase(),
1476
+ lineNumber: parseInt(match[4], 10),
1477
+ description: match[5],
1478
+ };
1479
+ }
1480
+ return {};
1481
+ }
1482
+ // List Local Screenshots - lists screenshots saved to .testdriver directory
1483
+ server.registerTool("list_local_screenshots", {
1484
+ description: `List and filter screenshots saved in the .testdriver directory.
1485
+
1486
+ Screenshots from auto-screenshot feature use the format: <seq>-<action>-<phase>-L<line>-<description>.png
1487
+ Example: 001-click-before-L42-submit-button.png
1488
+
1489
+ This tool supports powerful filtering to find specific screenshots:
1490
+ - By test file (directory)
1491
+ - By line number or range
1492
+ - By action type (click, find, type, assert, etc.)
1493
+ - By phase (before/after/error - error screenshots are captured when actions fail)
1494
+ - By regex pattern on filename
1495
+ - By sequence number range
1496
+
1497
+ Returns a list of screenshot paths that can be viewed with the 'view_local_screenshot' tool.`,
1498
+ inputSchema: z.object({
1499
+ directory: z.string().optional().describe("Test file or subdirectory to search (e.g., 'login.test', 'mcp-screenshots'). If not provided, searches all."),
1500
+ line: z.number().optional().describe("Filter by exact line number from test file (e.g., 42 matches L42)"),
1501
+ lineRange: z.object({
1502
+ start: z.number().describe("Start line number (inclusive)"),
1503
+ end: z.number().describe("End line number (inclusive)"),
1504
+ }).optional().describe("Filter by line number range (e.g., { start: 10, end: 20 })"),
1505
+ action: z.string().optional().describe("Filter by action type: click, find, type, assert, provision, scroll, hover, etc."),
1506
+ phase: z.enum(["before", "after", "error"]).optional().describe("Filter by phase: 'before' (pre-action), 'after' (post-action), or 'error' (when action fails)"),
1507
+ pattern: z.string().optional().describe("Regex pattern to match against filename (e.g., 'submit|login' or 'button.*click')"),
1508
+ sequence: z.number().optional().describe("Filter by exact sequence number"),
1509
+ sequenceRange: z.object({
1510
+ start: z.number().describe("Start sequence (inclusive)"),
1511
+ end: z.number().describe("End sequence (inclusive)"),
1512
+ }).optional().describe("Filter by sequence range (e.g., { start: 1, end: 10 })"),
1513
+ limit: z.number().optional().describe("Maximum number of results to return (default: 50)"),
1514
+ sortBy: z.enum(["modified", "sequence", "line"]).optional().describe("Sort by: 'modified' (newest first), 'sequence' (execution order), or 'line' (line number). Default: 'modified'"),
1515
+ }),
1516
+ }, async (params) => {
1517
+ const startTime = Date.now();
1518
+ logger.info("list_local_screenshots: Starting", { ...params });
1519
+ try {
1520
+ // Find .testdriver directory - check current working directory and common locations
1521
+ const possiblePaths = [
1522
+ path.join(process.cwd(), ".testdriver"),
1523
+ path.join(os.homedir(), ".testdriver"),
1524
+ ];
1525
+ let testdriverDir = null;
1526
+ for (const p of possiblePaths) {
1527
+ if (fs.existsSync(p)) {
1528
+ testdriverDir = p;
1529
+ break;
1530
+ }
1531
+ }
1532
+ if (!testdriverDir) {
1533
+ logger.warn("list_local_screenshots: .testdriver directory not found");
1534
+ return createToolResult(false, "No .testdriver directory found. Screenshots are saved here during test runs.", { error: "Directory not found" });
1535
+ }
1536
+ const screenshots = [];
1537
+ // Compile regex pattern if provided
1538
+ let regexPattern = null;
1539
+ if (params.pattern) {
1540
+ try {
1541
+ regexPattern = new RegExp(params.pattern, "i");
1542
+ }
1543
+ catch {
1544
+ return createToolResult(false, `Invalid regex pattern: ${params.pattern}`, { error: "Invalid regex" });
1545
+ }
1546
+ }
1547
+ // Function to recursively find PNG files
1548
+ const findPngFiles = (dir) => {
1549
+ if (!fs.existsSync(dir))
1550
+ return;
1551
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1552
+ for (const entry of entries) {
1553
+ const fullPath = path.join(dir, entry.name);
1554
+ if (entry.isDirectory()) {
1555
+ // If a specific directory was requested, only search that one
1556
+ if (!params.directory || entry.name === params.directory || dir !== testdriverDir) {
1557
+ findPngFiles(fullPath);
1558
+ }
1559
+ }
1560
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".png")) {
1561
+ const parsed = parseScreenshotFilename(entry.name);
1562
+ // Apply filters
1563
+ if (params.line !== undefined && parsed.lineNumber !== params.line)
1564
+ continue;
1565
+ if (params.lineRange && (parsed.lineNumber === undefined ||
1566
+ parsed.lineNumber < params.lineRange.start ||
1567
+ parsed.lineNumber > params.lineRange.end))
1568
+ continue;
1569
+ if (params.action && parsed.action !== params.action.toLowerCase())
1570
+ continue;
1571
+ if (params.phase && parsed.phase !== params.phase)
1572
+ continue;
1573
+ if (params.sequence !== undefined && parsed.sequence !== params.sequence)
1574
+ continue;
1575
+ if (params.sequenceRange && (parsed.sequence === undefined ||
1576
+ parsed.sequence < params.sequenceRange.start ||
1577
+ parsed.sequence > params.sequenceRange.end))
1578
+ continue;
1579
+ if (regexPattern && !regexPattern.test(entry.name))
1580
+ continue;
1581
+ const stats = fs.statSync(fullPath);
1582
+ screenshots.push({
1583
+ path: fullPath,
1584
+ name: entry.name,
1585
+ modified: stats.mtime,
1586
+ size: stats.size,
1587
+ parsed,
1588
+ });
1589
+ }
1590
+ }
1591
+ };
1592
+ findPngFiles(testdriverDir);
1593
+ // Sort based on sortBy parameter
1594
+ const sortBy = params.sortBy || "modified";
1595
+ if (sortBy === "modified") {
1596
+ screenshots.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1597
+ }
1598
+ else if (sortBy === "sequence") {
1599
+ screenshots.sort((a, b) => (a.parsed.sequence ?? Infinity) - (b.parsed.sequence ?? Infinity));
1600
+ }
1601
+ else if (sortBy === "line") {
1602
+ screenshots.sort((a, b) => (a.parsed.lineNumber ?? Infinity) - (b.parsed.lineNumber ?? Infinity));
1603
+ }
1604
+ const duration = Date.now() - startTime;
1605
+ logger.info("list_local_screenshots: Completed", { count: screenshots.length, duration });
1606
+ if (screenshots.length === 0) {
1607
+ const filters = [];
1608
+ if (params.directory)
1609
+ filters.push(`directory=${params.directory}`);
1610
+ if (params.line)
1611
+ filters.push(`line=${params.line}`);
1612
+ if (params.lineRange)
1613
+ filters.push(`lineRange=${params.lineRange.start}-${params.lineRange.end}`);
1614
+ if (params.action)
1615
+ filters.push(`action=${params.action}`);
1616
+ if (params.phase)
1617
+ filters.push(`phase=${params.phase}`);
1618
+ if (params.pattern)
1619
+ filters.push(`pattern=${params.pattern}`);
1620
+ if (params.sequence)
1621
+ filters.push(`sequence=${params.sequence}`);
1622
+ if (params.sequenceRange)
1623
+ filters.push(`sequenceRange=${params.sequenceRange.start}-${params.sequenceRange.end}`);
1624
+ const filterMsg = filters.length > 0 ? ` with filters: ${filters.join(", ")}` : "";
1625
+ return createToolResult(true, `No screenshots found in .testdriver directory${filterMsg}.`, {
1626
+ action: "list_local_screenshots",
1627
+ count: 0,
1628
+ directory: testdriverDir,
1629
+ filters: params,
1630
+ duration
1631
+ });
1632
+ }
1633
+ const limit = params.limit || 50;
1634
+ const limitedScreenshots = screenshots.slice(0, limit);
1635
+ // Format the list for display with parsed info
1636
+ const screenshotList = limitedScreenshots.map((s, i) => {
1637
+ const relativePath = path.relative(testdriverDir, s.path);
1638
+ const sizeKB = Math.round(s.size / 1024);
1639
+ const timeAgo = formatTimeAgo(s.modified);
1640
+ // Add parsed info if available
1641
+ const parts = [`${i + 1}. ${relativePath}`];
1642
+ const meta = [];
1643
+ if (s.parsed.lineNumber)
1644
+ meta.push(`L${s.parsed.lineNumber}`);
1645
+ if (s.parsed.action)
1646
+ meta.push(s.parsed.action);
1647
+ if (s.parsed.phase)
1648
+ meta.push(s.parsed.phase);
1649
+ meta.push(`${sizeKB}KB`);
1650
+ meta.push(timeAgo);
1651
+ parts.push(`(${meta.join(", ")})`);
1652
+ return parts.join(" ");
1653
+ }).join("\n");
1654
+ const message = screenshots.length > limit
1655
+ ? `Found ${screenshots.length} screenshots (showing ${limit} results, sorted by ${sortBy}):\n\n${screenshotList}`
1656
+ : `Found ${screenshots.length} screenshot(s) (sorted by ${sortBy}):\n\n${screenshotList}`;
1657
+ return createToolResult(true, message, {
1658
+ action: "list_local_screenshots",
1659
+ count: screenshots.length,
1660
+ returned: limitedScreenshots.length,
1661
+ directory: testdriverDir,
1662
+ filters: params,
1663
+ sortBy,
1664
+ screenshots: limitedScreenshots.map(s => ({
1665
+ path: s.path,
1666
+ relativePath: path.relative(testdriverDir, s.path),
1667
+ name: s.name,
1668
+ modified: s.modified.toISOString(),
1669
+ sizeBytes: s.size,
1670
+ sequence: s.parsed.sequence,
1671
+ action: s.parsed.action,
1672
+ phase: s.parsed.phase,
1673
+ lineNumber: s.parsed.lineNumber,
1674
+ description: s.parsed.description,
1675
+ })),
1676
+ duration
1677
+ });
1678
+ }
1679
+ catch (error) {
1680
+ logger.error("list_local_screenshots: Failed", { error: String(error) });
1681
+ captureException(error, { tags: { tool: "list_local_screenshots" } });
1682
+ throw error;
1683
+ }
1684
+ });
1685
+ // Helper to format time ago
1686
+ function formatTimeAgo(date) {
1687
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
1688
+ if (seconds < 60)
1689
+ return `${seconds}s ago`;
1690
+ const minutes = Math.floor(seconds / 60);
1691
+ if (minutes < 60)
1692
+ return `${minutes}m ago`;
1693
+ const hours = Math.floor(minutes / 60);
1694
+ if (hours < 24)
1695
+ return `${hours}h ago`;
1696
+ const days = Math.floor(hours / 24);
1697
+ return `${days}d ago`;
1698
+ }
1699
+ // View Local Screenshot - view a screenshot from .testdriver directory
1700
+ // Returns the image so AI clients that support images can see it
1701
+ // Also displays to the user via MCP App
1702
+ registerAppTool(server, "view_local_screenshot", {
1703
+ title: "View Local Screenshot",
1704
+ description: `View a screenshot from the .testdriver directory.
1705
+
1706
+ Use 'list_local_screenshots' first to see available screenshots, then use this tool to view one.
1707
+
1708
+ This tool returns the image content so AI clients that support images can see it directly.
1709
+ The image is also displayed to the user via the MCP App UI.
1710
+
1711
+ Useful for:
1712
+ - Reviewing screenshots from previous test runs
1713
+ - Debugging test failures by examining saved screenshots
1714
+ - Comparing current screen state to saved screenshots`,
1715
+ inputSchema: z.object({
1716
+ path: z.string().describe("Full path to the screenshot file (from list_local_screenshots)"),
1717
+ }),
1718
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1719
+ }, async (params) => {
1720
+ const startTime = Date.now();
1721
+ logger.info("view_local_screenshot: Starting", { path: params.path });
1722
+ try {
1723
+ // Validate the path exists and is a PNG
1724
+ if (!fs.existsSync(params.path)) {
1725
+ logger.warn("view_local_screenshot: File not found", { path: params.path });
1726
+ return createToolResult(false, `Screenshot not found: ${params.path}`, { error: "File not found" });
1727
+ }
1728
+ if (!params.path.toLowerCase().endsWith(".png")) {
1729
+ logger.warn("view_local_screenshot: Not a PNG file", { path: params.path });
1730
+ return createToolResult(false, "Only PNG files are supported", { error: "Invalid file type" });
1731
+ }
1732
+ // Security check - only allow files from .testdriver directory
1733
+ const normalizedPath = path.resolve(params.path);
1734
+ if (!normalizedPath.includes(".testdriver")) {
1735
+ logger.warn("view_local_screenshot: Path not in .testdriver", { path: normalizedPath });
1736
+ return createToolResult(false, "Can only view screenshots from .testdriver directory", { error: "Security: path not allowed" });
1737
+ }
1738
+ // Read the file
1739
+ const imageBuffer = fs.readFileSync(params.path);
1740
+ const imageBase64 = imageBuffer.toString("base64");
1741
+ // Store image for MCP App UI display
1742
+ const screenshotResourceUri = storeImage(imageBase64, "screenshot");
1743
+ const stats = fs.statSync(params.path);
1744
+ const sizeKB = Math.round(stats.size / 1024);
1745
+ const fileName = path.basename(params.path);
1746
+ const duration = Date.now() - startTime;
1747
+ logger.info("view_local_screenshot: Completed", { path: params.path, sizeKB, duration });
1748
+ // Return the image content for AI clients that support images
1749
+ // The content array includes both text and image for maximum compatibility
1750
+ const content = [
1751
+ { type: "text", text: `Screenshot: ${fileName} (${sizeKB}KB)` },
1752
+ {
1753
+ type: "image",
1754
+ data: imageBase64,
1755
+ mimeType: "image/png"
1756
+ },
1757
+ ];
1758
+ return {
1759
+ content,
1760
+ structuredContent: {
1761
+ action: "view_local_screenshot",
1762
+ success: true,
1763
+ path: params.path,
1764
+ fileName,
1765
+ sizeBytes: stats.size,
1766
+ modified: stats.mtime.toISOString(),
1767
+ screenshotResourceUri,
1768
+ duration
1769
+ },
1770
+ };
1771
+ }
1772
+ catch (error) {
1773
+ logger.error("view_local_screenshot: Failed", { error: String(error), path: params.path });
1774
+ captureException(error, { tags: { tool: "view_local_screenshot" }, extra: { path: params.path } });
1775
+ throw error;
1776
+ }
1777
+ });
1778
+ // Screenshot - captures full screen to show user the current state
1779
+ // NOTE: This is for SHOWING the user the screen, not for AI understanding.
1780
+ // Use 'check' tool for AI to understand screen state.
1781
+ registerAppTool(server, "screenshot", {
1782
+ title: "Screenshot",
1783
+ description: `Display a screenshot to the user. This tool does NOT return the image to you (the AI).
1784
+
1785
+ ⚠️ IMPORTANT: Do NOT use this tool to understand the screen state. The screenshot is ONLY displayed to the human user - you will NOT receive the image or any analysis.
1786
+
1787
+ If you need to:
1788
+ - See what's on screen → use 'check' instead
1789
+ - Verify an action worked → use 'check' instead
1790
+ - Understand the current state → use 'check' instead
1791
+
1792
+ Only use 'screenshot' when you explicitly want to show something to the human user without needing to see it yourself.`,
1793
+ inputSchema: z.object({}),
1794
+ _meta: { ui: { resourceUri: RESOURCE_URI, expanded: true } },
1795
+ }, async () => {
1796
+ const startTime = Date.now();
1797
+ logger.info("screenshot: Starting");
1798
+ const sessionCheck = requireActiveSession();
1799
+ if (!sessionCheck.valid) {
1800
+ logger.warn("screenshot: No active session");
1801
+ return sessionCheck.error;
1802
+ }
1803
+ try {
1804
+ // Capture full screen screenshot
1805
+ const screenshotBase64 = await sdk.agent.system.captureScreenBase64(1, false, true);
1806
+ let screenshotResourceUri;
1807
+ if (screenshotBase64) {
1808
+ // Store raw base64 for the resource blob with unique ID
1809
+ screenshotResourceUri = storeImage(screenshotBase64, "screenshot");
1810
+ }
1811
+ const duration = Date.now() - startTime;
1812
+ logger.info("screenshot: Completed", { duration, hasImage: !!screenshotBase64 });
1813
+ // Only send the resource URI - the MCP app will fetch the image via resources/read
1814
+ // This keeps the base64 image data OUT of AI context
1815
+ return createToolResult(true, "Screenshot captured and displayed to user", {
1816
+ action: "screenshot",
1817
+ screenshotResourceUri,
1818
+ duration
1819
+ });
1820
+ }
1821
+ catch (error) {
1822
+ logger.error("screenshot: Failed", { error: String(error) });
1823
+ return createToolResult(false, `Screenshot failed: ${error}`, { error: String(error) });
1824
+ }
1825
+ });
1826
+ // Init - Initialize a new TestDriver project
1827
+ server.registerTool("init", {
1828
+ description: `Initialize a new TestDriver project with Vitest SDK examples.
1829
+
1830
+ This creates:
1831
+ - package.json with proper dependencies
1832
+ - Example test files (tests/example.test.js, tests/login.js)
1833
+ - vitest.config.js
1834
+ - .gitignore
1835
+ - GitHub Actions workflow (.github/workflows/testdriver.yml)
1836
+ - VSCode MCP config (.vscode/mcp.json)
1837
+ - VSCode extensions recommendations (.vscode/extensions.json)
1838
+ - TestDriver skills (.github/skills/)
1839
+ - TestDriver agents (.github/agents/)
1840
+ - .env file with API key (if provided)
1841
+
1842
+ API Key: The apiKey parameter is optional. If not provided, you'll need to manually add TD_API_KEY to the .env file after initialization. The project structure will still be created successfully.`,
1843
+ inputSchema: z.object({
1844
+ directory: z.string().optional().describe("Target directory (defaults to current working directory)"),
1845
+ apiKey: z.string().optional().describe("TestDriver API key (will be saved to .env)"),
1846
+ skipInstall: z.boolean().default(false).describe("Skip npm install step"),
1847
+ }),
1848
+ }, async (params) => {
1849
+ const startTime = Date.now();
1850
+ const targetDir = params.directory ? path.resolve(params.directory) : process.cwd();
1851
+ logger.info("init: Starting", { targetDir, hasApiKey: !!params.apiKey, skipInstall: params.skipInstall });
1852
+ try {
1853
+ // Import the shared init logic (dynamic import for ESM/CJS compatibility)
1854
+ const initProjectPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "lib", "init-project.js");
1855
+ const { initProject } = await import(pathToFileURL(initProjectPath).href);
1856
+ // Run the shared init logic
1857
+ const result = await initProject({
1858
+ targetDir,
1859
+ apiKey: params.apiKey,
1860
+ skipInstall: params.skipInstall,
1861
+ });
1862
+ const duration = Date.now() - startTime;
1863
+ logger.info("init: Completed", { targetDir, duration, success: result.success });
1864
+ const nextSteps = `
1865
+
1866
+ 📚 Next steps:
1867
+
1868
+ 1. Run your tests:
1869
+ vitest run
1870
+
1871
+ 2. Use AI agents to write tests:
1872
+ Open VSCode/Cursor and use @testdriver agent
1873
+
1874
+ 3. MCP server configured:
1875
+ TestDriver tools available via MCP in .vscode/mcp.json
1876
+
1877
+ 4. For CI/CD, add TD_API_KEY to your GitHub repository secrets:
1878
+ Settings → Secrets → Actions → New repository secret
1879
+
1880
+ Learn more at https://docs.testdriver.ai/v7/getting-started/
1881
+ `;
1882
+ const allMessages = [...result.results, ...result.errors.map((e) => `⚠️ ${e}`)];
1883
+ return createToolResult(result.success, result.success
1884
+ ? `✅ TestDriver project initialized successfully!\n\n${allMessages.join("\n")}${nextSteps}`
1885
+ : `⚠️ TestDriver project initialization completed with errors:\n\n${allMessages.join("\n")}`, {
1886
+ action: "init",
1887
+ targetDir,
1888
+ filesCreated: result.results.length,
1889
+ hasApiKey: !!params.apiKey,
1890
+ errors: result.errors,
1891
+ duration
1892
+ });
1893
+ }
1894
+ catch (error) {
1895
+ logger.error("init: Failed", { error: String(error), targetDir });
1896
+ captureException(error, { tags: { tool: "init" }, extra: { targetDir } });
1897
+ throw error;
1898
+ }
1899
+ });
1900
+ // Start the server
1901
+ async function main() {
1902
+ logger.info("Starting TestDriver MCP Server", {
1903
+ version,
1904
+ logLevel: process.env.TD_LOG_LEVEL || "INFO",
1905
+ distDir: DIST_DIR,
1906
+ sentryEnabled: isSentryEnabled(),
1907
+ });
1908
+ const transport = new StdioServerTransport();
1909
+ await server.connect(transport);
1910
+ logger.info("TestDriver MCP Server running on stdio");
1911
+ // Handle graceful shutdown
1912
+ const shutdown = async () => {
1913
+ logger.info("Shutting down MCP Server");
1914
+ await flushSentry();
1915
+ process.exit(0);
1916
+ };
1917
+ process.on("SIGINT", shutdown);
1918
+ process.on("SIGTERM", shutdown);
1919
+ }
1920
+ main().catch(async (error) => {
1921
+ logger.error("Server failed to start", { error: String(error) });
1922
+ captureException(error, { tags: { phase: "startup" } });
1923
+ await flushSentry();
1924
+ process.exit(1);
1925
+ });