playwriter 0.0.16 → 0.0.21

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 (47) hide show
  1. package/dist/cdp-session.d.ts +21 -0
  2. package/dist/cdp-session.d.ts.map +1 -0
  3. package/dist/cdp-session.js +131 -0
  4. package/dist/cdp-session.js.map +1 -0
  5. package/dist/cdp-types.d.ts +15 -0
  6. package/dist/cdp-types.d.ts.map +1 -1
  7. package/dist/cdp-types.js.map +1 -1
  8. package/dist/create-logger.d.ts +9 -0
  9. package/dist/create-logger.d.ts.map +1 -0
  10. package/dist/create-logger.js +43 -0
  11. package/dist/create-logger.js.map +1 -0
  12. package/dist/extension/cdp-relay.d.ts +7 -3
  13. package/dist/extension/cdp-relay.d.ts.map +1 -1
  14. package/dist/extension/cdp-relay.js +22 -12
  15. package/dist/extension/cdp-relay.js.map +1 -1
  16. package/dist/mcp.js +86 -44
  17. package/dist/mcp.js.map +1 -1
  18. package/dist/mcp.test.d.ts.map +1 -1
  19. package/dist/mcp.test.js +669 -183
  20. package/dist/mcp.test.js.map +1 -1
  21. package/dist/prompt.md +38 -8
  22. package/dist/selector-generator.js +331 -0
  23. package/dist/start-relay-server.d.ts +1 -3
  24. package/dist/start-relay-server.d.ts.map +1 -1
  25. package/dist/start-relay-server.js +3 -16
  26. package/dist/start-relay-server.js.map +1 -1
  27. package/dist/utils.d.ts +3 -0
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js +36 -0
  30. package/dist/utils.js.map +1 -1
  31. package/dist/wait-for-page-load.d.ts +16 -0
  32. package/dist/wait-for-page-load.d.ts.map +1 -0
  33. package/dist/wait-for-page-load.js +126 -0
  34. package/dist/wait-for-page-load.js.map +1 -0
  35. package/package.json +16 -12
  36. package/src/cdp-session.ts +156 -0
  37. package/src/cdp-types.ts +6 -0
  38. package/src/create-logger.ts +56 -0
  39. package/src/debugger.md +453 -0
  40. package/src/extension/cdp-relay.ts +32 -14
  41. package/src/mcp.test.ts +795 -189
  42. package/src/mcp.ts +101 -47
  43. package/src/prompt.md +38 -8
  44. package/src/snapshots/shadcn-ui-accessibility.md +94 -91
  45. package/src/start-relay-server.ts +3 -20
  46. package/src/utils.ts +45 -0
  47. package/src/wait-for-page-load.ts +173 -0
@@ -1,6 +1,4 @@
1
1
  export declare function startServer({ port }?: {
2
2
  port?: number;
3
- }): Promise<{
4
- close(): void;
5
- }>;
3
+ }): Promise<import("./extension/cdp-relay.js").RelayServer>;
6
4
  //# sourceMappingURL=start-relay-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"start-relay-server.d.ts","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,EAAE,IAAY,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO;;GAmBzE"}
