testdriverai 7.2.73 → 7.2.75

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.
@@ -1,13 +1,16 @@
1
1
  ---
2
+ name: testdriver
2
3
  description: An expert at creating and refining automated tests using TestDriver.ai
3
- capabilities:
4
- [
5
- "create tests",
6
- "refine tests",
7
- "debug tests",
8
- "use MCP workflow",
9
- "visual verification",
10
- ]
4
+ tools:
5
+ mcp-servers:
6
+ testdriver:
7
+ command: npx
8
+ args:
9
+ - -p
10
+ - testdriverai@beta
11
+ - testdriverai-mcp
12
+ env:
13
+ TD_API_KEY: ${TD_API_KEY}
11
14
  ---
12
15
 
13
16
  # TestDriver Expert
@@ -35,11 +38,12 @@ Use this agent when the user asks to:
35
38
  ### Workflow
36
39
 
37
40
  1. **Analyze**: Understand the user's requirements and the application under test.
38
- 2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app.
39
- 3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot showing the result.
40
- 4. **Verify**: Use `check` after actions and `assert` for test conditions.
41
- 5. **Commit**: Use `commit` to write recorded commands to a test file.
42
- 6. **Verify Test**: Use `verify` to run the generated test from scratch.
41
+ 2. **Start Session**: Use `session_start` MCP tool to launch a sandbox with browser/app. Specify `testFile` to track where code should be written.
42
+ 3. **Interact**: Use MCP tools (`find`, `click`, `type`, etc.) - each returns a screenshot AND generated code.
43
+ 4. **⚠️ WRITE CODE IMMEDIATELY**: After EVERY successful action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the end.
44
+ 5. **Verify Actions**: Use `check` after actions to verify they succeeded (for YOUR understanding only).
45
+ 6. **Add Assertions**: Use `assert` for test conditions that should be in the final test file.
46
+ 7. **⚠️ RUN THE TEST YOURSELF**: Use `npx vitest run <testFile>` to run the test - do NOT tell the user to run it. Iterate until it passes.
43
47
 
44
48
  ## Prerequisites
45
49
 
@@ -108,12 +112,16 @@ describe("My Test Suite", () => {
108
112
  await testdriver.provision.chrome({
109
113
  url: "https://example.com",
110
114
  });
115
+ await testdriver.screenshot(); // Capture initial page state
111
116
 
112
117
  // Find elements and interact
113
118
  const button = await testdriver.find("Sign In button");
119
+ await testdriver.screenshot(); // Capture before click
114
120
  await button.click();
121
+ await testdriver.screenshot(); // Capture after click
115
122
 
116
123
  // Assert using natural language
124
+ await testdriver.screenshot(); // Capture before assertion
117
125
  const result = await testdriver.assert("the dashboard is visible");
118
126
  expect(result).toBeTruthy();
119
127
  });
@@ -177,9 +185,9 @@ await element.mouseUp(); // release mouse
177
185
  element.found(); // check if found (boolean)
178
186
  ```
179
187
 
180
- ### Screenshots
188
+ ### Screenshots for Debugging
181
189
 
182
- Use `screenshot()` **only when the user explicitly asks** to see what the screen looks like. Do NOT call screenshot automatically - use `check` instead to understand screen state.
190
+ **Use `screenshot()` liberally throughout your tests** to capture the screen state at key moments. This makes debugging much easier when tests fail - you can see exactly what the screen looked like at each step.
183
191
 
184
192
  ```javascript
185
193
  // Capture a screenshot - saved to .testdriver/screenshots/<test-file>/
@@ -190,6 +198,14 @@ console.log("Screenshot saved to:", screenshotPath);
190
198
  await testdriver.screenshot(1, false, true);
191
199
  ```
192
200
 
201
+ **When to add screenshots:**
202
+ - After provisioning (initial page load)
203
+ - Before and after clicking important elements
204
+ - After typing text into fields
205
+ - Before assertions (to see what the AI is evaluating)
206
+ - After any action that changes the page state
207
+ - When debugging a flaky or failing test
208
+
193
209
  **Screenshot file organization:**
194
210
 
195
211
  ```
@@ -210,107 +226,106 @@ await testdriver.screenshot(1, false, true);
210
226
  ### Key Advantages
211
227
 
212
228
  - **No need to restart** - continue from current state
213
- - **Automatic command recording** - successful commands are logged
214
- - **Code generation** - convert recorded commands to test files
229
+ - **Generated code with every action** - each tool returns the code to add to your test
215
230
  - **Use `check` to verify** - understand screen state without explicit screenshots
216
231
 
232
+ ### ⚠️ CRITICAL: Write Code Immediately & Run Tests Yourself
233
+
234
+ **Every MCP tool response includes "ACTION REQUIRED: Append this code..." - you MUST write that code to the test file IMMEDIATELY before proceeding to the next action.**
235
+
236
+ **When ready to validate, RUN THE TEST YOURSELF using `npx vitest run`. Do NOT tell the user to run it.**
237
+
217
238
  ### Step 1: Start a Session
218
239
 
219
240
  ```
220
- session_start({ type: "chrome", url: "https://your-app.com/login" })
241
+ session_start({ type: "chrome", url: "https://your-app.com/login", testFile: "tests/login.test.mjs" })
221
242
  → Screenshot shows login page
243
+ → Response includes: "ACTION REQUIRED: Append this code..."
244
+ → ⚠️ IMMEDIATELY write to tests/login.test.mjs:
245
+ await testdriver.provision.chrome({ url: "https://your-app.com/login" });
246
+ await testdriver.screenshot(); // Capture initial page state
222
247
  ```
223
248
 
224
249
  This provisions a sandbox with Chrome and navigates to your URL. You'll see a screenshot of the initial page.
225
250
 
226
251
  ### Step 2: Interact with the App
227
252
 
228
- Find elements and interact with them:
253
+ Find elements and interact with them. **Write code to file after EACH action, including screenshots for debugging:**
229
254
 
230
255
  ```
231
- find({ description: "email input field" })
232
- → Returns: screenshot with element highlighted, coordinates, and a ref ID
233
-
234
- click({ elementRef: "el-123456" })
235
- Returns: screenshot with click marker
256
+ find_and_click({ description: "email input field" })
257
+ → Returns: screenshot with element highlighted
258
+ → ⚠️ IMMEDIATELY append to test file:
259
+ await testdriver.find("email input field").click();
260
+ await testdriver.screenshot(); // Capture after click
236
261
 
237
262
  type({ text: "user@example.com" })
238
263
  → Returns: screenshot showing typed text
264
+ → ⚠️ IMMEDIATELY append to test file:
265
+ await testdriver.type("user@example.com");
266
+ await testdriver.screenshot(); // Capture after typing
239
267
  ```
240
268
 
241
- Or combine find + click in one step:
242
-
243
- ```
244
- find_and_click({ description: "Sign In button" })
245
- ```
246
-
247
- ### Step 3: Verify Actions Succeeded
269
+ ### Step 3: Verify Actions Succeeded (For Your Understanding)
248
270
 
249
- After each action, use `check` to verify it worked:
271
+ After actions, use `check` to verify they worked. This is for YOUR understanding - does NOT generate code:
250
272
 
251
273
  ```
252
274
  check({ task: "Was the email entered into the field?" })
253
275
  → Returns: AI analysis comparing previous screenshot to current state
254
276
  ```
255
277
 
256
- ### Step 4: Add Assertions
278
+ ### Step 4: Add Assertions (Generates Code)
257
279
 
258
- Use `assert` for pass/fail conditions that get recorded in test files:
280
+ Use `assert` for pass/fail conditions. This DOES generate code for the test file:
259
281
 
260
282
  ```
261
283
  assert({ assertion: "the dashboard is visible" })
262
284
  → Returns: pass/fail with screenshot
285
+ → ⚠️ IMMEDIATELY append to test file:
286
+ await testdriver.screenshot(); // Capture before assertion
287
+ const assertResult = await testdriver.assert("the dashboard is visible");
288
+ expect(assertResult).toBeTruthy();
263
289
  ```
264
290
 
265
- ### Step 5: Commit to Test File
291
+ ### Step 5: Run the Test Yourself
266
292
 
267
- When your sequence works, save it:
293
+ **⚠️ YOU must run the test - do NOT tell the user to run it:**
268
294
 
295
+ ```bash
296
+ npx vitest run tests/login.test.mjs
269
297
  ```
