playwriter 0.0.2 → 0.0.4

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 (46) hide show
  1. package/bin.js +1 -1
  2. package/dist/browser-config.js +1 -3
  3. package/dist/browser-config.js.map +1 -1
  4. package/dist/cdp-types.d.ts +25 -0
  5. package/dist/cdp-types.d.ts.map +1 -0
  6. package/dist/cdp-types.js +91 -0
  7. package/dist/cdp-types.js.map +1 -0
  8. package/dist/extension/cdp-relay.d.ts +12 -0
  9. package/dist/extension/cdp-relay.d.ts.map +1 -0
  10. package/dist/extension/cdp-relay.js +378 -0
  11. package/dist/extension/cdp-relay.js.map +1 -0
  12. package/dist/extension/protocol.d.ts +29 -0
  13. package/dist/extension/protocol.d.ts.map +1 -0
  14. package/dist/extension/protocol.js +2 -0
  15. package/dist/extension/protocol.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/mcp-client.d.ts.map +1 -1
  21. package/dist/mcp-client.js +1 -1
  22. package/dist/mcp-client.js.map +1 -1
  23. package/dist/mcp.js +74 -464
  24. package/dist/mcp.js.map +1 -1
  25. package/dist/mcp.test.js +101 -142
  26. package/dist/mcp.test.js.map +1 -1
  27. package/dist/prompt.md +41 -487
  28. package/dist/resource.md +436 -0
  29. package/dist/start-relay-server.d.ts +8 -0
  30. package/dist/start-relay-server.d.ts.map +1 -0
  31. package/dist/start-relay-server.js +33 -0
  32. package/dist/start-relay-server.js.map +1 -0
  33. package/package.json +42 -36
  34. package/src/browser-config.ts +48 -50
  35. package/src/cdp-types.ts +124 -0
  36. package/src/extension/cdp-relay.ts +480 -0
  37. package/src/extension/protocol.ts +34 -0
  38. package/src/index.ts +1 -0
  39. package/src/mcp-client.ts +46 -46
  40. package/src/mcp.test.ts +109 -165
  41. package/src/mcp.ts +202 -694
  42. package/src/prompt.md +41 -487
  43. package/src/resource.md +436 -0
  44. package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
  45. package/src/snapshots/shadcn-ui-accessibility.md +300 -510
  46. package/src/start-relay-server.ts +43 -0
