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.
- package/dist/cdp-session.d.ts +21 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +131 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +15 -0
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js.map +1 -1
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +43 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +7 -3
- package/dist/extension/cdp-relay.d.ts.map +1 -1
- package/dist/extension/cdp-relay.js +22 -12
- package/dist/extension/cdp-relay.js.map +1 -1
- package/dist/mcp.js +86 -44
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.d.ts.map +1 -1
- package/dist/mcp.test.js +669 -183
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +38 -8
- package/dist/selector-generator.js +331 -0
- package/dist/start-relay-server.d.ts +1 -3
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +3 -16
- package/dist/start-relay-server.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +36 -0
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +126 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +16 -12
- package/src/cdp-session.ts +156 -0
- package/src/cdp-types.ts +6 -0
- package/src/create-logger.ts +56 -0
- package/src/debugger.md +453 -0
- package/src/extension/cdp-relay.ts +32 -14
- package/src/mcp.test.ts +795 -189
- package/src/mcp.ts +101 -47
- package/src/prompt.md +38 -8
- package/src/snapshots/shadcn-ui-accessibility.md +94 -91
- package/src/start-relay-server.ts +3 -20
- package/src/utils.ts +45 -0
- package/src/wait-for-page-load.ts +173 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-relay-server.d.ts","sourceRoot":"","sources":["../src/start-relay-server.ts"],"names":[],"mappings":"
|
|
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
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAA;AACxE,OAAO,EAAE,
|
|
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
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"
|
|
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":"AAEA,MAAM,gBAAgB,GAAG;IACvB,aAAa;IACb,mBAAmB;IACnB,kBAAkB;IAClB,kBAAkB;IAClB,kBAAkB;IAClB,cAAc;IACd,WAAW;IACX,aAAa;IACb,cAAc;IACd,QAAQ;IACR,UAAU;IACV,YAAY;IACZ,aAAa;IACb,UAAU;IACV,WAAW;IACX,WAAW;IACX,WAAW;IACX,WAAW;IACX,UAAU;IACV,YAAY;IACZ,cAAc;IACd,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,UAAU;IACV,WAAW;IACX,YAAY;IACZ,cAAc;IACd,MAAM;IACN,YAAY;IACZ,WAAW;IACX,QAAQ;CACT,CAAA;AAED,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAiB/F,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAA+B;IACnE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,KAAK,EAAE,YAAY,GAAG,GAAG,EAAE,OAAO,GAAG,GAAG,EAAE,GAAG,OAAO,CAAA;IAE5E,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC5B,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,IAAI,cAAc,GAAG,EAAE,CAAA;IACvB,IAAI,mBAAmB,GAAa,EAAE,CAAA;IAEtC,MAAM,cAAc,GAAG,KAAK,IAAgF,EAAE;QAC5G,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAChC,CAAC,EAAE,eAAe,EAAE,kBAAkB,EAAE,cAAc,EAAE,qBAAqB,EAAE,EAI7E,EAAE;YACF,MAAM,GAAG,GAAG,UAAU,CAAC,QAAkC,CAAA;YACzD,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAA;YAEjC,IAAI,UAAU,KAAK,UAAU,EAAE,CAAC;gBAC9B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC,wBAAwB,UAAU,EAAE,CAAC,EAAE,CAAA;YAC9F,CAAC;YAED,MAAM,SAAS,GAAI,WAAmB,CAAC,gBAAgB,CAAC,UAAU,CAIhE,CAAA;YACF,MAAM,GAAG,GAAI,WAAmB,CAAC,GAAG,EAAY,CAAA;YAEhD,MAAM,eAAe,GAAG,SAAS;iBAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;gBACZ,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC;oBACtB,OAAO,KAAK,CAAA;gBACd,CAAC;gBAED,MAAM,OAAO,GAAG,GAAG,GAAG,CAAC,CAAC,SAAS,CAAA;gBACjC,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAA;gBAEhC,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC5B,OAAO,KAAK,CAAA;gBACd,CAAC;gBAED,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;oBACnE,OAAO,KAAK,CAAA;gBACd,CAAC;gBAED,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;oBAC7B,OAAO,KAAK,CAAA;gBACd,CAAC;gBAED,IAAI,OAAO,GAAG,qBAAqB,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBACnG,OAAO,KAAK,CAAA;gBACd,CAAC;gBAED,OAAO,IAAI,CAAA;YACb,CAAC,CAAC;iBACD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;YAErB,OAAO;gBACL,KAAK,EAAE,eAAe,CAAC,MAAM,KAAK,CAAC;gBACnC,UAAU;gBACV,eAAe;aAChB,CAAA;QACH,CAAC,EACD;YACE,eAAe,EAAE,gBAAgB;YACjC,kBAAkB,EAAE,mBAAmB;YACvC,cAAc,EAAE,KAAK;YACrB,qBAAqB,EAAE,IAAI;SAC5B,CACF,CAAA;QAED,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;IAE5D,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,OAAO,EAAE,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,MAAM,cAAc,EAAE,CAAA;YACrE,cAAc,GAAG,UAAU,CAAA;YAC3B,mBAAmB,GAAG,eAAe,CAAA;YAErC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,UAAU;oBACV,eAAe,EAAE,EAAE;oBACnB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;oBAClC,QAAQ,EAAE,KAAK;iBAChB,CAAA;YACH,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,CAAC,CAAC,CAAA;YAC3D,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,UAAU,EAAE,OAAO;gBACnB,eAAe,EAAE,CAAC,0DAA0D,CAAC;gBAC7E,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;gBAClC,QAAQ,EAAE,KAAK;aAChB,CAAA;QACH,CAAC;QAED,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAA;IACnE,CAAC;IAED,QAAQ,GAAG,IAAI,CAAA;IAEf,OAAO;QACL,OAAO,EAAE,KAAK;QACd,UAAU,EAAE,cAAc;QAC1B,eAAe,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QACjD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;QAClC,QAAQ;KACT,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwriter",
|
|
3
3
|
"description": "",
|
|
4
|
-
"version": "0.0.
|
|
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
|
+
}
|