@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,158 @@
1
+ import { z } from 'zod';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import type { ConsoleBuffer, ConsoleLevel } from '../console-buffer.js';
4
+
5
+ // Raw shapes for MCP SDK (expects ZodRawShape, not ZodObject)
6
+
7
+ const getConsoleLogsShape = {
8
+ level: z
9
+ .enum(['log', 'warn', 'error', 'info', 'debug'])
10
+ .optional()
11
+ .describe('Filter by single log level'),
12
+ levels: z
13
+ .array(z.enum(['log', 'warn', 'error', 'info', 'debug']))
14
+ .optional()
15
+ .describe('Filter by multiple log levels'),
16
+ sinceMs: z
17
+ .number()
18
+ .int()
19
+ .positive()
20
+ .optional()
21
+ .describe('Only messages from the last N milliseconds'),
22
+ limit: z
23
+ .number()
24
+ .int()
25
+ .positive()
26
+ .default(100)
27
+ .describe('Maximum messages to return')
28
+ };
29
+
30
+ const getErrorsShape = {
31
+ sinceMs: z
32
+ .number()
33
+ .int()
34
+ .positive()
35
+ .optional()
36
+ .describe('Only errors from the last N milliseconds'),
37
+ limit: z
38
+ .number()
39
+ .int()
40
+ .positive()
41
+ .default(50)
42
+ .describe('Maximum errors to return'),
43
+ includeWarnings: z
44
+ .boolean()
45
+ .default(false)
46
+ .describe('Include warnings in addition to errors')
47
+ };
48
+
49
+ export function registerErrorTools(
50
+ server: McpServer,
51
+ buffer: ConsoleBuffer
52
+ ): void {
53
+ server.tool(
54
+ 'browser_get_console_logs',
55
+ 'Get console log messages from the browser. Use this to see console output during testing.',
56
+ getConsoleLogsShape,
57
+ async ({ level, levels, sinceMs, limit }) => {
58
+ let filterLevels: ConsoleLevel | ConsoleLevel[] | undefined;
59
+
60
+ if (levels && levels.length > 0) {
61
+ filterLevels = levels as ConsoleLevel[];
62
+ } else if (level) {
63
+ filterLevels = level as ConsoleLevel;
64
+ }
65
+
66
+ const messages = buffer.getAll({
67
+ level: filterLevels,
68
+ sinceMs,
69
+ limit
70
+ });
71
+
72
+ const total = buffer.size();
73
+
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text' as const,
78
+ text: JSON.stringify(
79
+ {
80
+ messages,
81
+ count: messages.length,
82
+ total,
83
+ hasMore: total > messages.length
84
+ },
85
+ null,
86
+ 2
87
+ )
88
+ }
89
+ ]
90
+ };
91
+ }
92
+ );
93
+
94
+ server.tool(
95
+ 'browser_get_errors',
96
+ 'Get console errors (and optionally warnings) from the browser. Use this to check for JavaScript errors while testing an application.',
97
+ getErrorsShape,
98
+ async ({ sinceMs, limit, includeWarnings }) => {
99
+ const levels: ConsoleLevel[] = includeWarnings
100
+ ? ['error', 'warn']
101
+ : ['error'];
102
+
103
+ const errors = buffer.getAll({
104
+ level: levels,
105
+ sinceMs,
106
+ limit
107
+ });
108
+
109
+ const total = buffer.size();
110
+
111
+ if (errors.length === 0) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text' as const,
116
+ text: 'No errors found.'
117
+ }
118
+ ]
119
+ };
120
+ }
121
+
122
+ return {
123
+ content: [
124
+ {
125
+ type: 'text' as const,
126
+ text: JSON.stringify(
127
+ {
128
+ errors,
129
+ count: errors.length,
130
+ hasMore: total > errors.length
131
+ },
132
+ null,
133
+ 2
134
+ )
135
+ }
136
+ ]
137
+ };
138
+ }
139
+ );
140
+
141
+ server.tool(
142
+ 'browser_clear_errors',
143
+ 'Clear the console message buffer. Use this before testing to get a clean slate.',
144
+ {},
145
+ async () => {
146
+ const cleared = buffer.clear();
147
+
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text' as const,
152
+ text: `Cleared ${cleared} console messages.`
153
+ }
154
+ ]
155
+ };
156
+ }
157
+ );
158
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Canonical list of MCP tool names registered by this server.
3
+ * Shared between the smoke test and e2e test suite so both stay in sync.
4
+ */
5
+ export const EXPECTED_TOOLS = [
6
+ 'browser_start',
7
+ 'browser_stop',
8
+ 'browser_screenshot',
9
+ 'browser_navigate',
10
+ 'browser_click',
11
+ 'browser_type',
12
+ 'browser_evaluate',
13
+ 'browser_wait',
14
+ 'browser_page_info',
15
+ 'browser_list_tabs',
16
+ 'browser_new_tab',
17
+ 'browser_close_tab',
18
+ 'browser_switch_tab',
19
+ 'browser_viewport_resize',
20
+ 'browser_get_console_logs',
21
+ 'browser_get_errors',
22
+ 'browser_clear_errors'
23
+ ] as const;
24
+
25
+ export type ToolName = (typeof EXPECTED_TOOLS)[number];
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+
3
+ const ConfigSchema = z.object({
4
+ cdpPort: z.number().int().min(1).max(65535).default(9222),
5
+ cdpHost: z.string().default('127.0.0.1'),
6
+ cdpRetryMs: z.number().int().min(100).default(2000),
7
+ cdpMaxRetryMs: z.number().int().min(1000).default(30000),
8
+ logLevel: z
9
+ .enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal'])
10
+ .default('info'),
11
+ chromePath: z.string().optional(),
12
+ headless: z.boolean().default(true),
13
+ profileName: z.string().default('profile1'),
14
+ viewport: z.string().default('desktop'),
15
+ responsive: z.boolean().optional()
16
+ });
17
+
18
+ export type Config = z.infer<typeof ConfigSchema>;
19
+
20
+ export interface CliArgs {
21
+ cdpPort?: number;
22
+ cdpHost?: string;
23
+ logLevel?: string;
24
+ chromePath?: string;
25
+ headless?: boolean;
26
+ profileName?: string;
27
+ viewport?: string;
28
+ responsive?: boolean;
29
+ }
30
+
31
+ export function loadConfig(cliArgs: CliArgs = {}): Config {
32
+ const env = {
33
+ cdpPort: process.env.PILOT_CDP_PORT
34
+ ? parseInt(process.env.PILOT_CDP_PORT, 10)
35
+ : undefined,
36
+ cdpHost: process.env.PILOT_CDP_HOST,
37
+ cdpRetryMs: process.env.PILOT_CDP_RETRY_MS
38
+ ? parseInt(process.env.PILOT_CDP_RETRY_MS, 10)
39
+ : undefined,
40
+ cdpMaxRetryMs: process.env.PILOT_CDP_MAX_RETRY_MS
41
+ ? parseInt(process.env.PILOT_CDP_MAX_RETRY_MS, 10)
42
+ : undefined,
43
+ logLevel: process.env.PILOT_LOG_LEVEL,
44
+ chromePath: process.env.PILOT_CHROME_PATH,
45
+ headless:
46
+ process.env.PILOT_HEADLESS !== undefined
47
+ ? process.env.PILOT_HEADLESS !== 'false'
48
+ : undefined,
49
+ profileName: process.env.PILOT_PROFILE_NAME,
50
+ viewport: process.env.PILOT_VIEWPORT,
51
+ responsive:
52
+ process.env.PILOT_RESPONSIVE !== undefined
53
+ ? process.env.PILOT_RESPONSIVE !== 'false'
54
+ : undefined
55
+ };
56
+
57
+ const merged = {
58
+ cdpPort: cliArgs.cdpPort ?? env.cdpPort,
59
+ cdpHost: cliArgs.cdpHost ?? env.cdpHost,
60
+ cdpRetryMs: env.cdpRetryMs,
61
+ cdpMaxRetryMs: env.cdpMaxRetryMs,
62
+ logLevel: cliArgs.logLevel ?? env.logLevel,
63
+ chromePath: cliArgs.chromePath ?? env.chromePath,
64
+ headless: cliArgs.headless ?? env.headless,
65
+ profileName: cliArgs.profileName ?? env.profileName,
66
+ viewport: cliArgs.viewport ?? env.viewport,
67
+ responsive: cliArgs.responsive ?? env.responsive
68
+ };
69
+
70
+ return ConfigSchema.parse(merged);
71
+ }
@@ -0,0 +1,18 @@
1
+ import pino from 'pino';
2
+
3
+ let _logger: pino.Logger | null = null;
4
+
5
+ export function createLogger(level = 'info'): pino.Logger {
6
+ // Always write to stderr: this is an MCP server that communicates over stdio,
7
+ // so stdout must carry only MCP protocol messages. pino-pretty is a
8
+ // devDependency and is not available in production/global installs.
9
+ _logger = pino({ level }, process.stderr);
10
+ return _logger;
11
+ }
12
+
13
+ export function getLogger(): pino.Logger {
14
+ if (!_logger) {
15
+ _logger = createLogger();
16
+ }
17
+ return _logger;
18
+ }
@@ -0,0 +1,204 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ interface EmulationDomain {
4
+ setDeviceMetricsOverride(params: {
5
+ width: number;
6
+ height: number;
7
+ deviceScaleFactor: number;
8
+ mobile: boolean;
9
+ }): Promise<void>;
10
+ setTouchEmulationEnabled(params: {
11
+ enabled: boolean;
12
+ maxTouchPoints?: number;
13
+ }): Promise<void>;
14
+ setUserAgentOverride(params: { userAgent: string }): Promise<void>;
15
+ }
16
+
17
+ interface BrowserDomain {
18
+ getWindowForTarget(params?: {
19
+ targetId?: string;
20
+ }): Promise<{ windowId: number }>;
21
+ setWindowBounds(params: {
22
+ windowId: number;
23
+ bounds: { width?: number; height?: number };
24
+ }): Promise<void>;
25
+ }
26
+
27
+ export interface ViewportConfig {
28
+ width: number;
29
+ height: number;
30
+ deviceScaleFactor: number;
31
+ mobile: boolean;
32
+ userAgent?: string;
33
+ responsive?: boolean;
34
+ }
35
+
36
+ const MOBILE_USER_AGENT =
37
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36';
38
+
39
+ export const VIEWPORT_PRESETS = {
40
+ desktop: {
41
+ width: 1920,
42
+ height: 1080,
43
+ deviceScaleFactor: 1,
44
+ mobile: false,
45
+ responsive: true
46
+ },
47
+ 'desktop-small': {
48
+ width: 1366,
49
+ height: 768,
50
+ deviceScaleFactor: 1,
51
+ mobile: false,
52
+ responsive: true
53
+ },
54
+ tablet: {
55
+ width: 768,
56
+ height: 1024,
57
+ deviceScaleFactor: 2,
58
+ mobile: true,
59
+ userAgent: MOBILE_USER_AGENT
60
+ },
61
+ 'tablet-landscape': {
62
+ width: 1024,
63
+ height: 768,
64
+ deviceScaleFactor: 2,
65
+ mobile: true,
66
+ userAgent: MOBILE_USER_AGENT
67
+ },
68
+ mobile: {
69
+ width: 390,
70
+ height: 844,
71
+ deviceScaleFactor: 3,
72
+ mobile: true,
73
+ userAgent: MOBILE_USER_AGENT
74
+ },
75
+ 'mobile-landscape': {
76
+ width: 844,
77
+ height: 390,
78
+ deviceScaleFactor: 3,
79
+ mobile: true,
80
+ userAgent: MOBILE_USER_AGENT
81
+ },
82
+ 'mobile-small': {
83
+ width: 360,
84
+ height: 800,
85
+ deviceScaleFactor: 3,
86
+ mobile: true,
87
+ userAgent: MOBILE_USER_AGENT
88
+ },
89
+ 'desktop-scaled': {
90
+ width: 1536,
91
+ height: 864,
92
+ deviceScaleFactor: 1.25,
93
+ mobile: false
94
+ },
95
+ 'desktop-qhd': {
96
+ width: 2560,
97
+ height: 1440,
98
+ deviceScaleFactor: 1,
99
+ mobile: false,
100
+ responsive: true
101
+ },
102
+ 'mobile-pro': {
103
+ width: 393,
104
+ height: 852,
105
+ deviceScaleFactor: 3,
106
+ mobile: true,
107
+ userAgent: MOBILE_USER_AGENT
108
+ },
109
+ 'mobile-large': {
110
+ width: 430,
111
+ height: 932,
112
+ deviceScaleFactor: 3,
113
+ mobile: true,
114
+ userAgent: MOBILE_USER_AGENT
115
+ },
116
+ 'mobile-android': {
117
+ width: 412,
118
+ height: 915,
119
+ deviceScaleFactor: 2.625,
120
+ mobile: true,
121
+ userAgent: MOBILE_USER_AGENT
122
+ }
123
+ } satisfies Record<string, ViewportConfig>;
124
+
125
+ export type ViewportPresetName = keyof typeof VIEWPORT_PRESETS;
126
+
127
+ export interface ResolveViewportOptions {
128
+ preset?: string;
129
+ width?: number;
130
+ height?: number;
131
+ deviceScaleFactor?: number;
132
+ responsive?: boolean;
133
+ }
134
+
135
+ export function resolveViewport(
136
+ opts: ResolveViewportOptions = {}
137
+ ): ViewportConfig {
138
+ const presetName = opts.preset ?? 'desktop';
139
+ const presets = VIEWPORT_PRESETS as Record<string, ViewportConfig>;
140
+ const base = presets[presetName];
141
+ if (!base) {
142
+ const valid = Object.keys(VIEWPORT_PRESETS).join(', ');
143
+ throw new Error(
144
+ `Unknown viewport preset "${presetName}". Valid presets: ${valid}`
145
+ );
146
+ }
147
+
148
+ return {
149
+ ...base,
150
+ ...(opts.width !== undefined && { width: opts.width }),
151
+ ...(opts.height !== undefined && { height: opts.height }),
152
+ ...(opts.deviceScaleFactor !== undefined && {
153
+ deviceScaleFactor: opts.deviceScaleFactor
154
+ }),
155
+ ...(opts.responsive !== undefined && { responsive: opts.responsive })
156
+ };
157
+ }
158
+
159
+ export async function applyViewport(
160
+ client: Client,
161
+ config: ViewportConfig
162
+ ): Promise<void> {
163
+ const emulation = client.Emulation as unknown as EmulationDomain;
164
+ const browser = client.Browser as unknown as BrowserDomain;
165
+
166
+ // Resize the actual browser window so it matches the viewport dimensions.
167
+ // This is needed because --window-size is unreliable (Chrome may restore
168
+ // a previous size from the profile) and setDeviceMetricsOverride only
169
+ // overrides the rendering viewport, not the window itself.
170
+ try {
171
+ const { windowId } = await browser.getWindowForTarget();
172
+ await browser.setWindowBounds({
173
+ windowId,
174
+ bounds: { width: config.width, height: config.height }
175
+ });
176
+ } catch {
177
+ // Best-effort: headless Chrome or older versions may not support this.
178
+ }
179
+
180
+ // In responsive mode, skip setDeviceMetricsOverride so the page uses the
181
+ // real window dimensions and reflows naturally when the window is resized
182
+ // — just like a normal browser. This is the default for desktop presets.
183
+ if (!config.responsive) {
184
+ await emulation.setDeviceMetricsOverride({
185
+ width: config.width,
186
+ height: config.height,
187
+ deviceScaleFactor: config.deviceScaleFactor,
188
+ mobile: config.mobile
189
+ });
190
+ }
191
+
192
+ if (config.mobile) {
193
+ await emulation.setTouchEmulationEnabled({
194
+ enabled: true,
195
+ maxTouchPoints: 5
196
+ });
197
+ }
198
+
199
+ if (config.userAgent) {
200
+ await emulation.setUserAgentOverride({
201
+ userAgent: config.userAgent
202
+ });
203
+ }
204
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["src/*"]
20
+ }
21
+ },
22
+ "include": ["src"],
23
+ "exclude": ["node_modules", "dist", "test", "e2e"]
24
+ }