270
- commit({
271
- testFile: "tests/login.test.mjs",
272
- testName: "Login Flow",
273
- testDescription: "User can log in with email and password"
274
- })
275
- ```
276
-
277
- ### Step 6: Verify the Test
278
298
 
279
- Run the generated test from scratch to ensure it works:
280
-
281
- ```
282
- verify({ testFile: "tests/login.test.mjs" })
283
- ```
299
+ Analyze the output, fix any issues, and iterate until the test passes.
284
300
 
285
301
  ### MCP Tools Reference
286
302
 
287
303
  | Tool | Description |
288
304
  |------|-------------|
289
- | `session_start` | Start sandbox with browser/app, capture initial screenshot |
290
- | `session_status` | Check session health, time remaining, command count |
305
+ | `session_start` | Start sandbox with browser/app, returns screenshot + provision code |
306
+ | `session_status` | Check session health and time remaining |
291
307
  | `session_extend` | Add more time before session expires |
292
308
  | `find` | Locate element by description, returns ref for later use |
293
- | `click` | Click on element ref or coordinates |
309
+ | `click` | Click on element ref |
294
310
  | `find_and_click` | Find and click in one action |
295
311
  | `type` | Type text into focused field |
296
312
  | `press_keys` | Press keyboard shortcuts (e.g., `["ctrl", "a"]`) |
297
313
  | `scroll` | Scroll page (up/down/left/right) |
298
- | `check` | AI analysis of whether a task completed |
299
- | `assert` | AI-powered boolean assertion (pass/fail for test files) |
314
+ | `check` | AI analysis of screen state - for YOUR understanding only, does NOT generate code |
315
+ | `assert` | AI-powered boolean assertion - GENERATES CODE for test files |
300
316
  | `exec` | Execute JavaScript, shell, or PowerShell in sandbox |
301
317
  | `screenshot` | Capture screenshot - **only use when user explicitly asks** |
302
- | `commit` | Write recorded commands to test file |
303
- | `verify` | Run test file from scratch |
304
- | `get_command_log` | View recorded commands before committing |
305
318
 
306
319
  ### Tips for MCP Workflow
307
320
 
308
- 1. **Work incrementally** - Don't try to build the entire test at once
309
- 2. **Use `check` after every action** - Verify your actions succeeded before moving on
310
- 3. **Be specific with element descriptions** - "the blue Sign In button in the header" is better than "button"
311
- 4. **Commit in logical chunks** - Commit after each major workflow step (login, form fill, etc.)
312
- 5. **Extend session proactively** - Sessions expire after 5 minutes; use `session_extend` if needed
313
- 6. **Review the command log** - Use `get_command_log` to see what will be committed
321
+ 1. **⚠️ Write code IMMEDIATELY** - After EVERY action, append generated code to test file RIGHT AWAY
322
+ 2. **⚠️ Run tests YOURSELF** - Use `npx vitest run` - do NOT tell user to run tests
323
+ 3. **⚠️ Add screenshots liberally** - Include `await testdriver.screenshot()` after every significant action for debugging
324
+ 4. **Work incrementally** - Don't try to build the entire test at once
325
+ 5. **Use `check` after actions** - Verify your actions succeeded before moving on (for YOUR understanding)
326
+ 6. **Use `assert` for test verifications** - These generate code that goes in the test file
327
+ 7. **Be specific with element descriptions** - "the blue Sign In button in the header" is better than "button"
328
+ 8. **Extend session proactively** - Sessions expire after 5 minutes; use `session_extend` if needed
314
329
 
315
330
  ## Recommended Development Workflow
316
331
 
@@ -325,17 +340,21 @@ verify({ testFile: "tests/login.test.mjs" })
325
340
  it("should incrementally build test", async (context) => {
326
341
  const testdriver = TestDriver(context);
327
342
  await testdriver.provision.chrome({ url: "https://example.com" });
343
+ await testdriver.screenshot(); // Capture initial state
328
344
 
329
345
  // Step 1: Find and inspect
330
346
  const element = await testdriver.find("Some button");
331
347
  console.log("Element found:", element.found());
332
348
  console.log("Coordinates:", element.x, element.y);
333
349
  console.log("Confidence:", element.confidence);
350
+ await testdriver.screenshot(); // Capture after find
334
351
 
335
352
  // Step 2: Interact
336
353
  await element.click();
354
+ await testdriver.screenshot(); // Capture after click
337
355
 
338
356
  // Step 3: Assert and log
357
+ await testdriver.screenshot(); // Capture before assertion
339
358
  const result = await testdriver.assert("Something happened");
340
359
  console.log("Assertion result:", result);
341
360
  expect(result).toBeTruthy();
@@ -417,33 +436,42 @@ const date = await testdriver.exec("pwsh", "Get-Date", 5000);
417
436
 
418
437
  ### Capturing Screenshots
419
438
 
439
+ **Add screenshots liberally throughout your tests** for debugging. When a test fails, you'll have a visual trail showing exactly what happened at each step.
440
+
420
441
  ```javascript
421
- // Capture a screenshot and save to file
422
- const screenshot = await testdriver.screenshot();
423
- const filepath = "screenshot.png";
424
- fs.writeFileSync(filepath, Buffer.from(screenshot, "base64"));
425
- console.log("Screenshot saved to:", filepath);
442
+ // Basic screenshot - automatically saved to .testdriver/screenshots/<test-file>/
443
+ await testdriver.screenshot();
426
444
 
427
445
  // Capture with mouse cursor visible
428
- const screenshotWithMouse = await testdriver.screenshot(1, false, true);
429
- fs.writeFileSync(
430
- "screenshot-with-mouse.png",
431
- Buffer.from(screenshotWithMouse, "base64"),
432
- );
433
- console.log("Screenshot with mouse saved to: screenshot-with-mouse.png");
446
+ await testdriver.screenshot(1, false, true);
447
+
448
+ // Recommended pattern: screenshot after every significant action
449
+ await testdriver.provision.chrome({ url: "https://example.com" });
450
+ await testdriver.screenshot(); // After page load
451
+
452
+ await testdriver.find("Login button").click();
453
+ await testdriver.screenshot(); // After click
454
+
455
+ await testdriver.type("user@example.com");
456
+ await testdriver.screenshot(); // After typing
457
+
458
+ await testdriver.screenshot(); // Before assertion
459
+ const result = await testdriver.assert("dashboard is visible");
434
460
  ```
435
461
 
436
462
  ## Tips for Agents
437
463
 
438
- 1. **Use MCP tools for development** - Don't write test files manually; use the MCP workflow to build tests interactively
439
- 2. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
440
- 3. **Look at test samples** in `node_modules/testdriverai/test` for working examples
441
- 4. **Use `check` to understand screen state** - This is how you verify what the sandbox shows. Only use `screenshot` when the user asks to see the screen.
442
- 5. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis, `assert` gives boolean pass/fail
443
- 6. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
444
- 7. **Start simple** - get one step working before adding more
445
- 8. **Commit working sequences** - Don't lose progress; use `commit` after each successful interaction sequence
446
- 9. **Always `await` async methods** - TestDriver will warn if you forget, but for TypeScript projects, add `@typescript-eslint/no-floating-promises` to your ESLint config to catch missing `await` at compile time:
464
+ 1. **⚠️ WRITE CODE IMMEDIATELY** - After EVERY successful MCP action, append the generated code to the test file RIGHT AWAY. Do NOT wait until the session ends.
465
+ 2. **⚠️ RUN TESTS YOURSELF** - Do NOT tell the user to run tests. YOU must run the tests using `npx vitest run <testFile>`. Analyze the output and iterate until the test passes.
466
+ 3. **⚠️ ADD SCREENSHOTS LIBERALLY** - Include `await testdriver.screenshot()` throughout your tests: after provision, before/after clicks, after typing, and before assertions. This creates a visual trail that makes debugging failures much easier.
467
+ 4. **Use MCP tools for development** - Build tests interactively with visual feedback
468
+ 5. **Always check `sdk.d.ts`** for method signatures and types when debugging generated tests
469
+ 6. **Look at test samples** in `node_modules/testdriverai/test` for working examples
470
+ 7. **Use `check` to understand screen state** - This is how you verify what the sandbox shows during MCP development.
471
+ 8. **Use `check` after actions, `assert` for test files** - `check` gives detailed AI analysis (no code), `assert` gives boolean pass/fail (generates code)
472
+ 9. **Be specific with element descriptions** - "blue Sign In button in the header" > "button"
473
+ 10. **Start simple** - get one step working before adding more
474
+ 11. **Always `await` async methods** - TestDriver will warn if you forget, but for TypeScript projects, add `@typescript-eslint/no-floating-promises` to your ESLint config to catch missing `await` at compile time:
447
475
 
448
476
  ```json
449
477
  // eslint.config.js (for TypeScript projects)
@@ -453,5 +481,3 @@ console.log("Screenshot with mouse saved to: screenshot-with-mouse.png");
453
481
  }
454
482
  }
455
483
  ```
456
-
457
- 10. **Use `verify` to validate tests** - After committing, run `verify` to ensure the generated test works from scratch.
@@ -21,6 +21,7 @@ class InitCommand extends BaseCommand {
21
21
  await this.createGitHubWorkflow();
22
22
  await this.createGitignore();
23
23
  await this.createVscodeMcpConfig();
24
+ await this.createVscodeExtensions();
24
25
  await this.installDependencies();
25
26
  await this.copySkills();
26
27
  await this.createAgents();
@@ -149,7 +150,10 @@ class InitCommand extends BaseCommand {
149
150
  try {
150
151
  execSync(`setx ${key} "${value}"`, { stdio: "ignore" });
151
152
  console.log(
152
- chalk.green(` ✓ Set ${key} as user environment variable\n`),
153
+ chalk.green(` ✓ Set ${key} as user environment variable`),
154
+ );
155
+ console.log(
156
+ chalk.gray(` Restart your terminal for changes to take effect\n`),
153
157
  );
154
158
  } catch (error) {
155
159
  console.log(
@@ -184,7 +188,10 @@ class InitCommand extends BaseCommand {
184
188
  );
185
189
  fs.writeFileSync(profilePath, updated);
186
190
  console.log(
187
- chalk.green(` ✓ Updated ${key} in ${profilePath}\n`),
191
+ chalk.green(` ✓ Updated ${key} in ${profilePath}`),
192
+ );
193
+ console.log(
194
+ chalk.gray(` Run: source ${profilePath} (or open a new terminal)\n`),
188
195
  );
189
196
  return;
190
197
  }
@@ -193,7 +200,10 @@ class InitCommand extends BaseCommand {
193
200
  // Append to profile
194
201
  fs.appendFileSync(profilePath, `\n${exportLine}\n`);
195
202
  console.log(
196
- chalk.green(` ✓ Added ${key} to ${profilePath}\n`),
203
+ chalk.green(` ✓ Added ${key} to ${profilePath}`),
204
+ );
205
+ console.log(
206
+ chalk.gray(` Run: source ${profilePath} (or open a new terminal)\n`),
197
207
  );
198
208
  }
199
209
 
@@ -346,11 +356,8 @@ test('should login and add item to cart', async (context) => {
346
356
  if (!fs.existsSync(configFile)) {
347
357
  const configContent = `import { defineConfig } from 'vitest/config';
348
358
  import TestDriver from 'testdriverai/vitest';
349
- import { config } from 'dotenv';
350
-
351
- // Load environment variables from .env file
352
- config();
353
359
 
