@everydaydevopsio/pilot 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.ai-agent-bridge.yaml +10 -0
  2. package/.dockerignore +9 -0
  3. package/CLAUDE.md +61 -0
  4. package/LICENSE +21 -0
  5. package/README.md +252 -0
  6. package/dist/browser.d.ts +100 -0
  7. package/dist/browser.d.ts.map +1 -0
  8. package/dist/browser.js +654 -0
  9. package/dist/browser.js.map +1 -0
  10. package/dist/cli/init.d.ts +4 -0
  11. package/dist/cli/init.d.ts.map +1 -0
  12. package/dist/cli/init.js +56 -0
  13. package/dist/cli/init.js.map +1 -0
  14. package/dist/cli/skill-template.d.ts +2 -0
  15. package/dist/cli/skill-template.d.ts.map +1 -0
  16. package/dist/cli/skill-template.js +114 -0
  17. package/dist/cli/skill-template.js.map +1 -0
  18. package/dist/commands/click.d.ts +21 -0
  19. package/dist/commands/click.d.ts.map +1 -0
  20. package/dist/commands/click.js +64 -0
  21. package/dist/commands/click.js.map +1 -0
  22. package/dist/commands/evaluate.d.ts +14 -0
  23. package/dist/commands/evaluate.d.ts.map +1 -0
  24. package/dist/commands/evaluate.js +28 -0
  25. package/dist/commands/evaluate.js.map +1 -0
  26. package/dist/commands/navigate.d.ts +19 -0
  27. package/dist/commands/navigate.d.ts.map +1 -0
  28. package/dist/commands/navigate.js +78 -0
  29. package/dist/commands/navigate.js.map +1 -0
  30. package/dist/commands/page_info.d.ts +8 -0
  31. package/dist/commands/page_info.d.ts.map +1 -0
  32. package/dist/commands/page_info.js +13 -0
  33. package/dist/commands/page_info.js.map +1 -0
  34. package/dist/commands/screenshot.d.ts +18 -0
  35. package/dist/commands/screenshot.d.ts.map +1 -0
  36. package/dist/commands/screenshot.js +37 -0
  37. package/dist/commands/screenshot.js.map +1 -0
  38. package/dist/commands/tabs.d.ts +10 -0
  39. package/dist/commands/tabs.d.ts.map +1 -0
  40. package/dist/commands/tabs.js +15 -0
  41. package/dist/commands/tabs.js.map +1 -0
  42. package/dist/commands/type.d.ts +14 -0
  43. package/dist/commands/type.d.ts.map +1 -0
  44. package/dist/commands/type.js +61 -0
  45. package/dist/commands/type.js.map +1 -0
  46. package/dist/commands/wait.d.ts +17 -0
  47. package/dist/commands/wait.d.ts.map +1 -0
  48. package/dist/commands/wait.js +61 -0
  49. package/dist/commands/wait.js.map +1 -0
  50. package/dist/mcp/console-buffer.d.ts +28 -0
  51. package/dist/mcp/console-buffer.d.ts.map +1 -0
  52. package/dist/mcp/console-buffer.js +68 -0
  53. package/dist/mcp/console-buffer.js.map +1 -0
  54. package/dist/mcp/index.d.ts +3 -0
  55. package/dist/mcp/index.d.ts.map +1 -0
  56. package/dist/mcp/index.js +74 -0
  57. package/dist/mcp/index.js.map +1 -0
  58. package/dist/mcp/server.d.ts +18 -0
  59. package/dist/mcp/server.d.ts.map +1 -0
  60. package/dist/mcp/server.js +46 -0
  61. package/dist/mcp/server.js.map +1 -0
  62. package/dist/mcp/tools/browser.d.ts +5 -0
  63. package/dist/mcp/tools/browser.d.ts.map +1 -0
  64. package/dist/mcp/tools/browser.js +435 -0
  65. package/dist/mcp/tools/browser.js.map +1 -0
  66. package/dist/mcp/tools/errors.d.ts +4 -0
  67. package/dist/mcp/tools/errors.d.ts.map +1 -0
  68. package/dist/mcp/tools/errors.js +117 -0
  69. package/dist/mcp/tools/errors.js.map +1 -0
  70. package/dist/mcp/tools/names.d.ts +7 -0
  71. package/dist/mcp/tools/names.d.ts.map +1 -0
  72. package/dist/mcp/tools/names.js +24 -0
  73. package/dist/mcp/tools/names.js.map +1 -0
  74. package/dist/util/config.d.ts +34 -0
  75. package/dist/util/config.d.ts.map +1 -0
  76. package/dist/util/config.js +53 -0
  77. package/dist/util/config.js.map +1 -0
  78. package/dist/util/logger.d.ts +4 -0
  79. package/dist/util/logger.d.ts.map +1 -0
  80. package/dist/util/logger.js +16 -0
  81. package/dist/util/logger.js.map +1 -0
  82. package/dist/viewport.d.ts +105 -0
  83. package/dist/viewport.d.ts.map +1 -0
  84. package/dist/viewport.js +145 -0
  85. package/dist/viewport.js.map +1 -0
  86. package/package.json +74 -0
  87. package/smoke/mcp-smoke.ts +161 -0
  88. package/src/browser.ts +798 -0
  89. package/src/cli/init.ts +62 -0
  90. package/src/cli/skill-template.ts +113 -0
  91. package/src/commands/click.ts +93 -0
  92. package/src/commands/evaluate.ts +45 -0
  93. package/src/commands/navigate.ts +105 -0
  94. package/src/commands/page_info.ts +21 -0
  95. package/src/commands/screenshot.ts +72 -0
  96. package/src/commands/tabs.ts +35 -0
  97. package/src/commands/type.ts +77 -0
  98. package/src/commands/wait.ts +90 -0
  99. package/src/mcp/console-buffer.ts +94 -0
  100. package/src/mcp/index.ts +107 -0
  101. package/src/mcp/server.ts +67 -0
  102. package/src/mcp/tools/browser.ts +605 -0
  103. package/src/mcp/tools/errors.ts +158 -0
  104. package/src/mcp/tools/names.ts +25 -0
  105. package/src/util/config.ts +71 -0
  106. package/src/util/logger.ts +18 -0
  107. package/src/viewport.ts +204 -0
  108. package/tsconfig.json +24 -0