@@ -0,0 +1,124 @@
1
+ import type { Protocol } from 'devtools-protocol';
2
+ import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js';
3
+
4
+ export type CDPCommand<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = {
5
+ id: number;
6
+ sessionId?: string;
7
+ method: T;
8
+ params?: ProtocolMapping.Commands[T]['paramsType'][0];
9
+ };
10
+
11
+ export type CDPResponse<T extends keyof ProtocolMapping.Commands = keyof ProtocolMapping.Commands> = {
12
+ id: number;
13
+ sessionId?: string;
14
+ result?: ProtocolMapping.Commands[T]['returnType'];
15
+ error?: { code?: number; message: string };
16
+ };
17
+
18
+ export type CDPEvent<T extends keyof ProtocolMapping.Events = keyof ProtocolMapping.Events> = {
19
+ method: T;
20
+ sessionId?: string;
21
+ params?: ProtocolMapping.Events[T][0];
22
+ };
23
+
24
+ export type CDPMessage = CDPCommand | CDPResponse | CDPEvent;
25
+
26
+ export { Protocol, ProtocolMapping };
27
+
28
+ // types tests. to see if types are right with some simple examples
29
+ if (false as any) {
30
+ const browserVersionCommand = {
31
+ id: 1,
32
+ method: 'Browser.getVersion',
33
+ } satisfies CDPCommand;
34
+
35
+ const browserVersionResponse = {
36
+ id: 1,
37
+ result: {
38
+ protocolVersion: '1.3',
39
+ product: 'Chrome',
40
+ revision: '123',
41
+ userAgent: 'Mozilla/5.0',
42
+ jsVersion: 'V8',
43
+ }
44
+ } satisfies CDPResponse;
45
+
46
+ const targetAttachCommand = {
47
+ id: 2,
48
+ method: 'Target.setAutoAttach',
49
+ params: {
50
+ autoAttach: true,
51
+ waitForDebuggerOnStart: false,
52
+ }
53
+ } satisfies CDPCommand;
54
+
55
+ const targetAttachResponse = {
56
+ id: 2,
57
+ result: undefined,
58
+ } satisfies CDPResponse;
59
+
60
+ const attachedToTargetEvent = {
61
+ method: 'Target.attachedToTarget',
62
+ params: {
63
+ sessionId: 'session-1',
64
+ targetInfo: {
65
+ targetId: 'target-1',
66
+ type: 'page',
67
+ title: 'Example',
68
+ url: 'https://example.com',
69
+ attached: true,
70
+ canAccessOpener: false,
71
+ },
72
+ waitingForDebugger: false,
73
+ }
74
+ } satisfies CDPEvent;
75
+
76
+ const consoleMessageEvent = {
77
+ method: 'Runtime.consoleAPICalled',
78
+ params: {
79
+ type: 'log',
80
+ args: [],
81
+ executionContextId: 1,
82
+ timestamp: 123456789,
83
+ }
84
+ } satisfies CDPEvent;
85
+
86
+ const pageNavigateCommand = {
87
+ id: 3,
88
+ method: 'Page.navigate',
89
+ params: {
90
+ url: 'https://example.com',
91
+ }
92
+ } satisfies CDPCommand;
93
+
94
+ const pageNavigateResponse = {
95
+ id: 3,
96
+ result: {
97
+ frameId: 'frame-1',
98
+ }
99
+ } satisfies CDPResponse;
100
+
101
+ const networkRequestEvent = {
102
+ method: 'Network.requestWillBeSent',
103
+ sessionId: 'session-1',
104
+ params: {
105
+ requestId: 'req-1',
106
+ loaderId: 'loader-1',
107
+ documentURL: 'https://example.com',
108
+ request: {
109
+ url: 'https://example.com/api',
110
+ method: 'GET',
111
+ headers: {},
112
+ initialPriority: 'High',
113
+ referrerPolicy: 'no-referrer',
114
+ },
115
+ timestamp: 123456789,
116
+ wallTime: 123456789,
117
+ initiator: {
118
+ type: 'other',
119
+ },
120
+ redirectHasExtraInfo: false,
121
+ type: 'XHR',
122
+ }
123
+ } satisfies CDPEvent;
124
+ }
@@ -0,0 +1,480 @@
1
+ import { Hono } from 'hono'
2
+ import { serve } from '@hono/node-server'
3
+ import { createNodeWebSocket } from '@hono/node-ws'
4
+ import type { WSContext } from 'hono/ws'
5
+ import type { Protocol } from '../cdp-types.js'
6
+ import type { CDPCommand, CDPResponse, CDPEvent } from '../cdp-types.js'
7
+ import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js'
8
+ import chalk from 'chalk'
9
+
10
+ type ConnectedTarget = {
11
+ sessionId: string
12
+ targetId: string
13
+ targetInfo: Protocol.Target.TargetInfo
14
+ }
15
+
16
+
17
+
18
+ type PlaywrightClient = {
19
+ id: string
20
+ ws: WSContext
21
+ }
22
+
23
+ export async function startPlayWriterCDPRelayServer({ port = 19988, logger = console }: { port?: number; logger?: { log(...args: any[]): void; error(...args: any[]): void } } = {}) {
24
+ const connectedTargets = new Map<string, ConnectedTarget>()
25
+
26
+ const playwrightClients = new Map<string, PlaywrightClient>()
27
+ let extensionWs: WSContext | null = null
28
+
29
+ const extensionPendingRequests = new Map<number, {
30
+ resolve: (result: any) => void
31
+ reject: (error: Error) => void
32
+ }>()
33
+ let extensionMessageId = 0
34
+
35
+ function logCdpMessage({
36
+ direction,
37
+ clientId,
38
+ method,
39
+ sessionId,
40
+ params,
41
+ id,
42
+ source
43
+ }: {
44
+ direction: 'to-playwright' | 'from-playwright' | 'from-extension'
45
+ clientId?: string
46
+ method: string
47
+ sessionId?: string
48
+ params?: any
49
+ id?: number
50
+ source?: 'extension' | 'server'
51
+ }) {
52
+ const noisyEvents = [
53
+ 'Network.requestWillBeSentExtraInfo',
54
+ 'Network.responseReceived',
55
+ 'Network.responseReceivedExtraInfo',
56
+ 'Network.dataReceived',
57
+ 'Network.requestWillBeSent',
58
+ 'Network.loadingFinished'
59
+ ]
60
+
61
+ if (noisyEvents.includes(method)) {
62
+ return
63
+ }
64
+
65
+ const details: string[] = []
66
+
67
+ if (id !== undefined) {
68
+ details.push(`id=${id}`)
69
+ }
70
+
71
+ if (sessionId) {
72
+ details.push(`sessionId=${sessionId}`)
73
+ }
74
+
75
+ if (params) {
76
+ if (params.targetId) {
77
+ details.push(`targetId=${params.targetId}`)
78
+ }
79
+ if (params.targetInfo?.targetId) {
80
+ details.push(`targetId=${params.targetInfo.targetId}`)
81
+ }
82
+ if (params.sessionId && params.sessionId !== sessionId) {
83
+ details.push(`sessionId=${params.sessionId}`)
84
+ }
85
+ }
86
+
87
+ const detailsStr = details.length > 0 ? ` ${chalk.gray(details.join(', '))}` : ''
88
+
89
+ if (direction === 'from-playwright') {
90
+ const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : ''
91
+ logger.log(chalk.cyan('← Playwright'), clientLabel + ':', method + detailsStr)
92
+ } else if (direction === 'from-extension') {
93
+ logger.log(chalk.yellow('← Extension:'), method + detailsStr)
94
+ } else if (direction === 'to-playwright') {
95
+ const color = source === 'server' ? chalk.magenta : chalk.green
96
+ const sourceLabel = source === 'server' ? chalk.gray(' (server-generated)') : ''
97
+ const clientLabel = clientId ? chalk.blue(`[${clientId}]`) : chalk.blue('[ALL]')
98
+ logger.log(color('→ Playwright'), clientLabel + ':', method + detailsStr + sourceLabel)
99
+ }
100
+ }
101
+
102
+ function sendToPlaywright({
103
+ message,
104
+ clientId,
105
+ source = 'extension'
106
+ }: {
107
+ message: CDPResponse | CDPEvent
108
+ clientId?: string
109
+ source?: 'extension' | 'server'
110
+ }) {
111
+ const messageToSend = source === 'server' && 'method' in message
112
+ ? { ...message, __serverGenerated: true }
113
+ : message
114
+
115
+ if ('method' in message) {
116
+ logCdpMessage({
117
+ direction: 'to-playwright',
118
+ clientId,
119
+ method: message.method,
120
+ sessionId: 'sessionId' in message ? message.sessionId : undefined,
121
+ params: 'params' in message ? message.params : undefined,
122
+ source
123
+ })
124
+ }
125
+
126
+ const messageStr = JSON.stringify(messageToSend)
127
+
128
+ if (clientId) {
129
+ const client = playwrightClients.get(clientId)
130
+ if (client) {
131
+ client.ws.send(messageStr)
132
+ }
133
+ } else {
134
+ for (const client of playwrightClients.values()) {
135
+ client.ws.send(messageStr)
136
+ }
137
+ }
138
+ }
139
+
140
+ async function sendToExtension({ method, params }: { method: string; params?: any }) {
141
+ if (!extensionWs) {
142
+ throw new Error('Extension not connected')
143
+ }
144
+
145
+ const id = ++extensionMessageId
146
+ const message = { id, method, params }
147
+
148
+ extensionWs.send(JSON.stringify(message))
149
+
150
+ return new Promise((resolve, reject) => {
151
+ extensionPendingRequests.set(id, { resolve, reject })
152
+ })
153
+ }
154
+
155
+ async function routeCdpCommand({ method, params, sessionId }: { method: string; params: any; sessionId?: string }) {
156
+ switch (method) {
157
+ case 'Browser.getVersion': {
158
+ return {
159
+ protocolVersion: '1.3',
160
+ product: 'Chrome/Extension-Bridge',
161
+ revision: '1.0.0',
162
+ userAgent: 'CDP-Bridge-Server/1.0.0',
163
+ jsVersion: 'V8'
164
+ } satisfies Protocol.Browser.GetVersionResponse
165
+ }
166
+
167
+ case 'Browser.setDownloadBehavior': {
168
+ return {}
169
+ }
170
+
171
+ case 'Target.setAutoAttach': {
172
+ if (sessionId) {
173
+ break
174
+ }
175
+ return {}
176
+ }
177
+
178
+ case 'Target.getTargetInfo': {
179
+ const targetId = params?.targetId
180
+
181
+ if (targetId) {
182
+ for (const target of connectedTargets.values()) {
183
+ if (target.targetId === targetId) {
184
+ return { targetInfo: target.targetInfo }
185
+ }
186
+ }
187
+ }
188
+
189
+ if (sessionId) {
190
+ const target = connectedTargets.get(sessionId)
191
+ if (target) {
192
+ return { targetInfo: target.targetInfo }
193
+ }
194
+ }
195
+
196
+ const firstTarget = Array.from(connectedTargets.values())[0]
197
+ return { targetInfo: firstTarget?.targetInfo }
198
+ }
199
+
200
+ case 'Target.getTargets': {
201
+ return {
202
+ targetInfos: Array.from(connectedTargets.values()).map((t) => ({
203
+ ...t.targetInfo,
204
+ attached: true
205
+ }))
206
+ }
207
+ }
208
+
209
+ case 'Target.createTarget': {
210
+ return await sendToExtension({
211
+ method: 'forwardCDPCommand',
212
+ params: { method, params }
213
+ })
214
+ }
215
+
216
+ case 'Target.closeTarget': {
217
+ return await sendToExtension({
218
+ method: 'forwardCDPCommand',
219
+ params: { method, params }
220
+ })
221
+ }
222
+ }
223
+
224
+ return await sendToExtension({
225
+ method: 'forwardCDPCommand',
226
+ params: { sessionId, method, params }
227
+ })
228
+ }
229
+
230
+ const app = new Hono()
231
+ const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
232
+
233
+ app.get('/', (c) => {
234
+ return c.text('OK')
235
+ })
236
+
237
+ app.get('/cdp/:clientId?', upgradeWebSocket((c) => {
238
+ const clientId = c.req.param('clientId') || 'default'
239
+
240
+ return {
241
+ onOpen(_event, ws) {
242
+ if (playwrightClients.has(clientId)) {
243
+ logger.log(chalk.red(`Rejecting duplicate client ID: ${clientId}`))
244
+ ws.close(1000, 'Client ID already connected')
245
+ return
246
+ }
247
+
248
+ playwrightClients.set(clientId, { id: clientId, ws })
249
+ logger.log(chalk.green(`Playwright client connected: ${clientId} (${playwrightClients.size} total)`))
250
+ },
251
+
252
+ async onMessage(event, ws) {
253
+ let message: CDPCommand
254
+
255
+ try {
256
+ message = JSON.parse(event.data.toString())
257
+ } catch {
258
+ return
259
+ }
260
+
261
+ const { id, sessionId, method, params } = message
262
+
263
+ logCdpMessage({
264
+ direction: 'from-playwright',
265
+ clientId,
266
+ method,
267
+ sessionId,
268
+ id
269
+ })
270
+
271
+ if (!extensionWs) {
272
+ sendToPlaywright({
273
+ message: {
274
+ id,
275
+ sessionId,
276
+ error: { message: 'Extension not connected' }
277
+ },
278
+ clientId
279
+ })
280
+ return
281
+ }
282
+
283
+ try {
284
+ const result: any = await routeCdpCommand({ method, params, sessionId })
285
+
286
+ if (method === 'Target.setAutoAttach' && !sessionId) {
287
+ for (const target of connectedTargets.values()) {
288
+ sendToPlaywright({
289
+ message: {
290
+ method: 'Target.attachedToTarget',
291
+ params: {
292
+ sessionId: target.sessionId,
293
+ targetInfo: {
294
+ ...target.targetInfo,
295
+ attached: true
296
+ },
297
+ waitingForDebugger: false
298
+ }
299
+ } satisfies CDPEvent,
300
+ clientId,
301
+ source: 'server'
302
+ })
303
+ }
304
+ }
305
+
306
+ sendToPlaywright({
307
+ message: { id, sessionId, result },
308
+ clientId
309
+ })
310
+ } catch (e) {
311
+ logger.error('Error handling CDP command:', method, params, e)
312
+ sendToPlaywright({
313
+ message: {
314
+ id,
315
+ sessionId,
316
+ error: { message: (e as Error).message }
317
+ },
318
+ clientId
319
+ })
320
+ }
321
+ },
322
+
323
+ onClose() {
324
+ playwrightClients.delete(clientId)
325
+ logger.log(chalk.yellow(`Playwright client disconnected: ${clientId} (${playwrightClients.size} remaining)`))
326
+ },
327
+
328
+ onError(event) {
329
+ logger.error(`Playwright WebSocket error [${clientId}]:`, event)
330
+ }
331
+ }
332
+ }))
333
+
334
+ app.get('/extension', upgradeWebSocket(() => {
335
+ return {
336
+ onOpen(_event, ws) {
337
+ if (extensionWs) {
338
+ logger.log('Rejecting second extension connection')
339
+ ws.close(1000, 'Another extension connection already established')
340
+ return
341
+ }
342
+
343
+ extensionWs = ws
344
+ logger.log('Extension connected')
345
+ },
346
+
347
+ async onMessage(event, ws) {
348
+ let message: ExtensionMessage
349
+
350
+ try {
351
+ message = JSON.parse(event.data.toString())
352
+ } catch {
353
+ ws.close(1000, 'Invalid JSON')
354
+ return
355
+ }
356
+
357
+ if ('id' in message) {
358
+ const pending = extensionPendingRequests.get(message.id)
359
+ if (!pending) {
360
+ logger.log('Unexpected response with id:', message.id)
361
+ return
362
+ }
363
+
364
+ extensionPendingRequests.delete(message.id)
365
+
366
+ if (message.error) {
367
+ pending.reject(new Error(message.error))
368
+ } else {
369
+ pending.resolve(message.result)
370
+ }
371
+ } else {
372
+ const extensionEvent = message as ExtensionEventMessage
373
+
374
+ if (extensionEvent.method !== 'forwardCDPEvent') {
375
+ return
376
+ }
377
+
378
+ const { method, params, sessionId } = extensionEvent.params
379
+
380
+ logCdpMessage({
381
+ direction: 'from-extension',
382
+ method,
383
+ sessionId,
384
+ params
385
+ })
386
+
387
+ if (method === 'Target.attachedToTarget') {
388
+ const targetParams = params as Protocol.Target.AttachedToTargetEvent
389
+
390
+ // Check if we already sent this target to clients (e.g., from Target.setAutoAttach response)
391
+ const alreadyConnected = connectedTargets.has(targetParams.sessionId)
392
+
393
+ // Always update our local state with latest target info
394
+ connectedTargets.set(targetParams.sessionId, {
395
+ sessionId: targetParams.sessionId,
396
+ targetId: targetParams.targetInfo.targetId,
397
+ targetInfo: targetParams.targetInfo
398
+ })
399
+
400
+ // Only forward to Playwright if this is a new target to avoid duplicates
401
+ if (!alreadyConnected) {
402
+ sendToPlaywright({
403
+ message: {
404
+ method: 'Target.attachedToTarget',
405
+ params: targetParams
406
+ } as CDPEvent,
407
+ source: 'extension'
408
+ })
409
+ }
410
+ } else if (method === 'Target.detachedFromTarget') {
411
+ const detachParams = params as Protocol.Target.DetachedFromTargetEvent
412
+ connectedTargets.delete(detachParams.sessionId)
413
+
414
+ sendToPlaywright({
415
+ message: {
416
+ method: 'Target.detachedFromTarget',
417
+ params: detachParams
418
+ } as CDPEvent,
419
+ source: 'extension'
420
+ })
421
+ } else {
422
+ sendToPlaywright({
423
+ message: {
424
+ sessionId,
425
+ method,
426
+ params
427
+ } as CDPEvent,
428
+ source: 'extension'
429
+ })
430
+ }
431
+ }
432
+ },
433
+
434
+ onClose() {
435
+ logger.log('Extension disconnected')
436
+
437
+ for (const pending of extensionPendingRequests.values()) {
438
+ pending.reject(new Error('Extension connection closed'))
439
+ }
440
+ extensionPendingRequests.clear()
441
+
442
+ extensionWs = null
443
+ connectedTargets.clear()
444
+
445
+ for (const client of playwrightClients.values()) {
446
+ client.ws.close(1000, 'Extension disconnected')
447
+ }
448
+ playwrightClients.clear()
449
+ },
450
+
451
+ onError(event) {
452
+ logger.error('Extension WebSocket error:', event)
453
+ }
454
+ }
455
+ }))
456
+
457
+ const server = serve({ fetch: app.fetch, port })
458
+ injectWebSocket(server)
459
+
460
+ const wsHost = `ws://localhost:${port}`
461
+ const cdpEndpoint = `${wsHost}/cdp`
462
+ const extensionEndpoint = `${wsHost}/extension`
463
+
464
+ logger.log('CDP relay server started')
465
+ logger.log('Extension endpoint:', extensionEndpoint)
466
+ logger.log('CDP endpoint:', cdpEndpoint)
467
+
468
+ return {
469
+ cdpEndpoint,
470
+ extensionEndpoint,
471
+ close() {
472
+ for (const client of playwrightClients.values()) {
473
+ client.ws.close(1000, 'Server stopped')
474
+ }
475
+ playwrightClients.clear()
476
+ extensionWs?.close(1000, 'Server stopped')
477
+ server.close()
478
+ }
479
+ }
480
+ }
@@ -0,0 +1,34 @@
1
+ export const VERSION = 1
2
+
3
+ export type ExtensionCommandMessage =
4
+ | {
5
+ id: number
6
+ method: 'attachToTab'
7
+ params?: object
8
+ }
9
+ | {
10
+ id: number
11
+ method: 'forwardCDPCommand'
12
+ params: {
13
+ method: string
14
+ sessionId?: string
15
+ params?: any
16
+ }
17
+ }
18
+
19
+ export type ExtensionResponseMessage = {
20
+ id: number
21
+ result?: any
22
+ error?: string
23
+ }
24
+
25
+ export type ExtensionEventMessage = {
26
+ method: 'forwardCDPEvent'
27
+ params: {
28
+ method: string
29
+ sessionId?: string
30
+ params?: any
31
+ }
32
+ }
33
+
34
+ export type ExtensionMessage = ExtensionResponseMessage | ExtensionEventMessage
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './extension/cdp-relay.js'