360
+ // Note: dotenv is loaded automatically by the TestDriver SDK
354
361
  export default defineConfig({
355
362
  test: {
356
363
  testTimeout: 300000,
@@ -478,10 +485,10 @@ jobs:
478
485
 
479
486
  if (!fs.existsSync(mcpConfigFile)) {
480
487
  const mcpConfig = {
481
- mcpServers: {
488
+ servers: {
482
489
  testdriver: {
483
490
  command: "npx",
484
- args: ["testdriverai-mcp"],
491
+ args: ["-p", "testdriverai@beta", "testdriverai-mcp"],
485
492
  env: {
486
493
  TD_API_KEY: "${TD_API_KEY}",
487
494
  },
@@ -499,6 +506,36 @@ jobs:
499
506
  }
500
507
  }
501
508
 
509
+ /**
510
+ * Create VSCode extensions recommendations
511
+ */
512
+ async createVscodeExtensions() {
513
+ const vscodeDir = path.join(process.cwd(), ".vscode");
514
+ const extensionsFile = path.join(vscodeDir, "extensions.json");
515
+
516
+ // Create .vscode directory if it doesn't exist
517
+ if (!fs.existsSync(vscodeDir)) {
518
+ fs.mkdirSync(vscodeDir, { recursive: true });
519
+ console.log(chalk.gray(` Created directory: ${vscodeDir}`));
520
+ }
521
+
522
+ if (!fs.existsSync(extensionsFile)) {
523
+ const extensionsConfig = {
524
+ recommendations: [
525
+ "vitest.explorer",
526
+ ],
527
+ };
528
+
529
+ fs.writeFileSync(
530
+ extensionsFile,
531
+ JSON.stringify(extensionsConfig, null, 2) + "\n",
532
+ );
533
+ console.log(chalk.green(` Created extensions config: ${extensionsFile}`));
534
+ } else {
535
+ console.log(chalk.gray(" Extensions config already exists, skipping..."));
536
+ }
537
+ }
538
+
502
539
  /**
503
540
  * Copy TestDriver skills from the package to the project
504
541
  */
@@ -557,7 +594,7 @@ jobs:
557
594
  }
558
595
 
559
596
  /**
560
- * Create TestDriver agents in GitHub Copilot format
597
+ * Copy TestDriver agents to .github/agents
561
598
  */
562
599
  async createAgents() {
563
600
  const agentsDestDir = path.join(process.cwd(), ".github", "agents");
@@ -576,98 +613,36 @@ jobs:
576
613
  }
577
614
  }
578
615
 
616
+ if (!agentsSourceDir) {
617
+ console.log(chalk.yellow(" ⚠️ Agents directory not found, skipping agents copy..."));
618
+ return;
619
+ }
620
+
579
621
  // Create .github/agents directory if it doesn't exist
580
622
  if (!fs.existsSync(agentsDestDir)) {
581
623
  fs.mkdirSync(agentsDestDir, { recursive: true });
582
624
  console.log(chalk.gray(` Created directory: ${agentsDestDir}`));
583
625
  }
584
626
 
585
- // If we found source agents, convert them to .agent.md format
586
- if (agentsSourceDir) {
587
- const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
588
-
589
- for (const agentFile of agentFiles) {
590
- const sourcePath = path.join(agentsSourceDir, agentFile);
591
- const agentName = agentFile.replace(".md", "");
592
- const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
593
-
594
- if (!fs.existsSync(destPath)) {
595
- const sourceContent = fs.readFileSync(sourcePath, "utf8");
596
-
597
- // Parse the source frontmatter and body
598
- const frontmatterMatch = sourceContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
599
-
600
- if (frontmatterMatch) {
601
- const frontmatterText = frontmatterMatch[1];
602
- const body = frontmatterMatch[2];
603
-
604
- // Extract description from frontmatter
605
- const descMatch = frontmatterText.match(/description:\s*["']?(.*?)["']?$/m);
606
- const description = descMatch ? descMatch[1] : `TestDriver ${agentName} agent`;
607
-
608
- // Create GitHub Copilot agent format
609
- const agentContent = `---
610
- name: ${agentName}
611
- description: ${description}
612
- tools:
613
- - testdriver/*
614
- mcp-servers:
615
- testdriver:
616
- command: npx
617
- args:
618
- - testdriverai-mcp
619
- env:
620
- TD_API_KEY: \${TD_API_KEY}
621
- ---
622
- ${body}`;
623
-
624
- fs.writeFileSync(destPath, agentContent);
625
- console.log(chalk.green(` Created agent: ${destPath}`));
626
- }
627
- } else {
628
- console.log(chalk.gray(` Agent ${agentName}.agent.md already exists, skipping...`));
629
- }
627
+ // Copy agent files with .agent.md extension
628
+ const agentFiles = fs.readdirSync(agentsSourceDir).filter(f => f.endsWith(".md"));
629
+ let copiedCount = 0;
630
+
631
+ for (const agentFile of agentFiles) {
632
+ const sourcePath = path.join(agentsSourceDir, agentFile);
633
+ const agentName = agentFile.replace(".md", "");
634
+ const destPath = path.join(agentsDestDir, `${agentName}.agent.md`);
635
+
636
+ if (!fs.existsSync(destPath)) {
637
+ fs.copyFileSync(sourcePath, destPath);
638
+ copiedCount++;
630
639
  }
631
- } else {
632
- // Create a default test-writer agent if no source found
633
- const defaultAgentPath = path.join(agentsDestDir, "test-writer.agent.md");
634
-
635
- if (!fs.existsSync(defaultAgentPath)) {
636
- const defaultAgentContent = `---
637
- name: test-writer
638
- description: An expert at creating and refining automated tests using TestDriver.ai
639
- tools:
640
- - testdriver/*
641
- mcp-servers:
642
- testdriver:
643
- command: npx
644
- args:
645
- - testdriverai-mcp
646
- env:
647
- TD_API_KEY: \${TD_API_KEY}
648
- ---
649
- # TestDriver Expert
650
-
651
- You are an expert at writing automated tests using the TestDriver library. Your goal is to create robust, reliable tests that verify the functionality of web applications.
652
-
653
- ## Workflow
654
-
655
- 1. **Start Session**: Use \`session_start\` to provision a sandbox with browser
656
- 2. **Interact**: Use \`find\`, \`click\`, \`type\` etc. - each returns a screenshot
657
- 3. **Verify**: Use \`check\` after actions and \`assert\` for test conditions
658
- 4. **Build Test**: Append generated code to your test file
659
- 5. **Validate**: Use \`verify\` to run the test from scratch
660
-
661
- ## Tips
662
-
663
- - Be specific with element descriptions: "blue Sign In button in the header" > "button"
664
- - Use \`check\` after actions to verify they succeeded
665
- - Start simple - get one step working before adding more
666
- `;
640
+ }
667
641
 
668
- fs.writeFileSync(defaultAgentPath, defaultAgentContent);
669
- console.log(chalk.green(` Created default agent: ${defaultAgentPath}`));
670
- }
642
+ if (copiedCount > 0) {
643
+ console.log(chalk.green(` Copied ${copiedCount} agent(s) to ${agentsDestDir}`));
644
+ } else {
645
+ console.log(chalk.gray(" Agents already exist, skipping..."));
671
646
  }
672
647
  }
673
648
 
@@ -705,7 +680,7 @@ You are an expert at writing automated tests using the TestDriver library. Your
705
680
  console.log(" 1. Run your tests:");
706
681
  console.log(chalk.gray(" npx vitest run\n"));
707
682
  console.log(" 2. Use AI agents to write tests:");
708
- console.log(chalk.gray(" Open VSCode/Cursor and use @test-writer agent\n"));
683
+ console.log(chalk.gray(" Open VSCode/Cursor and use @testdriver agent\n"));
709
684
  console.log(" 3. MCP server configured:");
710
685
  console.log(chalk.gray(" TestDriver tools available via MCP in .vscode/mcp.json\n"));
711
686
  console.log(
@@ -47,7 +47,7 @@ class SetupCommand extends Command {
47
47
 
48
48
  this.installSkills(sourceSkills, path.join(CLAUDE_HOME, "skills"));
49
49
  this.installAgents(sourceAgents, path.join(CLAUDE_HOME, "agents"));
50
- this.installMcp();
50
+ this.installClaudeMcp();
51
51
  this.installCursorMcp();
52
52
  await this.promptForApiKey();
53
53
 
@@ -125,7 +125,7 @@ class SetupCommand extends Command {
125
125
  /**
126
126
  * Add testdriver MCP server to ~/.claude.json
127
127
  */
128
- installMcp() {
128
+ installClaudeMcp() {
129
129
  let config = {};
130
130
 
131
131
  if (fs.existsSync(CLAUDE_MCP_FILE)) {
@@ -182,13 +182,13 @@ class SetupCommand extends Command {
182
182
  }
183
183
  }
184
184
 
185
- if (!config.mcpServers) {
186
- config.mcpServers = {};
185
+ if (!config.servers) {
186
+ config.servers = {};
187
187
  }
188
188
 
189
- const alreadyConfigured = config.mcpServers["testdriver-cloud"];
189
+ const alreadyConfigured = config.servers["testdriver-cloud"];
190
190
 
191
- Object.assign(config.mcpServers, CURSOR_MCP_SERVER_CONFIG);
191
+ Object.assign(config.servers, CURSOR_MCP_SERVER_CONFIG);
192
192
  fs.writeFileSync(CURSOR_MCP_FILE, JSON.stringify(config, null, 2) + "\n");
193
193
 
194
194
  if (alreadyConfigured) {
@@ -90,8 +90,8 @@ Note: This type uses \`Record<K, string | undefined>\` rather than \`Partial<Rec
90
90
  for compatibility with Zod schema generation. Both are functionally equivalent for validation.`);c.object({method:c.literal("ui/open-link"),params:c.object({url:c.string().describe("URL to open in the host's browser")})});var fI=c.object({isError:c.boolean().optional().describe("True if the host failed to open the URL (e.g., due to security policy).")}).passthrough(),pI=c.object({isError:c.boolean().optional().describe("True if the host rejected or failed to deliver the message.")}).passthrough();c.object({method:c.literal("ui/notifications/sandbox-proxy-ready"),params:c.object({})});var fo=c.object({connectDomains:c.array(c.string()).optional().describe("Origins for network requests (fetch/XHR/WebSocket)."),resourceDomains:c.array(c.string()).optional().describe("Origins for static resources (scripts, images, styles, fonts)."),frameDomains:c.array(c.string()).optional().describe("Origins for nested iframes (frame-src directive)."),baseUriDomains:c.array(c.string()).optional().describe("Allowed base URIs for the document (base-uri directive).")}),po=c.object({camera:c.object({}).optional().describe("Request camera access (Permission Policy `camera` feature)."),microphone:c.object({}).optional().describe("Request microphone access (Permission Policy `microphone` feature)."),geolocation:c.object({}).optional().describe("Request geolocation access (Permission Policy `geolocation` feature)."),clipboardWrite:c.object({}).optional().describe("Request clipboard write access (Permission Policy `clipboard-write` feature).")});c.object({method:c.literal("ui/notifications/size-changed"),params:c.object({width:c.number().optional().describe("New width in pixels."),height:c.number().optional().describe("New height in pixels.")})});var vI=c.object({method:c.literal("ui/notifications/tool-input"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Complete tool call arguments as key-value pairs.")).optional().describe("Complete tool call arguments as key-value pairs.")})}),hI=c.object({method:c.literal("ui/notifications/tool-input-partial"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Partial tool call arguments (incomplete, may change).")).optional().describe("Partial tool call arguments (incomplete, may change).")})}),gI=c.object({method:c.literal("ui/notifications/tool-cancelled"),params:c.object({reason:c.string().optional().describe('Optional reason for the cancellation (e.g., "user action", "timeout").')})}),$I=c.object({fonts:c.string().optional()}),_I=c.object({variables:mI.optional().describe("CSS variables for theming the app."),css:$I.optional().describe("CSS blocks that apps can inject.")}),bI=c.object({method:c.literal("ui/resource-teardown"),params:c.object({})});c.record(c.string(),c.unknown());var ts=c.object({text:c.object({}).optional().describe("Host supports text content blocks."),image:c.object({}).optional().describe("Host supports image content blocks."),audio:c.object({}).optional().describe("Host supports audio content blocks."),resource:c.object({}).optional().describe("Host supports resource content blocks."),resourceLink:c.object({}).optional().describe("Host supports resource link content blocks."),structuredContent:c.object({}).optional().describe("Host supports structured content.")}),yI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),openLinks:c.object({}).optional().describe("Host supports opening external URLs."),serverTools:c.object({listChanged:c.boolean().optional().describe("Host supports tools/list_changed notifications.")}).optional().describe("Host can proxy tool calls to the MCP server."),serverResources:c.object({listChanged:c.boolean().optional().describe("Host supports resources/list_changed notifications.")}).optional().describe("Host can proxy resource reads to the MCP server."),logging:c.object({}).optional().describe("Host accepts log messages."),sandbox:c.object({permissions:po.optional().describe("Permissions granted by the host (camera, microphone, geolocation)."),csp:fo.optional().describe("CSP domains approved by the host.")}).optional().describe("Sandbox configuration applied by the host."),updateModelContext:ts.optional().describe("Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns."),message:ts.optional().describe("Host supports receiving content messages (ui/message) from the view.")}),kI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),tools:c.object({listChanged:c.boolean().optional().describe("App supports tools/list_changed notifications.")}).optional().describe("App exposes MCP-style tools that the host can call."),availableDisplayModes:c.array(Ut).optional().describe("Display modes the app supports.")});c.object({method:c.literal("ui/notifications/initialized"),params:c.object({}).optional()});c.object({csp:fo.optional().describe("Content Security Policy configuration."),permissions:po.optional().describe("Sandbox permissions requested by the UI."),domain:c.string().optional().describe("Dedicated origin for view sandbox."),prefersBorder:c.boolean().optional().describe("Visual boundary preference - true if UI prefers a visible border.")});c.object({method:c.literal("ui/request-display-mode"),params:c.object({mode:Ut.describe("The display mode being requested.")})});var II=c.object({mode:Ut.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),wI=c.union([c.literal("model"),c.literal("app")]).describe("Tool visibility scope - who can access the tool.");c.object({resourceUri:c.string().optional(),visibility:c.array(wI).optional().describe(`Who can access this tool. Default: ["model", "app"]
91
91
  - "model": Tool visible to and callable by the agent
92
92
  - "app": Tool callable by the app from this server only`)});c.object({mimeTypes:c.array(c.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});c.object({method:c.literal("ui/message"),params:c.object({role:c.literal("user").describe('Message role, currently only "user" is supported.'),content:c.array(Ct).describe("Message content blocks (text, image, etc.).")})});c.object({method:c.literal("ui/notifications/sandbox-resource-ready"),params:c.object({html:c.string().describe("HTML content to load into the inner iframe."),sandbox:c.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:fo.optional().describe("CSP configuration from resource metadata."),permissions:po.optional().describe("Sandbox permissions from resource metadata.")})});var SI=c.object({method:c.literal("ui/notifications/tool-result"),params:Li.describe("Standard MCP tool execution result.")}),Ef=c.object({toolInfo:c.object({id:Pt.optional().describe("JSON-RPC id of the tools/call request."),tool:hr.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:cI.optional().describe("Current color theme preference."),styles:_I.optional().describe("Style configuration for theming the app."),displayMode:Ut.optional().describe("How the UI is currently displayed."),availableDisplayModes:c.array(Ut).optional().describe("Display modes the host supports."),containerDimensions:c.union([c.object({height:c.number().describe("Fixed container height in pixels.")}),c.object({maxHeight:c.union([c.number(),c.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(c.union([c.object({width:c.number().describe("Fixed container width in pixels.")}),c.object({maxWidth:c.union([c.number(),c.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
93
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),xI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Ef.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ai.describe("App identification (name and version)."),appCapabilities:kI.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var zI=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ai.describe("Host application identification and version."),hostCapabilities:yI.describe("Features and capabilities provided by the host."),hostContext:Ef.describe("Rich context about the host environment.")}).passthrough();function ZI(e){let t=document.documentElement;t.setAttribute("data-theme",e),t.style.colorScheme=e}function UI(e,t=document.documentElement){for(let[n,r]of Object.entries(e))r!==void 0&&t.style.setProperty(n,r)}function OI(e){if(document.getElementById("__mcp-host-fonts"))return;let t=document.createElement("style");t.id="__mcp-host-fonts",t.textContent=e,document.head.appendChild(t)}class NI extends U_{constructor(n,r={},i={autoResize:!0}){super(i);se(this,"_appInfo");se(this,"_capabilities");se(this,"options");se(this,"_hostCapabilities");se(this,"_hostInfo");se(this,"_hostContext");se(this,"sendOpenLink",this.openLink);this._appInfo=n,this._capabilities=r,this.options=i,this.setRequestHandler(Ci,a=>(console.log("Received ping:",a.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(n){this.setNotificationHandler(vI,r=>n(r.params))}set ontoolinputpartial(n){this.setNotificationHandler(hI,r=>n(r.params))}set ontoolresult(n){this.setNotificationHandler(SI,r=>n(r.params))}set ontoolcancelled(n){this.setNotificationHandler(gI,r=>n(r.params))}set onhostcontextchanged(n){this.setNotificationHandler(xI,r=>{this._hostContext={...this._hostContext,...r.params},n(r.params)})}set onteardown(n){this.setRequestHandler(bI,(r,i)=>n(r.params,i))}set oncalltool(n){this.setRequestHandler(Ws,(r,i)=>n(r.params,i))}set onlisttools(n){this.setRequestHandler(qs,(r,i)=>n(r.params,i))}assertCapabilityForMethod(n){}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){return await this.request({method:"tools/call",params:n},Li,r)}sendMessage(n,r){return this.request({method:"ui/message",params:n},pI,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this.request({method:"ui/update-model-context",params:n},tr,r)}openLink(n,r){return this.request({method:"ui/open-link",params:n},fI,r)}requestDisplayMode(n,r){return this.request({method:"ui/request-display-mode",params:n},II,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,i=0,a=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let s=document.documentElement,u=s.style.width,l=s.style.height;s.style.width="fit-content",s.style.height="fit-content";let m=s.getBoundingClientRect();s.style.width=u,s.style.height=l;let d=window.innerWidth-s.clientWidth,p=Math.ceil(m.width+d),g=Math.ceil(m.height);(p!==r||g!==i)&&(r=p,i=g,this.sendSizeChanged({width:p,height:g}))}))};a();let o=new ResizeObserver(a);return o.observe(document.documentElement),o.observe(document.body),()=>o.disconnect()}async connect(n=new N_(window.parent,window.parent),r){var i;await super.connect(n);try{let a=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:T_}},zI,r);if(a===void 0)throw Error(`Server sent invalid initialize result: ${a}`);this._hostCapabilities=a.hostCapabilities,this._hostInfo=a.hostInfo,this._hostContext=a.hostContext,await this.notification({method:"ui/notifications/initialized"}),(i=this.options)!=null&&i.autoResize&&this.setupSizeChangedNotifications()}catch(a){throw this.close(),a}}}const je=document.querySelector(".main"),Ee=document.getElementById("screenshot-container"),$e=document.getElementById("screenshot"),_t=document.getElementById("overlays"),Me=document.getElementById("action-status"),qe=document.getElementById("session-info"),vo=document.getElementById("loading-overlay"),TI=vo.querySelector(".loading-text"),rt=document.createElement("div");rt.id="target-info";rt.className="target-info hidden";let _i=0,qn=0,We=null;function PI(e){return e.structuredContent??{}}function ho(e="Waiting for screenshot..."){TI.textContent=e,vo.classList.remove("hidden")}function yt(){vo.classList.add("hidden")}function Df(e){var n,r,i;e.theme&&ZI(e.theme),(n=e.styles)!=null&&n.variables&&UI(e.styles.variables),(i=(r=e.styles)==null?void 0:r.css)!=null&&i.fonts&&OI(e.styles.css.fonts),e.safeAreaInsets&&(je.style.paddingTop=`${e.safeAreaInsets.top}px`,je.style.paddingRight=`${e.safeAreaInsets.right}px`,je.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,je.style.paddingLeft=`${e.safeAreaInsets.left}px`);const t=e.containerDimensions;t&&("height"in t?(document.documentElement.style.height="100vh",je.style.height="100%"):"maxHeight"in t&&t.maxHeight&&(document.documentElement.style.maxHeight=`${t.maxHeight}px`,je.style.maxHeight="100%"),"width"in t?(document.documentElement.style.width="100vw",je.style.width="100%"):"maxWidth"in t&&t.maxWidth&&(document.documentElement.style.maxWidth=`${t.maxWidth}px`,je.style.maxWidth="100%"))}function is(e,t,n){return t===0?e:e/t*n}function jI(){Ee.style.transform="none",Ee.classList.remove("zoomed")}function Rf(e){requestAnimationFrame(()=>{var i,a;_t.innerHTML="";const t=$e.clientWidth,n=$e.clientHeight;if(console.info("addOverlays:",{action:e.action,hasElement:!!e.element,hasClickPosition:!!e.clickPosition,displayedWidth:t,displayedHeight:n,naturalWidth:_i,naturalHeight:qn}),t===0||n===0){console.warn("addOverlays: Dimensions not ready, retrying..."),setTimeout(()=>Rf(e),50);return}if((e.action==="find"||e.action==="find_and_click"||e.action==="findall")&&e.element){const o=document.createElement("div");o.className="element-target",o.style.left=`${t/2}px`,o.style.top=`${n/2}px`;const s=document.createElement("div");s.className="crosshair-h",o.appendChild(s);const u=document.createElement("div");u.className="crosshair-v",o.appendChild(u);const l=document.createElement("div");l.className="element-label",l.textContent=((i=e.element)==null?void 0:i.description)||"Element",(a=e.element)!=null&&a.confidence&&(l.textContent+=` (${Math.round(e.element.confidence*100)}%)`),o.appendChild(l),_t.appendChild(o),console.info("addOverlays: Added element target at center")}if(e.clickPosition){const o=e.clickPosition.centerX??e.clickPosition.x,s=e.clickPosition.centerY??e.clickPosition.y;if(o!==void 0&&s!==void 0&&_i>0){const u=document.createElement("div");u.className="click-marker";const l=is(o,_i,t),m=is(s,qn,n);u.style.left=`${l}px`,u.style.top=`${m}px`;const d=document.createElement("div");d.className="click-ripple",u.appendChild(d),_t.appendChild(u),console.info("addOverlays: Added click marker at",{clickX:o,clickY:s,scaledX:l,scaledY:m})}}if(e.scrollDirection){const o=document.createElement("div");o.className=`scroll-indicator scroll-${e.scrollDirection}`,o.textContent=e.scrollDirection==="up"?"↑":e.scrollDirection==="down"?"↓":e.scrollDirection==="left"?"←":"→",_t.appendChild(o)}jI(),delete Ee.dataset.focalX,delete Ee.dataset.focalY})}function EI(e){_t.innerHTML="";const t=e.action||"unknown",n=e.success?"✓":"✗",r=e.success?"success":"error";let i=`${n} ${t}`;if(e.duration&&(i+=` (${e.duration}ms)`),e.assertion&&(i+=`: "${e.assertion}"`),e.text&&e.action==="type"&&(i+=`: "${e.text}"`),e.error&&(i+=` - ${e.error}`),Me.textContent=i,Me.className=r,e.debuggerUrl&&(We=e.debuggerUrl),e.session){const a=e.session.expiresIn?Math.round(e.session.expiresIn/1e3):0;if(qe.innerHTML="",We){const o=document.createElement("a");o.href=We,o.target="_blank",o.rel="noopener noreferrer",o.textContent=`${a}s remaining`,o.className="debugger-link",o.title=`Open debugger: ${We}`,qe.appendChild(o)}else qe.textContent=`${a}s remaining`;qe.className=a<30?"warning":""}else if(We){qe.innerHTML="";const a=document.createElement("a");a.href=We,a.target="_blank",a.rel="noopener noreferrer",a.textContent="Open Debugger",a.className="debugger-link",a.title=We,qe.appendChild(a)}else e.action==="session_start"&&(qe.textContent="Session started");if(e.element&&(e.action==="find"||e.action==="find_and_click")){const a=e.element;let o=`<strong>Target:</strong> "${a.description||"Element"}"`;if(a.centerX!==void 0&&a.centerY!==void 0&&(o+=` <span class="target-coords">(${Math.round(a.centerX)}, ${Math.round(a.centerY)})</span>`),a.confidence!==void 0){const s=Math.round(a.confidence*100);o+=` <span class="target-confidence ${s>=70?"high":s>=40?"medium":"low"}">${s}%</span>`}a.ref&&(o+=` <span class="target-ref">ref: ${a.ref}</span>`),rt.innerHTML=o,rt.classList.remove("hidden")}else rt.classList.add("hidden");e.imageUrl?(ho("Loading image..."),$e.onerror=()=>{console.error("Image failed to load"),$e.alt="Image failed to load",Ee.style.display="none",yt()},$e.onload=()=>{console.info("Image loaded:",$e.naturalWidth,"x",$e.naturalHeight),_i=$e.naturalWidth,qn=$e.naturalHeight,Ee.style.display="block",Rf(e),yt()},$e.src=e.imageUrl,$e.style.display="block"):($e.style.display="none",Ee.style.display="none",yt())}const Oe=new NI({name:"TestDriver Screenshot",version:"1.0.0"});async function DI(e){try{console.info("Fetching screenshot from resource:",e);const n=(await Oe.request({method:"resources/read",params:{uri:e}},Js)).contents[0];if(!n||!("blob"in n))return console.error("Resource did not contain blob data"),null;const r=`data:${n.mimeType||"image/png"};base64,${n.blob}`;return console.info("Screenshot fetched successfully, blob length:",n.blob.length),r}catch(t){return console.error("Failed to fetch screenshot resource:",t),null}}Oe.onteardown=async()=>(console.info("TestDriver app being torn down"),{});Oe.ontoolinput=e=>{console.info("Received tool input:",e),Me.textContent="Running action...",Me.className="loading",Ee.style.display="none",ho("Running action...")};Oe.ontoolresult=async e=>{console.info("Received tool result:",e),console.info("structuredContent:",e.structuredContent);const t=PI(e);console.info("Extracted data keys:",Object.keys(t)),console.info("Has imageUrl:",!!t.imageUrl),console.info("Has screenshotResourceUri:",!!t.screenshotResourceUri),console.info("Has croppedImageResourceUri:",!!t.croppedImageResourceUri);const n=t.screenshotResourceUri||t.croppedImageResourceUri;if(n&&!t.imageUrl){ho("Fetching image...");const r=await DI(n);r&&(t.imageUrl=r)}EI(t)};Oe.ontoolcancelled=e=>{console.info("Tool cancelled:",e.reason),Me.textContent=`Cancelled: ${e.reason}`,Me.className="error",yt()};Oe.onerror=e=>{console.error("App error:",e),Me.textContent=`Error: ${e}`,Me.className="error",yt()};Oe.onhostcontextchanged=Df;Oe.connect().then(()=>{const e=Oe.getHostContext();e&&Df(e)});const $i=document.querySelector(".screenshot-wrapper");$i&&$i.parentNode&&$i.parentNode.insertBefore(rt,$i.nextSibling);</script>
94
- <style rel="stylesheet" crossorigin>*{box-sizing:border-box;margin:0;padding:0}html,body{font-family:var(--font-sans, system-ui, -apple-system, sans-serif);font-size:var(--font-text-sm-size, 14px);line-height:var(--font-text-sm-line-height, 1.5);background:var(--color-background-primary, #fff);color:var(--color-text-primary, #1a1a1a);overflow:hidden;margin:0;padding:0}body{height:100%;display:flex;flex-direction:column}.main{width:100%;max-width:100%;display:flex;flex-direction:column;overflow:hidden}.screenshot-wrapper{position:relative;width:100%;flex:1;min-height:0;background:var(--color-background-secondary, #f5f5f5);border-radius:var(--border-radius-md, 8px);overflow:hidden;display:flex;flex-direction:column}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px;flex:1;min-height:100px;background:var(--color-background-secondary, #f5f5f5)}.loading-overlay.hidden{display:none}.loading-spinner{width:32px;height:32px;border:3px solid var(--color-border-primary, #e5e5e5);border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:var(--font-text-sm-size, 14px);color:var(--color-text-secondary, #666)}.screenshot-container{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center;justify-content:center}#screenshot{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}#overlays{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.element-target{position:absolute;width:24px;height:24px;margin-left:-12px;margin-top:-12px;background:#3b82f64d;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d;animation:target-pulse 1.5s ease-in-out infinite}@keyframes target-pulse{0%,to{transform:scale(1);border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d}50%{transform:scale(1.1);border-color:#60a5fa;box-shadow:0 0 0 2px #fff,0 0 12px #3b82f680}}.crosshair-h,.crosshair-v{position:absolute;background:#3b82f6}.crosshair-h{width:40px;height:2px;left:50%;top:50%;margin-left:-20px;margin-top:-1px}.crosshair-v{width:2px;height:40px;left:50%;top:50%;margin-left:-1px;margin-top:-20px}.element-label{position:absolute;top:100%;left:50%;transform:translate(-50%);margin-top:24px;background:#3b82f6;color:#fff;font-size:11px;padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px #0003}.click-marker{position:absolute;width:16px;height:16px;margin-left:-8px;margin-top:-8px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 8px #0000004d}.click-ripple{position:absolute;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px;border:2px solid #ef4444;border-radius:50%;animation:ripple 1s ease-out infinite}@keyframes ripple{0%{transform:scale(.5);opacity:1}to{transform:scale(2);opacity:0}}.scroll-indicator{position:absolute;font-size:48px;color:#3b82f6;text-shadow:0 2px 8px rgba(0,0,0,.3);animation:scroll-bounce .5s ease-in-out}.scroll-up{top:20px;left:50%;transform:translate(-50%)}.scroll-down{bottom:20px;left:50%;transform:translate(-50%)}.scroll-left{left:20px;top:50%;transform:translateY(-50%)}.scroll-right{right:20px;top:50%;transform:translateY(-50%)}@keyframes scroll-bounce{0%,to{opacity:1}50%{opacity:.5}}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-top:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-xs-size, 12px)}#action-status{font-weight:var(--font-weight-medium, 500)}#action-status.success{color:#10b981}#action-status.error{color:#ef4444}#action-status.loading,#session-info{color:var(--color-text-secondary, #666)}#session-info.warning{color:#f59e0b}.debugger-link{color:#3b82f6;text-decoration:none;transition:color .15s ease}.debugger-link:hover{color:#2563eb;text-decoration:underline}.target-info{padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-bottom:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-sm-size, 14px);color:var(--color-text-primary, #1a1a1a)}.target-info.hidden{display:none}.target-info strong{color:#3b82f6;font-weight:var(--font-weight-medium, 500)}.target-coords{color:var(--color-text-secondary, #666);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 12px);margin-left:4px}.target-confidence{display:inline-block;padding:1px 6px;border-radius:10px;font-size:var(--font-text-xs-size, 11px);font-weight:var(--font-weight-medium, 500);margin-left:6px}.target-confidence.high{background:#dcfce7;color:#166534}.target-confidence.medium{background:#fef3c7;color:#92400e}.target-confidence.low{background:#fee2e2;color:#991b1b}.target-ref{color:var(--color-text-tertiary, #999);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 11px);margin-left:8px}@media(prefers-color-scheme:dark){html:not([data-theme=light]) .element-target{border-color:#60a5fa;background:#60a5fa33;box-shadow:0 0 0 2px #1f2937,0 2px 8px #00000080}html:not([data-theme=light]) .crosshair-h,html:not([data-theme=light]) .crosshair-v{background:#60a5fa}html:not([data-theme=light]) .element-label{background:#2563eb}html:not([data-theme=light]) .click-marker{background:#f87171;border-color:#1f2937}html:not([data-theme=light]) .click-ripple{border-color:#f87171}html:not([data-theme=light]) .scroll-indicator,html:not([data-theme=light]) .debugger-link{color:#60a5fa}html:not([data-theme=light]) .debugger-link:hover{color:#93c5fd}html:not([data-theme=light]) .target-info strong{color:#60a5fa}html:not([data-theme=light]) .target-confidence.high{background:#166534;color:#dcfce7}html:not([data-theme=light]) .target-confidence.medium{background:#92400e;color:#fef3c7}html:not([data-theme=light]) .target-confidence.low{background:#991b1b;color:#fee2e2}}</style>
93
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),xI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Ef.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ai.describe("App identification (name and version)."),appCapabilities:kI.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var zI=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ai.describe("Host application identification and version."),hostCapabilities:yI.describe("Features and capabilities provided by the host."),hostContext:Ef.describe("Rich context about the host environment.")}).passthrough();function ZI(e){let t=document.documentElement;t.setAttribute("data-theme",e),t.style.colorScheme=e}function UI(e,t=document.documentElement){for(let[n,r]of Object.entries(e))r!==void 0&&t.style.setProperty(n,r)}function OI(e){if(document.getElementById("__mcp-host-fonts"))return;let t=document.createElement("style");t.id="__mcp-host-fonts",t.textContent=e,document.head.appendChild(t)}class NI extends U_{constructor(n,r={},i={autoResize:!0}){super(i);se(this,"_appInfo");se(this,"_capabilities");se(this,"options");se(this,"_hostCapabilities");se(this,"_hostInfo");se(this,"_hostContext");se(this,"sendOpenLink",this.openLink);this._appInfo=n,this._capabilities=r,this.options=i,this.setRequestHandler(Ci,a=>(console.log("Received ping:",a.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(n){this.setNotificationHandler(vI,r=>n(r.params))}set ontoolinputpartial(n){this.setNotificationHandler(hI,r=>n(r.params))}set ontoolresult(n){this.setNotificationHandler(SI,r=>n(r.params))}set ontoolcancelled(n){this.setNotificationHandler(gI,r=>n(r.params))}set onhostcontextchanged(n){this.setNotificationHandler(xI,r=>{this._hostContext={...this._hostContext,...r.params},n(r.params)})}set onteardown(n){this.setRequestHandler(bI,(r,i)=>n(r.params,i))}set oncalltool(n){this.setRequestHandler(Ws,(r,i)=>n(r.params,i))}set onlisttools(n){this.setRequestHandler(qs,(r,i)=>n(r.params,i))}assertCapabilityForMethod(n){}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){return await this.request({method:"tools/call",params:n},Li,r)}sendMessage(n,r){return this.request({method:"ui/message",params:n},pI,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this.request({method:"ui/update-model-context",params:n},tr,r)}openLink(n,r){return this.request({method:"ui/open-link",params:n},fI,r)}requestDisplayMode(n,r){return this.request({method:"ui/request-display-mode",params:n},II,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,i=0,a=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let s=document.documentElement,u=s.style.width,l=s.style.height;s.style.width="fit-content",s.style.height="fit-content";let m=s.getBoundingClientRect();s.style.width=u,s.style.height=l;let d=window.innerWidth-s.clientWidth,p=Math.ceil(m.width+d),g=Math.ceil(m.height);(p!==r||g!==i)&&(r=p,i=g,this.sendSizeChanged({width:p,height:g}))}))};a();let o=new ResizeObserver(a);return o.observe(document.documentElement),o.observe(document.body),()=>o.disconnect()}async connect(n=new N_(window.parent,window.parent),r){var i;await super.connect(n);try{let a=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:T_}},zI,r);if(a===void 0)throw Error(`Server sent invalid initialize result: ${a}`);this._hostCapabilities=a.hostCapabilities,this._hostInfo=a.hostInfo,this._hostContext=a.hostContext,await this.notification({method:"ui/notifications/initialized"}),(i=this.options)!=null&&i.autoResize&&this.setupSizeChangedNotifications()}catch(a){throw this.close(),a}}}const je=document.querySelector(".main"),Ee=document.getElementById("screenshot-container"),$e=document.getElementById("screenshot"),_t=document.getElementById("overlays"),Me=document.getElementById("action-status"),qe=document.getElementById("session-info"),vo=document.getElementById("loading-overlay"),TI=vo.querySelector(".loading-text"),rt=document.createElement("div");rt.id="target-info";rt.className="target-info hidden";let _i=0,qn=0,We=null;function PI(e){return e.structuredContent??{}}function ho(e="Waiting for screenshot..."){TI.textContent=e,vo.classList.remove("hidden")}function yt(){vo.classList.add("hidden")}function Df(e){var n,r,i;e.theme&&ZI(e.theme),(n=e.styles)!=null&&n.variables&&UI(e.styles.variables),(i=(r=e.styles)==null?void 0:r.css)!=null&&i.fonts&&OI(e.styles.css.fonts),e.safeAreaInsets&&(je.style.paddingTop=`${e.safeAreaInsets.top}px`,je.style.paddingRight=`${e.safeAreaInsets.right}px`,je.style.paddingBottom=`${e.safeAreaInsets.bottom}px`,je.style.paddingLeft=`${e.safeAreaInsets.left}px`);const t=e.containerDimensions;t&&("height"in t?(document.documentElement.style.height="100vh",je.style.height="100%"):"maxHeight"in t&&t.maxHeight&&(document.documentElement.style.maxHeight=`${t.maxHeight}px`,je.style.maxHeight="100%"),"width"in t?(document.documentElement.style.width="100vw",je.style.width="100%"):"maxWidth"in t&&t.maxWidth&&(document.documentElement.style.maxWidth=`${t.maxWidth}px`,je.style.maxWidth="100%"))}function is(e,t,n){return t===0?e:e/t*n}function jI(){Ee.style.transform="none",Ee.classList.remove("zoomed")}function Rf(e){requestAnimationFrame(()=>{var i,a;_t.innerHTML="";const t=$e.clientWidth,n=$e.clientHeight;if(console.info("addOverlays:",{action:e.action,hasElement:!!e.element,hasClickPosition:!!e.clickPosition,displayedWidth:t,displayedHeight:n,naturalWidth:_i,naturalHeight:qn}),t===0||n===0){console.warn("addOverlays: Dimensions not ready, retrying..."),setTimeout(()=>Rf(e),50);return}if((e.action==="find"||e.action==="find_and_click"||e.action==="findall")&&e.element){const o=document.createElement("div");o.className="element-target",o.style.left=`${t/2}px`,o.style.top=`${n/2}px`;const s=document.createElement("div");s.className="crosshair-h",o.appendChild(s);const u=document.createElement("div");u.className="crosshair-v",o.appendChild(u);const l=document.createElement("div");l.className="element-label",l.textContent=((i=e.element)==null?void 0:i.description)||"Element",(a=e.element)!=null&&a.confidence&&(l.textContent+=` (${Math.round(e.element.confidence*100)}%)`),o.appendChild(l),_t.appendChild(o),console.info("addOverlays: Added element target at center")}if(e.clickPosition){const o=e.clickPosition.centerX??e.clickPosition.x,s=e.clickPosition.centerY??e.clickPosition.y;if(o!==void 0&&s!==void 0&&_i>0){const u=document.createElement("div");u.className="click-marker";const l=is(o,_i,t),m=is(s,qn,n);u.style.left=`${l}px`,u.style.top=`${m}px`;const d=document.createElement("div");d.className="click-ripple",u.appendChild(d),_t.appendChild(u),console.info("addOverlays: Added click marker at",{clickX:o,clickY:s,scaledX:l,scaledY:m})}}if(e.scrollDirection){const o=document.createElement("div");o.className=`scroll-indicator scroll-${e.scrollDirection}`,o.textContent=e.scrollDirection==="up"?"↑":e.scrollDirection==="down"?"↓":e.scrollDirection==="left"?"←":"→",_t.appendChild(o)}jI(),delete Ee.dataset.focalX,delete Ee.dataset.focalY})}function EI(e){_t.innerHTML="";const t=e.action||"unknown",n=e.success?"✓":"✗",r=e.success?"success":"error";let i=`${n} ${t}`;if(e.duration&&(i+=` (${e.duration}ms)`),e.assertion&&(i+=`: "${e.assertion}"`),e.text&&e.action==="type"&&(i+=`: "${e.text}"`),e.error&&(i+=` - ${e.error}`),Me.textContent=i,Me.className=r,e.debuggerUrl&&(We=e.debuggerUrl),e.session){const a=e.session.expiresIn?Math.round(e.session.expiresIn/1e3):0;if(qe.innerHTML="",We){const o=document.createElement("a");o.href=We,o.target="_blank",o.rel="noopener noreferrer",o.textContent=`${a}s remaining`,o.className="debugger-link",o.title=`Open debugger: ${We}`,qe.appendChild(o)}else qe.textContent=`${a}s remaining`;qe.className=a<30?"warning":""}else if(We){qe.innerHTML="";const a=document.createElement("a");a.href=We,a.target="_blank",a.rel="noopener noreferrer",a.textContent="Open Debugger",a.className="debugger-link",a.title=We,qe.appendChild(a)}else e.action==="session_start"&&(qe.textContent="Session started");if(e.element&&(e.action==="find"||e.action==="find_and_click")){const a=e.element;let o=`<strong>Target:</strong> "${a.description||"Element"}"`;if(a.centerX!==void 0&&a.centerY!==void 0&&(o+=` <span class="target-coords">(${Math.round(a.centerX)}, ${Math.round(a.centerY)})</span>`),a.confidence!==void 0){const s=Math.round(a.confidence*100);o+=` <span class="target-confidence ${s>=70?"high":s>=40?"medium":"low"}">${s}%</span>`}rt.innerHTML=o,rt.classList.remove("hidden")}else rt.classList.add("hidden");e.imageUrl?(ho("Loading image..."),$e.onerror=()=>{console.error("Image failed to load"),$e.alt="Image failed to load",Ee.style.display="none",yt()},$e.onload=()=>{console.info("Image loaded:",$e.naturalWidth,"x",$e.naturalHeight),_i=$e.naturalWidth,qn=$e.naturalHeight,Ee.style.display="block",Rf(e),yt()},$e.src=e.imageUrl,$e.style.display="block"):($e.style.display="none",Ee.style.display="none",yt())}const Oe=new NI({name:"TestDriver Screenshot",version:"1.0.0"});async function DI(e){try{console.info("Fetching screenshot from resource:",e);const n=(await Oe.request({method:"resources/read",params:{uri:e}},Js)).contents[0];if(!n||!("blob"in n))return console.error("Resource did not contain blob data"),null;const r=`data:${n.mimeType||"image/png"};base64,${n.blob}`;return console.info("Screenshot fetched successfully, blob length:",n.blob.length),r}catch(t){return console.error("Failed to fetch screenshot resource:",t),null}}Oe.onteardown=async()=>(console.info("TestDriver app being torn down"),{});Oe.ontoolinput=e=>{console.info("Received tool input:",e);const t=e.arguments,n=[];t&&(t.description&&n.push(`"${t.description}"`),t.text&&n.push(`"${t.text}"`),t.url&&n.push(`${t.url}`),t.direction&&n.push(`${t.direction}`),t.assertion&&n.push(`"${t.assertion}"`),t.task&&n.push(`"${t.task}"`),t.keys&&n.push(`[${t.keys.join("+")}]`),t.type&&n.push(`${t.type}`));const r=n.length>0?n.join(" "):"action";Me.textContent=`Running ${r}...`,Me.className="loading",Ee.style.display="none",ho(`Running ${r}...`)};Oe.ontoolresult=async e=>{console.info("Received tool result:",e),console.info("structuredContent:",e.structuredContent);const t=PI(e);console.info("Extracted data keys:",Object.keys(t)),console.info("Has imageUrl:",!!t.imageUrl),console.info("Has screenshotResourceUri:",!!t.screenshotResourceUri),console.info("Has croppedImageResourceUri:",!!t.croppedImageResourceUri);const n=t.screenshotResourceUri||t.croppedImageResourceUri;if(n&&!t.imageUrl){ho("Fetching image...");const r=await DI(n);r&&(t.imageUrl=r)}EI(t)};Oe.ontoolcancelled=e=>{console.info("Tool cancelled:",e.reason),Me.textContent=`Cancelled: ${e.reason}`,Me.className="error",yt()};Oe.onerror=e=>{console.error("App error:",e),Me.textContent=`Error: ${e}`,Me.className="error",yt()};Oe.onhostcontextchanged=Df;Oe.connect().then(()=>{const e=Oe.getHostContext();e&&Df(e)});const $i=document.querySelector(".screenshot-wrapper");$i&&$i.parentNode&&$i.parentNode.insertBefore(rt,$i.nextSibling);</script>
94
+ <style rel="stylesheet" crossorigin>*{box-sizing:border-box;margin:0;padding:0}html,body{font-family:var(--font-sans, system-ui, -apple-system, sans-serif);font-size:var(--font-text-sm-size, 14px);line-height:var(--font-text-sm-line-height, 1.5);background:var(--color-background-primary, #fff);color:var(--color-text-primary, #1a1a1a);overflow:hidden;margin:0;padding:0}body{height:100%;display:flex;flex-direction:column}.main{width:100%;max-width:100%;display:flex;flex-direction:column;overflow:hidden}.screenshot-wrapper{position:relative;width:100%;flex:1;min-height:0;background:var(--color-background-secondary, #f5f5f5);border-radius:var(--border-radius-md, 8px);overflow:hidden;display:flex;flex-direction:column}.loading-overlay{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px;flex:1;min-height:100px;background:var(--color-background-secondary, #f5f5f5)}.loading-overlay.hidden{display:none}.loading-spinner{width:32px;height:32px;border:3px solid var(--color-border-primary, #e5e5e5);border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:var(--font-text-sm-size, 14px);color:var(--color-text-secondary, #666)}.screenshot-container{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center;justify-content:center}#screenshot{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}#overlays{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.element-target{position:absolute;width:24px;height:24px;margin-left:-12px;margin-top:-12px;background:#3b82f64d;border:3px solid #3b82f6;border-radius:50%;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d;animation:target-pulse 1.5s ease-in-out infinite}@keyframes target-pulse{0%,to{transform:scale(1);border-color:#3b82f6;box-shadow:0 0 0 2px #fff,0 2px 8px #0000004d}50%{transform:scale(1.1);border-color:#60a5fa;box-shadow:0 0 0 2px #fff,0 0 12px #3b82f680}}.crosshair-h,.crosshair-v{position:absolute;background:#3b82f6}.crosshair-h{width:40px;height:2px;left:50%;top:50%;margin-left:-20px;margin-top:-1px}.crosshair-v{width:2px;height:40px;left:50%;top:50%;margin-left:-1px;margin-top:-20px}.element-label{position:absolute;top:100%;left:50%;transform:translate(-50%);margin-top:24px;background:#3b82f6;color:#fff;font-size:11px;padding:4px 8px;border-radius:4px;white-space:nowrap;max-width:300px;overflow:hidden;text-overflow:ellipsis;box-shadow:0 2px 8px #0003}.click-marker{position:absolute;width:16px;height:16px;margin-left:-8px;margin-top:-8px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 8px #0000004d}.click-ripple{position:absolute;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px;border:2px solid #ef4444;border-radius:50%;animation:ripple 1s ease-out infinite}@keyframes ripple{0%{transform:scale(.5);opacity:1}to{transform:scale(2);opacity:0}}.scroll-indicator{position:absolute;font-size:48px;color:#3b82f6;text-shadow:0 2px 8px rgba(0,0,0,.3);animation:scroll-bounce .5s ease-in-out}.scroll-up{top:20px;left:50%;transform:translate(-50%)}.scroll-down{bottom:20px;left:50%;transform:translate(-50%)}.scroll-left{left:20px;top:50%;transform:translateY(-50%)}.scroll-right{right:20px;top:50%;transform:translateY(-50%)}@keyframes scroll-bounce{0%,to{opacity:1}50%{opacity:.5}}.status-bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-top:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-xs-size, 12px)}#action-status{font-weight:var(--font-weight-medium, 500)}#action-status.success{color:#10b981}#action-status.error{color:#ef4444}#action-status.loading,#session-info{color:var(--color-text-secondary, #666)}#session-info.warning{color:#f59e0b}.debugger-link{color:#3b82f6;text-decoration:none;transition:color .15s ease}.debugger-link:hover{color:#2563eb;text-decoration:underline}.target-info{padding:8px 12px;background:var(--color-background-secondary, #f5f5f5);border-bottom:1px solid var(--color-border-primary, #e5e5e5);font-size:var(--font-text-sm-size, 14px);color:var(--color-text-primary, #1a1a1a)}.target-info.hidden{display:none}.target-info strong{color:#3b82f6;font-weight:var(--font-weight-medium, 500)}.target-coords{color:var(--color-text-secondary, #666);font-family:var(--font-mono, ui-monospace, monospace);font-size:var(--font-text-xs-size, 12px);margin-left:4px}.target-confidence{display:inline-block;padding:1px 6px;border-radius:10px;font-size:var(--font-text-xs-size, 11px);font-weight:var(--font-weight-medium, 500);margin-left:6px}.target-confidence.high{background:#dcfce7;color:#166534}.target-confidence.medium{background:#fef3c7;color:#92400e}.target-confidence.low{background:#fee2e2;color:#991b1b}@media(prefers-color-scheme:dark){html:not([data-theme=light]) .element-target{border-color:#60a5fa;background:#60a5fa33;box-shadow:0 0 0 2px #1f2937,0 2px 8px #00000080}html:not([data-theme=light]) .crosshair-h,html:not([data-theme=light]) .crosshair-v{background:#60a5fa}html:not([data-theme=light]) .element-label{background:#2563eb}html:not([data-theme=light]) .click-marker{background:#f87171;border-color:#1f2937}html:not([data-theme=light]) .click-ripple{border-color:#f87171}html:not([data-theme=light]) .scroll-indicator,html:not([data-theme=light]) .debugger-link{color:#60a5fa}html:not([data-theme=light]) .debugger-link:hover{color:#93c5fd}html:not([data-theme=light]) .target-info strong{color:#60a5fa}html:not([data-theme=light]) .target-confidence.high{background:#166534;color:#dcfce7}html:not([data-theme=light]) .target-confidence.medium{background:#92400e;color:#fef3c7}html:not([data-theme=light]) .target-confidence.low{background:#991b1b;color:#fee2e2}}</style>
95
95
  </head>
96
96
  <body>
97
97
  <main class="main">
@@ -150,7 +150,7 @@ export declare const SessionStartInputSchema: z.ZodObject<{
150
150
  os: z.ZodDefault<z.ZodEnum<["linux", "windows"]>>;
151
151
  /** Keep sandbox alive duration in ms (default: 5 minutes) */
152
152
  keepAlive: z.ZodDefault<z.ZodNumber>;
153
- /** Path to test file being built */
153
+ /** Path to test file - when provided, you MUST append generated code to this file after each action */
154
154
  testFile: z.ZodOptional<z.ZodString>;
155
155
  /** Reconnect to last sandbox */
156
156
  reconnect: z.ZodDefault<z.ZodBoolean>;
@@ -121,8 +121,8 @@ export const SessionStartInputSchema = z.object({
121
121
  os: z.enum(["linux", "windows"]).default("linux").describe("Sandbox OS"),
122
122
  /** Keep sandbox alive duration in ms (default: 5 minutes) */
123
123
  keepAlive: z.number().default(300000).describe("Keep sandbox alive for this many ms"),
124
- /** Path to test file being built */
125
- testFile: z.string().optional().describe("Path to test file being built"),
124
+ /** Path to test file - when provided, you MUST append generated code to this file after each action */
125
+ testFile: z.string().optional().describe("Path to test file. When provided, append generated code from each action to this file immediately."),
126
126
  /** Reconnect to last sandbox */
127
127
  reconnect: z.boolean().default(false).describe("Reconnect to last sandbox"),
128
128
  /** API endpoint URL */
@@ -226,14 +226,22 @@ function requireActiveSession() {
226
226
  * Images: imageUrl (data URL) goes to structuredContent for UI to display
227
227
  * The croppedImage from find() is small (~10KB) so it's acceptable as data URL
228
228
  *
229
- * If generatedCode is provided, it's appended to the text response so the agent
230
- * can add it to their test file.
229
+ * If generatedCode is provided, it's appended to the text response with instructions
230
+ * for the agent to write it to the test file.
231
231
  */
232
232
  function createToolResult(success, textContent, structuredData, generatedCode) {
233
- // Build text content - append generated code if provided
233
+ // Build text content - append generated code if provided with directive instructions
234
234
  let fullText = textContent;
235
235
  if (generatedCode && success) {
236
- fullText += `\n\nAdd to test file:\n${generatedCode}`;
236
+ // Get the test file from the current session
237
+ const session = sessionManager.getCurrentSession();
238
+ const testFile = session?.testFile;
239
+ if (testFile) {
240
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to ${testFile}:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
241
+ }
242
+ else {
243
+ fullText += `\n\n⚠️ ACTION REQUIRED: Append this code to the test file:\n\`\`\`javascript\n${generatedCode}\n\`\`\``;
244
+ }
237
245
  }
238
246
  const content = [{ type: "text", text: fullText }];
239
247
  logger.debug("createToolResult", {
@@ -244,9 +252,17 @@ function createToolResult(success, textContent, structuredData, generatedCode) {
244
252
  hasGeneratedCode: !!generatedCode
245
253
  });
246
254
  // structuredContent goes to UI (includes imageUrl for display)
255
+ // Always include success flag so UI can display correct status indicator
256
+ // Include generatedCode and testFile in structured data so agents can programmatically handle it
257
+ const session = sessionManager.getCurrentSession();
247
258
  return {
248
259
  content,
249
- structuredContent: structuredData,
260
+ structuredContent: {
261
+ ...structuredData,
262
+ success,
263
+ generatedCode: generatedCode && success ? generatedCode : undefined,
264
+ testFile: session?.testFile || undefined,
265
+ },
250
266
  };
251
267
  }
252
268
  // Create MCP server
@@ -322,6 +338,10 @@ registerAppTool(server, "session_start", {
322
338
  title: "Session Start",
323
339
  description: `Start a new TestDriver session and provision a sandbox with browser or app.
324
340
 
341
+ ⚠️ IMPORTANT - Test File Parameter:
342
+ When 'testFile' is provided, you MUST append the generated code to that file after EVERY successful action.
343
+ Each tool response includes "ACTION REQUIRED: Append this code..." - you must write that code to the test file immediately.
344
+
325
345
  Provision types:
326
346
  - chrome: Launch Chrome browser (default). Use 'url' for starting page.
327
347
  - chromeExtension: Launch Chrome with an extension. Use 'extensionPath' or 'extensionId'.
@@ -1369,50 +1389,6 @@ Only use 'screenshot' when you explicitly want to show something to the human us
1369
1389
  return createToolResult(false, `Screenshot failed: ${error}`, { error: String(error) });
1370
1390
  }
1371
1391
  });
1372
- // Verify
1373
- server.registerTool("verify", {
1374
- description: "Run the test file from scratch to verify it works",
1375
- inputSchema: z.object({
1376
- testFile: z.string().describe("Path to test file to run"),
1377
- }),
1378
- }, async (params) => {
1379
- const startTime = Date.now();
1380
- logger.info("verify: Starting", { testFile: params.testFile });
1381
- const session = sessionManager.getCurrentSession();
1382
- if (!fs.existsSync(params.testFile)) {
1383
- logger.warn("verify: Test file not found", { testFile: params.testFile });
1384
- return createToolResult(false, `Test file not found: ${params.testFile}`, { error: "Test file not found" });
1385
- }
1386
- const { execSync } = await import("child_process");
1387
- try {
1388
- logger.info("verify: Running vitest", { testFile: params.testFile });
1389
- const output = execSync(`npx vitest run "${params.testFile}" --reporter=verbose`, {
1390
- encoding: "utf-8",
1391
- timeout: 300000,
1392
- cwd: process.cwd(),
1393
- env: { ...process.env },
1394
- });
1395
- const duration = Date.now() - startTime;
1396
- logger.info("verify: Test passed", { testFile: params.testFile, duration });
1397
- return createToolResult(true, `✓ Test passed!\n\n${output}`, {
1398
- action: "verify",
1399
- success: true,
1400
- session: getSessionData(session),
1401
- duration,
1402
- });
1403
- }
1404
- catch (error) {
1405
- const duration = Date.now() - startTime;
1406
- logger.error("verify: Test failed", { testFile: params.testFile, error: error.message, duration });
1407
- return createToolResult(false, `✗ Test failed!\n\n${error.stdout || error.message}`, {
1408
- action: "verify",
1409
- success: false,
1410
- error: error.stdout || error.message,
1411
- session: getSessionData(session),
1412
- duration,
1413
- });
1414
- }
1415
- });
1416
1392
  // Start the server
1417
1393
  async function main() {
1418
1394
  logger.info("Starting TestDriver MCP Server", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.2.73",
3
+ "version": "7.2.75",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",
package/sdk.d.ts CHANGED
@@ -867,7 +867,22 @@ export interface DashcamAPI {
867
867
  }
868
868
 
869
869
  export default class TestDriverSDK {
870
- constructor(apiKey: string, options?: TestDriverOptions);
870
+ /**
871
+ * Create a new TestDriverSDK instance
872
+ * Automatically loads environment variables from .env file via dotenv.
873
+ *
874
+ * @param apiKey - API key (optional, defaults to TD_API_KEY environment variable)
875
+ * @param options - SDK configuration options
876
+ *
877
+ * @example
878
+ * // API key loaded automatically from TD_API_KEY in .env
879
+ * const client = new TestDriver();
880
+ *
881
+ * @example
882
+ * // Or pass API key explicitly
883
+ * const client = new TestDriver('your-api-key');
884
+ */
885
+ constructor(apiKey?: string, options?: TestDriverOptions);
871
886
 
872
887
  /**
873
888
  * Whether the SDK is currently connected to a sandbox
package/sdk.js CHANGED
@@ -5,6 +5,9 @@ const crypto = require("crypto");
5
5
  const { formatter } = require("./sdk-log-formatter");
6
6
  const logger = require("./agent/lib/logger");
7
7
 
8
+ // Load .env file into process.env by default
9
+ require("dotenv").config();
10
+
8
11
  /**
9
12
  * Get the file path of the caller (the file that called TestDriver)
10
13
  * @returns {string|null} File path or null if not found
@@ -1233,13 +1236,18 @@ function createChainablePromise(promise) {
1233
1236
  * TestDriver SDK
1234
1237
  *
1235
1238
  * This SDK provides programmatic access to TestDriver's AI-powered testing capabilities.
1239
+ * Automatically loads environment variables from .env file via dotenv.
1236
1240
  *
1237
1241
  * @example
1238
1242
  * const TestDriver = require('testdriverai');
1239
1243
  *
1240
- * const client = new TestDriver(process.env.TD_API_KEY);
1244
+ * // API key loaded automatically from TD_API_KEY in .env
1245
+ * const client = new TestDriver();
1241
1246
  * await client.connect();
1242
1247
  *
1248
+ * // Or pass API key explicitly
1249
+ * const client = new TestDriver('your-api-key');
1250
+ *
1243
1251
  * // New API
1244
1252
  * const element = await client.find('Submit button');
1245
1253
  * await element.click();
@@ -1264,9 +1272,18 @@ const { createMarkdownLogger } = require("./interfaces/logger.js");
1264
1272
 
1265
1273
  class TestDriverSDK {
1266
1274
  constructor(apiKey, options = {}) {
1275
+ // Support calling with just options: new TestDriver({ os: 'windows' })
1276
+ if (typeof apiKey === 'object' && apiKey !== null) {
1277
+ options = apiKey;
1278
+ apiKey = null;
1279
+ }
1280
+
1281
+ // Use provided API key or fall back to environment variable
1282
+ const resolvedApiKey = apiKey || process.env.TD_API_KEY;
1283
+
1267
1284
  // Set up environment with API key
1268
1285
  const environment = {
1269
- TD_API_KEY: apiKey,
1286
+ TD_API_KEY: resolvedApiKey,
1270
1287
  TD_API_ROOT: options.apiRoot || "https://testdriver-api.onrender.com",
1271
1288
  TD_RESOLUTION: options.resolution || "1366x768",
1272
1289
  TD_ANALYTICS: options.analytics !== false,