1
+ {"version":3,"file":"start-relay-server.d.ts","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"AAsBA,wBAAsB,WAAW,CAAC,EAAE,IAAY,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,2DAmBzE"}
@@ -1,20 +1,7 @@
1
1
  import { startPlayWriterCDPRelayServer } from './extension/cdp-relay.js';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import util from 'node:util';
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const logFilePath = path.join(__dirname, '..', 'relay-server.log');
2
+ import { createFileLogger } from './create-logger.js';
8
3
  process.title = 'playwriter-ws-server';
9
- fs.writeFileSync(logFilePath, '');
10
- const log = (...args) => {
11
- const message = args.map(arg => typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false })).join(' ');
12
- return fs.promises.appendFile(logFilePath, message + '\n');
13
- };
14
- const logger = {
15
- log,
16
- error: log
17
- };
4
+ const logger = createFileLogger();
18
5
  process.on('uncaughtException', async (err) => {
19
6
  await logger.error('Uncaught Exception:', err);
20
7
  process.exit(1);
@@ -29,7 +16,7 @@ process.on('exit', async (code) => {
29
16
  export async function startServer({ port = 19988 } = {}) {
30
17
  const server = await startPlayWriterCDPRelayServer({ port, logger });
31
18
  console.log('CDP Relay Server running. Press Ctrl+C to stop.');
32
- console.log('Logs are being written to:', logFilePath);
19
+ console.log('Logs are being written to:', logger.logFilePath);
33
20
  process.on('SIGINT', () => {
34
21
  console.log('\nShutting down...');
35
22
  server.close();
@@ -1 +1 @@
1
- {"version":3,"file":"start-relay-server.js","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"start-relay-server.js","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAErD,OAAO,CAAC,KAAK,GAAG,sBAAsB,CAAA;AAEtC,MAAM,MAAM,GAAG,gBAAgB,EAAE,CAAA;AAEjC,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAC5C,MAAM,MAAM,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IAChD,MAAM,MAAM,CAAC,KAAK,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;IAChC,MAAM,MAAM,CAAC,GAAG,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAGH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAE,IAAI,GAAG,KAAK,KAAwB,EAAE;IACxE,MAAM,MAAM,GAAG,MAAM,6BAA6B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;IAEpE,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAA;IAC9D,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;IAE7D,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAA;QACjC,MAAM,CAAC,KAAK,EAAE,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC;AACD,WAAW,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA"}
package/dist/utils.d.ts CHANGED
@@ -2,4 +2,7 @@ export declare function getCdpUrl({ port, host }?: {
2
2
  port?: number;
3
3
  host?: string;
4
4
  }): string;
5
+ export declare function getDataDir(): string;
6
+ export declare function ensureDataDir(): string;
7
+ export declare const LOG_FILE_PATH: string;
5
8
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,EAAE,IAAY,EAAE,IAAkB,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,UAGpG"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAKA,wBAAgB,SAAS,CAAC,EAAE,IAAY,EAAE,IAAkB,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,UAGpG;AAED,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAMtC;AAcD,eAAO,MAAM,aAAa,QAAmB,CAAA"}
package/dist/utils.js CHANGED
@@ -1,5 +1,41 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { xdgData } from 'xdg-basedir';
1
5
  export function getCdpUrl({ port = 19988, host = '127.0.0.1' } = {}) {
2
6
  const id = `${Math.random().toString(36).substring(2, 15)}_${Date.now()}`;
3
7
  return `ws://${host}:${port}/cdp/${id}`;
4
8
  }
9
+ export function getDataDir() {
10
+ const dataDir = xdgData || path.join(os.homedir(), '.local', 'share');
11
+ return path.join(dataDir, 'playwriter');
12
+ }
13
+ export function ensureDataDir() {
14
+ const dataDir = getDataDir();
15
+ if (!fs.existsSync(dataDir)) {
16
+ fs.mkdirSync(dataDir, { recursive: true });
17
+ }
18
+ return dataDir;
19
+ }
20
+ function getLogsDir() {
21
+ return path.join(getDataDir(), 'logs');
22
+ }
23
+ function getLogFilePath() {
24
+ if (process.env.PLAYWRITER_LOG_PATH) {
25
+ return process.env.PLAYWRITER_LOG_PATH;
26
+ }
27
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
28
+ return path.join(getLogsDir(), `relay-server-${timestamp}.log`);
29
+ }
30
+ export const LOG_FILE_PATH = getLogFilePath();
31
+ // export function getDidPromptReviewPath(): string {
32
+ // return path.join(getDataDir(), 'did-prompt-review')
33
+ // }
34
+ // export function hasReviewedPrompt(): boolean {
35
+ // return fs.existsSync(getDidPromptReviewPath())
36
+ // }
37
+ // export function markPromptReviewed(): void {
38
+ // ensureDataDir()
39
+ // fs.writeFileSync(getDidPromptReviewPath(), new Date().toISOString())
40
+ // }
5
41
  //# sourceMappingURL=utils.js.map
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,SAAS,CAAC,EAAE,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,WAAW,KAAuC,EAAE;IACnG,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;IACzE,OAAO,QAAQ,IAAI,IAAI,IAAI,QAAQ,EAAE,EAAE,CAAA;AACzC,CAAC"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAErC,MAAM,UAAU,SAAS,CAAC,EAAE,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,WAAW,KAAuC,EAAE;IACnG,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;IACzE,OAAO,QAAQ,IAAI,IAAI,IAAI,QAAQ,EAAE,EAAE,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;IACrE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,CAAC,CAAA;AACxC,CAAC;AAED,SAAS,cAAc;IACrB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC;QACpC,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAA;IACxC,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IAChE,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,gBAAgB,SAAS,MAAM,CAAC,CAAA;AACjE,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,EAAE,CAAA;AAE7C,qDAAqD;AACrD,wDAAwD;AACxD,IAAI;AAEJ,iDAAiD;AACjD,mDAAmD;AACnD,IAAI;AAEJ,+CAA+C;AAC/C,oBAAoB;AACpB,yEAAyE;AACzE,IAAI"}
@@ -0,0 +1,16 @@
1
+ import type { Page } from 'playwright-core';
2
+ export interface WaitForPageLoadOptions {
3
+ page: Page;
4
+ timeout?: number;
5
+ pollInterval?: number;
6
+ minWait?: number;
7
+ }
8
+ export interface WaitForPageLoadResult {
9
+ success: boolean;
10
+ readyState: string;
11
+ pendingRequests: string[];
12
+ waitTimeMs: number;
13
+ timedOut: boolean;
14
+ }
15
+ export declare function waitForPageLoad(options: WaitForPageLoadOptions): Promise<WaitForPageLoadResult>;
16
+ //# sourceMappingURL=wait-for-page-load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-for-page-load.d.ts","sourceRoot":"","sources":["../src/wait-for-page-load.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AA0C3C,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,IAAI,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmHrG"}
@@ -0,0 +1,126 @@
1
+ const FILTERED_DOMAINS = [
2
+ 'doubleclick',
3
+ 'googlesyndication',
4
+ 'googleadservices',
5
+ 'google-analytics',
6
+ 'googletagmanager',
7
+ 'facebook.net',
8
+ 'fbcdn.net',
9
+ 'twitter.com',
10
+ 'linkedin.com',
11
+ 'hotjar',
12
+ 'mixpanel',
13
+ 'segment.io',
14
+ 'segment.com',
15
+ 'newrelic',
16
+ 'datadoghq',
17
+ 'sentry.io',
18
+ 'fullstory',
19
+ 'amplitude',
20
+ 'intercom',
21
+ 'crisp.chat',
22
+ 'zdassets.com',
23
+ 'zendesk',
24
+ 'tawk.to',
25
+ 'hubspot',
26
+ 'marketo',
27
+ 'pardot',
28
+ 'optimizely',
29
+ 'crazyegg',
30
+ 'mouseflow',
31
+ 'clarity.ms',
32
+ 'bing.com/bat',
33
+ 'ads.',
34
+ 'analytics.',
35
+ 'tracking.',
36
+ 'pixel.',
37
+ ];
38
+ const FILTERED_EXTENSIONS = ['.gif', '.ico', '.cur', '.woff', '.woff2', '.ttf', '.otf', '.eot'];
39
+ export async function waitForPageLoad(options) {
40
+ const { page, timeout = 30000, pollInterval = 100, minWait = 500 } = options;
41
+ const startTime = Date.now();
42
+ let timedOut = false;
43
+ let lastReadyState = '';
44
+ let lastPendingRequests = [];
45
+ const checkPageReady = async () => {
46
+ const result = await page.evaluate(({ filteredDomains, filteredExtensions, stuckThreshold, slowResourceThreshold }) => {
47
+ const doc = globalThis.document;
48
+ const readyState = doc.readyState;
49
+ if (readyState !== 'complete') {
50
+ return { ready: false, readyState, pendingRequests: [`document.readyState: ${readyState}`] };
51
+ }
52
+ const resources = performance.getEntriesByType('resource');
53
+ const now = performance.now();
54
+ const pendingRequests = resources
55
+ .filter((r) => {
56
+ if (r.responseEnd > 0) {
57
+ return false;
58
+ }
59
+ const elapsed = now - r.startTime;
60
+ const url = r.name.toLowerCase();
61
+ if (url.startsWith('data:')) {
62
+ return false;
63
+ }
64
+ if (filteredDomains.some((domain) => url.includes(domain))) {
65
+ return false;
66
+ }
67
+ if (elapsed > stuckThreshold) {
68
+ return false;
69
+ }
70
+ if (elapsed > slowResourceThreshold && filteredExtensions.some((ext) => url.includes(ext))) {
71
+ return false;
72
+ }
73
+ return true;
74
+ })
75
+ .map((r) => r.name);
76
+ return {
77
+ ready: pendingRequests.length === 0,
78
+ readyState,
79
+ pendingRequests,
80
+ };
81
+ }, {
82
+ filteredDomains: FILTERED_DOMAINS,
83
+ filteredExtensions: FILTERED_EXTENSIONS,
84
+ stuckThreshold: 10000,
85
+ slowResourceThreshold: 3000,
86
+ });
87
+ return result;
88
+ };
89
+ await new Promise((resolve) => setTimeout(resolve, minWait));
90
+ while (Date.now() - startTime < timeout) {
91
+ try {
92
+ const { ready, readyState, pendingRequests } = await checkPageReady();
93
+ lastReadyState = readyState;
94
+ lastPendingRequests = pendingRequests;
95
+ if (ready) {
96
+ return {
97
+ success: true,
98
+ readyState,
99
+ pendingRequests: [],
100
+ waitTimeMs: Date.now() - startTime,
101
+ timedOut: false,
102
+ };
103
+ }
104
+ }
105
+ catch (e) {
106
+ console.error('[waitForPageLoad] page.evaluate failed:', e);
107
+ return {
108
+ success: false,
109
+ readyState: 'error',
110
+ pendingRequests: ['page.evaluate failed - page may have closed or navigated'],
111
+ waitTimeMs: Date.now() - startTime,
112
+ timedOut: false,
113
+ };
114
+ }
115
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
116
+ }
117
+ timedOut = true;
118
+ return {
119
+ success: false,
120
+ readyState: lastReadyState,
121
+ pendingRequests: lastPendingRequests.slice(0, 10),
122
+ waitTimeMs: Date.now() - startTime,
123
+ timedOut,
124
+ };
125
+ }
126
+ //# sourceMappingURL=wait-for-page-load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-for-page-load.js","sourceRoot":"","sources":["../src/wait-for-page-load.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "playwriter",
3
3
  "description": "",
4
- "version": "0.0.16",
4
+ "version": "0.0.21",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "repository": "https://github.com/remorses/playwriter",
9
+ "scripts": {
10
+ "build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/resource.md src/prompt.md dist/ && vite-node scripts/download-selector-generator.ts && tsc",
11
+ "prepublishOnly": "pnpm build",
12
+ "watch": "tsc -w",
13
+ "typecheck": "tsc --noEmit",
14
+ "mcp": "vite-node src/mcp.ts",
15
+ "test": "vitest run -u",
16
+ "test:watch": "vitest"
17
+ },
9
18
  "bin": "./bin.js",
10
19
  "files": [
11
20
  "dist",
@@ -21,10 +30,12 @@
21
30
  "@mizchi/selector-generator": "1.50.0-next",
22
31
  "@types/chrome": "^0.0.315",
23
32
  "@types/node": "^24.10.1",
33
+ "@types/ws": "^8.18.1",
24
34
  "@vitest/ui": "^4.0.8",
35
+ "image-size": "^2.0.2",
36
+ "mcp-extension": "workspace:*",
25
37
  "vite-node": "^5.0.0",
26
- "vitest": "^4.0.8",
27
- "mcp-extension": "0.0.52"
38
+ "vitest": "^4.0.8"
28
39
  },
29
40
  "dependencies": {
30
41
  "@hono/node-server": "^1.19.6",
@@ -38,14 +49,7 @@
38
49
  "string-dedent": "^3.0.2",
39
50
  "user-agents": "^1.1.669",
40
51
  "ws": "^8.18.3",
52
+ "xdg-basedir": "^5.1.0",
41
53
  "zod": "^3"
42
- },
43
- "scripts": {
44
- "build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/resource.md src/prompt.md dist/ && tsc",
45
- "watch": "tsc -w",
46
- "typecheck": "tsc --noEmit",
47
- "mcp": "vite-node src/mcp.ts",
48
- "test": "vitest run -u",
49
- "test:watch": "vitest"
50
54
  }
51
- }
55
+ }
@@ -0,0 +1,156 @@
1
+ import WebSocket from 'ws'
2
+ import type { Page } from 'playwright-core'
3
+ import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'
4
+ import type { CDPResponseBase, CDPEventBase } from './cdp-types.js'
5
+
6
+ interface PendingRequest {
7
+ resolve: (result: unknown) => void
8
+ reject: (error: Error) => void
9
+ }
10
+
11
+ export class CDPSession {
12
+ private ws: WebSocket
13
+ private pendingRequests = new Map<number, PendingRequest>()
14
+ private eventListeners = new Map<string, Set<(params: unknown) => void>>()
15
+ private messageId = 0
16
+ private sessionId: string | null = null
17
+
18
+ constructor(ws: WebSocket) {
19
+ this.ws = ws
20
+ this.ws.on('message', (data) => {
21
+ try {
22
+ const message = JSON.parse(data.toString()) as CDPResponseBase | CDPEventBase
23
+
24
+ if ('id' in message) {
25
+ const response = message as CDPResponseBase
26
+ const pending = this.pendingRequests.get(response.id)
27
+ if (pending) {
28
+ this.pendingRequests.delete(response.id)
29
+ if (response.error) {
30
+ pending.reject(new Error(response.error.message))
31
+ } else {
32
+ pending.resolve(response.result)
33
+ }
34
+ }
35
+ } else if ('method' in message) {
36
+ const event = message as CDPEventBase
37
+ if (event.sessionId === this.sessionId || !event.sessionId) {
38
+ const listeners = this.eventListeners.get(event.method)
39
+ if (listeners) {
40
+ for (const listener of listeners) {
41
+ listener(event.params)
42
+ }
43
+ }
44
+ }
45
+ }
46
+ } catch (e) {
47
+ console.error('[CDPSession] Message handling error:', e)
48
+ }
49
+ })
50
+ }
51
+
52
+ setSessionId(sessionId: string) {
53
+ this.sessionId = sessionId
54
+ }
55
+
56
+ send<K extends keyof ProtocolMapping.Commands>(
57
+ method: K,
58
+ params?: ProtocolMapping.Commands[K]['paramsType'][0],
59
+ ): Promise<ProtocolMapping.Commands[K]['returnType']> {
60
+ const id = ++this.messageId
61
+ const message: Record<string, unknown> = { id, method, params }
62
+ if (this.sessionId) {
63
+ message.sessionId = this.sessionId
64
+ }
65
+
66
+ return new Promise((resolve, reject) => {
67
+ const timeout = setTimeout(() => {
68
+ this.pendingRequests.delete(id)
69
+ reject(new Error(`CDP command timeout: ${method}`))
70
+ }, 30000)
71
+
72
+ this.pendingRequests.set(id, {
73
+ resolve: (result) => {
74
+ clearTimeout(timeout)
75
+ resolve(result as ProtocolMapping.Commands[K]['returnType'])
76
+ },
77
+ reject: (error) => {
78
+ clearTimeout(timeout)
79
+ reject(error)
80
+ },
81
+ })
82
+
83
+ try {
84
+ this.ws.send(JSON.stringify(message))
85
+ } catch (error) {
86
+ clearTimeout(timeout)
87
+ this.pendingRequests.delete(id)
88
+ reject(error instanceof Error ? error : new Error(String(error)))
89
+ }
90
+ })
91
+ }
92
+
93
+ on<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void) {
94
+ if (!this.eventListeners.has(event)) {
95
+ this.eventListeners.set(event, new Set())
96
+ }
97
+ this.eventListeners.get(event)!.add(callback as (params: unknown) => void)
98
+ }
99
+
100
+ off<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void) {
101
+ this.eventListeners.get(event)?.delete(callback as (params: unknown) => void)
102
+ }
103
+
104
+ detach() {
105
+ try {
106
+ for (const pending of this.pendingRequests.values()) {
107
+ pending.reject(new Error('CDPSession detached'))
108
+ }
109
+ this.pendingRequests.clear()
110
+ this.eventListeners.clear()
111
+ this.ws.close()
112
+ } catch (e) {
113
+ console.error('[CDPSession] WebSocket close error:', e)
114
+ }
115
+ }
116
+ }
117
+
118
+ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl: string }): Promise<CDPSession> {
119
+ const ws = new WebSocket(wsUrl)
120
+
121
+ await new Promise<void>((resolve, reject) => {
122
+ ws.on('open', resolve)
123
+ ws.on('error', reject)
124
+ })
125
+
126
+ const cdp = new CDPSession(ws)
127
+
128
+ const pages = page.context().pages()
129
+ const pageIndex = pages.indexOf(page)
130
+ if (pageIndex === -1) {
131
+ cdp.detach()
132
+ throw new Error('Page not found in context')
133
+ }
134
+
135
+ const { targetInfos } = await cdp.send('Target.getTargets')
136
+ const pageTargets = targetInfos.filter((t) => t.type === 'page')
137
+
138
+ if (pageIndex >= pageTargets.length) {
139
+ cdp.detach()
140
+ throw new Error(`Page index ${pageIndex} out of bounds (${pageTargets.length} targets)`)
141
+ }
142
+
143
+ const target = pageTargets[pageIndex]
144
+ if (target.url !== page.url()) {
145
+ cdp.detach()
146
+ throw new Error(`URL mismatch: page has "${page.url()}" but target has "${target.url}"`)
147
+ }
148
+
149
+ const { sessionId } = await cdp.send('Target.attachToTarget', {
150
+ targetId: target.targetId,
151
+ flatten: true,
152
+ })
153
+ cdp.setSessionId(sessionId)
154
+
155
+ return cdp
156
+ }
package/src/cdp-types.ts CHANGED
@@ -48,6 +48,12 @@ export type CDPEventBase = {
48
48
 
49
49
  export type CDPMessage = CDPCommand | CDPResponse | CDPEvent;
50
50
 
51
+ export type RelayServerEvents = {
52
+ 'cdp:command': (data: { clientId: string; command: CDPCommand }) => void
53
+ 'cdp:event': (data: { event: CDPEventBase; sessionId?: string }) => void
54
+ 'cdp:response': (data: { clientId: string; response: CDPResponseBase; command: CDPCommand }) => void
55
+ }
56
+
51
57
  export { Protocol, ProtocolMapping };
52
58
 
53
59
  // types tests. to see if types are right with some simple examples
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import util from 'node:util'
4
+ import { LOG_FILE_PATH } from './utils.js'
5
+
6
+ export type Logger = {
7
+ log(...args: unknown[]): Promise<void>
8
+ error(...args: unknown[]): Promise<void>
9
+ logFilePath: string
10
+ }
11
+
12
+ function cleanupOldLogs(logsDir: string): void {
13
+ if (!fs.existsSync(logsDir)) {
14
+ return
15
+ }
16
+ const files = fs.readdirSync(logsDir)
17
+ .filter((f) => f.startsWith('relay-server-') && f.endsWith('.log'))
18
+ .map((f) => ({
19
+ name: f,
20
+ path: path.join(logsDir, f),
21
+ mtime: fs.statSync(path.join(logsDir, f)).mtime.getTime(),
22
+ }))
23
+ .sort((a, b) => b.mtime - a.mtime)
24
+
25
+ if (files.length > 10) {
26
+ files.slice(10).forEach((f) => {
27
+ fs.unlinkSync(f.path)
28
+ })
29
+ }
30
+ }
31
+
32
+ export function createFileLogger({ logFilePath }: { logFilePath?: string } = {}): Logger {
33
+ const resolvedLogFilePath = logFilePath || LOG_FILE_PATH
34
+ const logDir = path.dirname(resolvedLogFilePath)
35
+ if (!fs.existsSync(logDir)) {
36
+ fs.mkdirSync(logDir, { recursive: true })
37
+ }
38
+ cleanupOldLogs(logDir)
39
+ fs.writeFileSync(resolvedLogFilePath, '')
40
+
41
+ let queue: Promise<void> = Promise.resolve()
42
+
43
+ const log = (...args: unknown[]): Promise<void> => {
44
+ const message = args.map(arg =>
45
+ typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false })
46
+ ).join(' ')
47
+ queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, message + '\n'))
48
+ return queue
49
+ }
50
+
51
+ return {
52
+ log,
53
+ error: log,
54
+ logFilePath: resolvedLogFilePath,
55
+ }
56
+ }