@@ -0,0 +1,62 @@
1
+ import { mkdir, writeFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { SKILL_CONTENT } from './skill-template.js';
4
+
5
+ const SKILL_DIR = '.claude/skills/debug-browser';
6
+ const SKILL_FILE = 'SKILL.md';
7
+
8
+ export async function runInit(
9
+ options: { force?: boolean } = {}
10
+ ): Promise<void> {
11
+ const cwd = process.cwd();
12
+ const skillDir = join(cwd, SKILL_DIR);
13
+ const skillPath = join(skillDir, SKILL_FILE);
14
+
15
+ // Check if skill already exists
16
+ try {
17
+ await stat(skillPath);
18
+ if (!options.force) {
19
+ console.log(`\x1b[33mSkill already exists:\x1b[0m ${skillPath}`);
20
+ console.log('Use --force to overwrite.');
21
+ process.exit(1);
22
+ }
23
+ } catch {
24
+ // File doesn't exist, continue
25
+ }
26
+
27
+ // Create directory structure
28
+ try {
29
+ await mkdir(skillDir, { recursive: true });
30
+ } catch (err) {
31
+ console.error(`\x1b[31mFailed to create directory:\x1b[0m ${skillDir}`);
32
+ console.error(err);
33
+ process.exit(1);
34
+ }
35
+
36
+ // Write SKILL.md
37
+ try {
38
+ await writeFile(skillPath, SKILL_CONTENT, 'utf-8');
39
+ } catch (err) {
40
+ console.error(`\x1b[31mFailed to write skill file:\x1b[0m ${skillPath}`);
41
+ console.error(err);
42
+ process.exit(1);
43
+ }
44
+
45
+ // Success message
46
+ console.log(`\x1b[32m✓ Created Claude Code skill:\x1b[0m ${skillPath}`);
47
+ console.log('');
48
+ console.log('\x1b[1mUsage:\x1b[0m');
49
+ console.log(' In Claude Code, say "debug in browser" or use /debug-browser');
50
+ console.log('');
51
+ console.log('\x1b[1mThe skill will:\x1b[0m');
52
+ console.log(' 1. Start Chrome with remote debugging');
53
+ console.log(' 2. Launch the pilot browser service');
54
+ console.log(' 3. Add the MCP server to your session');
55
+ console.log(' 4. Spawn an error-watching sub-agent');
56
+ console.log('');
57
+ console.log('\x1b[1mPrerequisites:\x1b[0m');
58
+ console.log(' - Google Chrome installed');
59
+ console.log(
60
+ ' - pilot (install with: npm install -g @everydaydevopsio/pilot)'
61
+ );
62
+ }
@@ -0,0 +1,113 @@
1
+ export const SKILL_CONTENT = `---
2
+ name: debug-browser
3
+ description: Add the pilot MCP server and watch for console errors. Use this skill when the user wants to debug a web application, watch for JavaScript errors, or test their app with AI assistance.
4
+ tools:
5
+ - Bash
6
+ - Task
7
+ ---
8
+
9
+ # Debug Browser Skill
10
+
11
+ This skill sets up a browser debugging environment for AI-assisted development using the pilot MCP server. The MCP server manages the Chrome lifecycle automatically via \`browser_start\`.
12
+
13
+ ## What This Skill Does
14
+
15
+ 1. Adds the pilot MCP server to your Claude Code session
16
+ 2. Starts the browser via \`browser_start\`
17
+ 3. Spawns a sub-agent to watch for console errors
18
+
19
+ ## Instructions
20
+
21
+ Follow these steps in order.
22
+
23
+ ### Step 1: Add MCP Server to Claude Code Session
24
+
25
+ Add the pilot MCP server to the current Claude Code session:
26
+
27
+ \`\`\`bash
28
+ claude mcp add pilot --scope session -- npx @everydaydevopsio/pilot
29
+ \`\`\`
30
+
31
+ This gives you access to browser control tools like:
32
+ - \`browser_start\` - Launch Chrome
33
+ - \`browser_stop\` - Stop Chrome
34
+ - \`browser_screenshot\` - Capture screenshots
35
+ - \`browser_navigate\` - Navigate to URLs
36
+ - \`browser_click\` - Click elements
37
+ - \`browser_type\` - Type text
38
+ - \`browser_evaluate\` - Run JavaScript
39
+ - \`browser_get_errors\` - Get console errors
40
+ - \`browser_clear_errors\` - Clear error buffer
41
+
42
+ ### Step 2: Start the Browser
43
+
44
+ Use the \`browser_start\` MCP tool to launch Chrome:
45
+
46
+ \`\`\`
47
+ Call browser_start with headless: false to open a visible Chrome window.
48
+ \`\`\`
49
+
50
+ ### Step 3: Spawn Error-Watching Sub-Agent
51
+
52
+ Use the Task tool to spawn a background sub-agent that monitors for console errors:
53
+
54
+ \`\`\`
55
+ Spawn a sub-agent with subagent_type "general-purpose" and run_in_background: true with this prompt:
56
+
57
+ "You are an error-watching agent monitoring the browser for console errors.
58
+
59
+ Your job:
60
+ 1. First, call browser_clear_errors to start with a clean slate
61
+ 2. Report back that monitoring has started
62
+ 3. Every 15-20 seconds, call browser_get_errors with includeWarnings: true
63
+ 4. If you find errors, report them with:
64
+ - The error message
65
+ - The source file and line number
66
+ - A brief analysis of what might be wrong
67
+ 5. Continue monitoring until the main conversation ends
68
+
69
+ Be concise but informative when reporting errors."
70
+ \`\`\`
71
+
72
+ ### Step 4: Inform the User
73
+
74
+ Tell the user:
75
+ - The browser debugging environment is ready
76
+ - They can navigate to their app using \`browser_navigate\`
77
+ - You will be notified of any console errors automatically
78
+ - They can ask you to take screenshots, click elements, or run JavaScript
79
+
80
+ ## Cleanup
81
+
82
+ When done debugging, call \`browser_stop\` to shut down Chrome cleanly.
83
+
84
+ ## Troubleshooting
85
+
86
+ ### browser_start fails
87
+ - Check if Chrome is installed: the MCP server auto-detects common paths
88
+ - Set \`PILOT_CHROME_PATH\` env var to the Chrome executable if auto-detect fails
89
+
90
+ ### No errors detected
91
+ - Errors are only captured while connected
92
+ - Try \`browser_get_errors\` with \`includeWarnings: true\` to see all console output
93
+ - Verify the page has loaded: \`browser_page_info\`
94
+
95
+ ## Example Session
96
+
97
+ User: "debug in browser"
98
+
99
+ Claude executes:
100
+ 1. Adds MCP server to session
101
+ 2. Calls \`browser_start\` (headless: false)
102
+ 3. Spawns error-watching sub-agent
103
+ 4. Reports: "Browser debugging environment ready! Navigate to your app and I'll watch for errors."
104
+
105
+ User: "go to localhost:3000"
106
+ Claude: Uses \`browser_navigate\` to go to http://localhost:3000
107
+
108
+ [User interacts with the app, an error occurs]
109
+
110
+ Sub-agent reports: "Error detected: TypeError: Cannot read property 'map' of undefined at App.tsx:42"
111
+
112
+ Claude: Reads App.tsx, identifies the bug, suggests a fix.
113
+ `;
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+ import type { Client } from 'chrome-remote-interface';
3
+
4
+ export const ClickParamsSchema = z
5
+ .object({
6
+ selector: z.string().optional(),
7
+ x: z.number().optional(),
8
+ y: z.number().optional(),
9
+ button: z.enum(['left', 'right', 'middle']).default('left'),
10
+ clickCount: z.number().int().positive().default(1),
11
+ timeoutMs: z.number().int().positive().default(5000)
12
+ })
13
+ .refine(
14
+ (v) => v.selector !== undefined || (v.x !== undefined && v.y !== undefined),
15
+ {
16
+ message: 'Either selector or x/y coordinates are required'
17
+ }
18
+ );
19
+
20
+ export type ClickParams = z.infer<typeof ClickParamsSchema>;
21
+
22
+ export interface ClickResult {
23
+ x: number;
24
+ y: number;
25
+ }
26
+
27
+ async function waitForSelector(
28
+ client: Client,
29
+ selector: string,
30
+ timeoutMs: number
31
+ ): Promise<{ x: number; y: number }> {
32
+ const start = Date.now();
33
+ while (Date.now() - start < timeoutMs) {
34
+ const result = await client.Runtime.evaluate({
35
+ expression: `(function() {
36
+ const el = document.querySelector(${JSON.stringify(selector)});
37
+ if (!el) return null;
38
+ const rect = el.getBoundingClientRect();
39
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
40
+ })()`,
41
+ returnByValue: true
42
+ });
43
+
44
+ if (result.result.value !== null && result.result.value !== undefined) {
45
+ return result.result.value as { x: number; y: number };
46
+ }
47
+
48
+ await new Promise((r) => setTimeout(r, 100));
49
+ }
50
+ throw new Error(`timeout waiting for selector: ${selector}`);
51
+ }
52
+
53
+ export async function executeClick(
54
+ client: Client,
55
+ params: ClickParams
56
+ ): Promise<ClickResult> {
57
+ let x: number;
58
+ let y: number;
59
+
60
+ if (params.selector !== undefined) {
61
+ const pos = await waitForSelector(
62
+ client,
63
+ params.selector,
64
+ params.timeoutMs
65
+ );
66
+ x = pos.x;
67
+ y = pos.y;
68
+ } else {
69
+ x = params.x!;
70
+ y = params.y!;
71
+ }
72
+
73
+ const button = params.button;
74
+
75
+ for (let i = 0; i < params.clickCount; i++) {
76
+ await client.Input.dispatchMouseEvent({
77
+ type: 'mousePressed',
78
+ x,
79
+ y,
80
+ button,
81
+ clickCount: 1
82
+ });
83
+ await client.Input.dispatchMouseEvent({
84
+ type: 'mouseReleased',
85
+ x,
86
+ y,
87
+ button,
88
+ clickCount: 1
89
+ });
90
+ }
91
+
92
+ return { x, y };
93
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ import type { Client } from 'chrome-remote-interface';
3
+
4
+ export const EvaluateParamsSchema = z.object({
5
+ expression: z.string(),
6
+ awaitPromise: z.boolean().default(true),
7
+ timeoutMs: z.number().int().positive().default(10000)
8
+ });
9
+
10
+ export type EvaluateParams = z.infer<typeof EvaluateParamsSchema>;
11
+
12
+ export interface EvaluateResult {
13
+ value: unknown;
14
+ type: string;
15
+ }
16
+
17
+ export async function executeEvaluate(
18
+ client: Client,
19
+ params: EvaluateParams
20
+ ): Promise<EvaluateResult> {
21
+ const result = await Promise.race([
22
+ client.Runtime.evaluate({
23
+ expression: params.expression,
24
+ awaitPromise: params.awaitPromise,
25
+ returnByValue: true,
26
+ userGesture: true
27
+ }),
28
+ new Promise<never>((_, reject) =>
29
+ setTimeout(() => reject(new Error('timeout')), params.timeoutMs)
30
+ )
31
+ ]);
32
+
33
+ if (result.exceptionDetails) {
34
+ const msg =
35
+ result.exceptionDetails.exception?.description ??
36
+ result.exceptionDetails.text ??
37
+ 'evaluation error';
38
+ throw new Error(msg);
39
+ }
40
+
41
+ return {
42
+ value: result.result.value,
43
+ type: result.result.type
44
+ };
45
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod';
2
+ import type { Client } from 'chrome-remote-interface';
3
+ import type { BrowserManager } from '../browser.js';
4
+
5
+ export const NavigateParamsSchema = z.object({
6
+ url: z.string().url(),
7
+ waitUntil: z
8
+ .enum(['load', 'domcontentloaded', 'networkidle'])
9
+ .default('load'),
10
+ timeoutMs: z.number().int().positive().default(30000)
11
+ });
12
+
13
+ export type NavigateParams = z.infer<typeof NavigateParamsSchema>;
14
+
15
+ export interface NavigateResult {
16
+ url: string;
17
+ status: number;
18
+ }
19
+
20
+ export async function executeNavigate(
21
+ client: Client,
22
+ manager: BrowserManager,
23
+ params: NavigateParams
24
+ ): Promise<NavigateResult> {
25
+ let finalStatus = 200;
26
+
27
+ const navigationPromise = new Promise<{ url: string; status: number }>(
28
+ (resolve, reject) => {
29
+ const timeout = setTimeout(() => {
30
+ reject(new Error('timeout'));
31
+ }, params.timeoutMs);
32
+
33
+ const cleanup = () => clearTimeout(timeout);
34
+
35
+ if (params.waitUntil === 'load') {
36
+ client.Page.loadEventFired(() => {
37
+ cleanup();
38
+ client.Runtime.evaluate({ expression: 'window.location.href' }).then(
39
+ (r) => {
40
+ resolve({
41
+ url: (r.result.value as string) ?? params.url,
42
+ status: finalStatus
43
+ });
44
+ },
45
+ reject
46
+ );
47
+ });
48
+ } else if (params.waitUntil === 'domcontentloaded') {
49
+ client.Page.domContentEventFired(() => {
50
+ cleanup();
51
+ client.Runtime.evaluate({ expression: 'window.location.href' }).then(
52
+ (r) => {
53
+ resolve({
54
+ url: (r.result.value as string) ?? params.url,
55
+ status: finalStatus
56
+ });
57
+ },
58
+ reject
59
+ );
60
+ });
61
+ } else {
62
+ // networkidle
63
+ manager
64
+ .waitForNetworkIdle(params.timeoutMs)
65
+ .then(() => {
66
+ cleanup();
67
+ return client.Runtime.evaluate({
68
+ expression: 'window.location.href'
69
+ });
70
+ })
71
+ .then((r) => {
72
+ resolve({
73
+ url: (r.result.value as string) ?? params.url,
74
+ status: finalStatus
75
+ });
76
+ })
77
+ .catch((err: unknown) => {
78
+ cleanup();
79
+ reject(err);
80
+ });
81
+ }
82
+ }
83
+ );
84
+
85
+ // Capture the response status
86
+ const responseHandler = (params: {
87
+ response: { url: string; status: number };
88
+ }) => {
89
+ if (params.response.url === params.response.url && params.response.status) {
90
+ finalStatus = params.response.status;
91
+ }
92
+ };
93
+ client.Network.responseReceived(responseHandler);
94
+
95
+ const { frameId } = await client.Page.navigate({ url: params.url });
96
+ void frameId; // used implicitly
97
+
98
+ try {
99
+ const result = await navigationPromise;
100
+ return result;
101
+ } finally {
102
+ // Event listeners are auto-removed when client disconnects;
103
+ // for long-lived clients this is acceptable for v0.1
104
+ }
105
+ }
@@ -0,0 +1,21 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ export interface PageInfoResult {
4
+ url: string;
5
+ title: string;
6
+ readyState: string;
7
+ }
8
+
9
+ export async function executePageInfo(client: Client): Promise<PageInfoResult> {
10
+ const result = await client.Runtime.evaluate({
11
+ expression: `JSON.stringify({
12
+ url: window.location.href,
13
+ title: document.title,
14
+ readyState: document.readyState
15
+ })`,
16
+ returnByValue: true
17
+ });
18
+
19
+ const value = result.result.value as string;
20
+ return JSON.parse(value) as PageInfoResult;
21
+ }
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import type { Client } from 'chrome-remote-interface';
3
+
4
+ export const ScreenshotParamsSchema = z.object({
5
+ format: z.enum(['png', 'jpeg']).default('png'),
6
+ quality: z.number().int().min(1).max(100).default(80),
7
+ fullPage: z.boolean().default(false)
8
+ });
9
+
10
+ export type ScreenshotParams = z.infer<typeof ScreenshotParamsSchema>;
11
+
12
+ export interface ScreenshotResult {
13
+ dataUrl: string;
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ export async function executeScreenshot(
19
+ client: Client,
20
+ params: ScreenshotParams
21
+ ): Promise<ScreenshotResult> {
22
+ let clip:
23
+ | {
24
+ x: number;
25
+ y: number;
26
+ width: number;
27
+ height: number;
28
+ scale: number;
29
+ }
30
+ | undefined;
31
+
32
+ if (params.fullPage) {
33
+ const layoutResult = await client.Page.getLayoutMetrics();
34
+ const { width, height } = layoutResult.contentSize;
35
+ clip = { x: 0, y: 0, width, height, scale: 1 };
36
+ }
37
+
38
+ const captureParams: {
39
+ format?: 'png' | 'jpeg' | 'webp';
40
+ quality?: number;
41
+ clip?: typeof clip;
42
+ captureBeyondViewport?: boolean;
43
+ } = {
44
+ format: params.format,
45
+ captureBeyondViewport: params.fullPage
46
+ };
47
+
48
+ if (params.format === 'jpeg') {
49
+ captureParams.quality = params.quality;
50
+ }
51
+
52
+ if (clip) {
53
+ captureParams.clip = clip;
54
+ }
55
+
56
+ const { data } = await client.Page.captureScreenshot(captureParams);
57
+
58
+ const mime = params.format === 'jpeg' ? 'image/jpeg' : 'image/png';
59
+ const dataUrl = `data:${mime};base64,${data}`;
60
+
61
+ // Decode dimensions from base64
62
+ const buf = Buffer.from(data, 'base64');
63
+ let width = 0;
64
+ let height = 0;
65
+
66
+ if (params.format === 'png' && buf.length > 24) {
67
+ width = buf.readUInt32BE(16);
68
+ height = buf.readUInt32BE(20);
69
+ }
70
+
71
+ return { dataUrl, width, height };
72
+ }
@@ -0,0 +1,35 @@
1
+ import type { BrowserManager, TabInfo } from '../browser.js';
2
+
3
+ export async function executeListTabs(
4
+ manager: BrowserManager
5
+ ): Promise<TabInfo[]> {
6
+ return manager.listTabs();
7
+ }
8
+
9
+ export interface NewTabResult {
10
+ targetId: string;
11
+ url: string;
12
+ }
13
+
14
+ export async function executeNewTab(
15
+ manager: BrowserManager,
16
+ url?: string
17
+ ): Promise<NewTabResult> {
18
+ const tab = await manager.createTab(url);
19
+ await manager.switchTab(tab.targetId);
20
+ return { targetId: tab.targetId, url: tab.url };
21
+ }
22
+
23
+ export async function executeCloseTab(
24
+ manager: BrowserManager,
25
+ targetId: string
26
+ ): Promise<void> {
27
+ await manager.closeTab(targetId);
28
+ }
29
+
30
+ export async function executeSwitchTab(
31
+ manager: BrowserManager,
32
+ targetId: string
33
+ ): Promise<void> {
34
+ await manager.switchTab(targetId);
35
+ }
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ import type { Client } from 'chrome-remote-interface';
3
+
4
+ export const TypeParamsSchema = z.object({
5
+ text: z.string(),
6
+ selector: z.string().optional(),
7
+ clearFirst: z.boolean().default(false),
8
+ delayMs: z.number().int().min(0).default(0)
9
+ });
10
+
11
+ export type TypeParams = z.infer<typeof TypeParamsSchema>;
12
+
13
+ export interface TypeResult {
14
+ ok: boolean;
15
+ }
16
+
17
+ async function focusSelector(client: Client, selector: string): Promise<void> {
18
+ const result = await client.Runtime.evaluate({
19
+ expression: `(function() {
20
+ const el = document.querySelector(${JSON.stringify(selector)});
21
+ if (!el) return false;
22
+ el.focus();
23
+ return true;
24
+ })()`
25
+ });
26
+ if (!result.result.value) {
27
+ throw new Error(`Element not found: ${selector}`);
28
+ }
29
+ }
30
+
31
+ export async function executeType(
32
+ client: Client,
33
+ params: TypeParams
34
+ ): Promise<TypeResult> {
35
+ if (params.selector) {
36
+ await focusSelector(client, params.selector);
37
+ }
38
+
39
+ if (params.clearFirst) {
40
+ // Select all and delete
41
+ await client.Input.dispatchKeyEvent({
42
+ type: 'keyDown',
43
+ key: 'a',
44
+ modifiers: 2 // Ctrl
45
+ });
46
+ await client.Input.dispatchKeyEvent({
47
+ type: 'keyUp',
48
+ key: 'a',
49
+ modifiers: 2
50
+ });
51
+ await client.Input.dispatchKeyEvent({
52
+ type: 'keyDown',
53
+ key: 'Delete'
54
+ });
55
+ await client.Input.dispatchKeyEvent({
56
+ type: 'keyUp',
57
+ key: 'Delete'
58
+ });
59
+ }
60
+
61
+ for (const char of params.text) {
62
+ await client.Input.dispatchKeyEvent({
63
+ type: 'keyDown',
64
+ text: char
65
+ });
66
+ await client.Input.dispatchKeyEvent({
67
+ type: 'keyUp',
68
+ text: char
69
+ });
70
+
71
+ if (params.delayMs > 0) {
72
+ await new Promise((r) => setTimeout(r, params.delayMs));
73
+ }
74
+ }
75
+
76
+ return { ok: true };
77
+ }