abelworkflow 1.1.3 → 1.2.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.
- package/LICENSE +21 -0
- package/README.md +30 -29
- package/extensions/gpt-responses-compat.ts +213 -0
- package/extensions/k2think-compat.ts +40 -0
- package/lib/cli/args.mjs +5 -19
- package/lib/cli/main.mjs +105 -72
- package/lib/cli/prompts.mjs +9 -2
- package/lib/config/dotenv.mjs +1 -1
- package/lib/config/jsonc.mjs +1 -1
- package/lib/config/store.mjs +197 -45
- package/lib/config/toml.mjs +147 -453
- package/lib/installer/assets.mjs +67 -45
- package/lib/installer/install.mjs +202 -45
- package/lib/installer/links.mjs +124 -101
- package/lib/installer/lock.mjs +82 -0
- package/lib/installer/state.mjs +14 -21
- package/lib/paths.mjs +9 -9
- package/lib/providers/claude.mjs +58 -114
- package/lib/providers/codex.mjs +101 -198
- package/lib/providers/pi.mjs +331 -497
- package/lib/providers/skills.mjs +88 -68
- package/lib/providers/url.mjs +35 -0
- package/lib/templates/workflow/commands/abel-design.md +23 -161
- package/lib/templates/workflow/commands/abel-diagnose.md +30 -56
- package/lib/templates/workflow/commands/abel-implement.md +30 -150
- package/lib/templates/workflow/commands/abel-init.md +1 -1
- package/lib/tools/cli-installer.mjs +29 -190
- package/package.json +7 -9
- package/skills/context7-auto-research/context7-api.cjs +75 -17
- package/skills/dev-browser/SKILL.md +15 -1
- package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
- package/skills/dev-browser/dist/scripts/start.js +8 -36
- package/skills/dev-browser/dist/src/client.d.ts +3 -4
- package/skills/dev-browser/dist/src/client.js +117 -117
- package/skills/dev-browser/dist/src/entrypoint.d.ts +1 -1
- package/skills/dev-browser/dist/src/entrypoint.js +38 -31
- package/skills/dev-browser/dist/src/index.d.ts +0 -1
- package/skills/dev-browser/dist/src/index.js +0 -1
- package/skills/dev-browser/dist/src/page-api.d.ts +0 -1
- package/skills/dev-browser/dist/src/page-api.js +0 -1
- package/skills/dev-browser/dist/src/relay.d.ts +0 -1
- package/skills/dev-browser/dist/src/relay.js +53 -7
- package/skills/dev-browser/dist/src/runtime.d.ts +3 -14
- package/skills/dev-browser/dist/src/runtime.js +5 -15
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -1
- package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -1
- package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -1
- package/skills/dev-browser/dist/src/snapshot/index.js +0 -1
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -1
- package/skills/dev-browser/dist/src/snapshot/inject.js +0 -1
- package/skills/dev-browser/dist/src/standalone.d.ts +0 -1
- package/skills/dev-browser/dist/src/standalone.js +2 -3
- package/skills/dev-browser/dist/src/startup.d.ts +3 -12
- package/skills/dev-browser/dist/src/startup.js +6 -10
- package/skills/dev-browser/dist/src/target-registry.d.ts +0 -1
- package/skills/dev-browser/dist/src/target-registry.js +0 -1
- package/skills/dev-browser/dist/src/types.d.ts +0 -1
- package/skills/dev-browser/dist/src/types.js +0 -1
- package/skills/dev-browser/package-lock.json +1 -1
- package/skills/dev-browser/package.json +2 -2
- package/skills/grok-search/.env.example +1 -1
- package/skills/grok-search/SKILL.md +13 -16
- package/skills/grok-search/scripts/_dotenv.py +1 -1
- package/skills/grok-search/scripts/groksearch_cli.py +271 -195
- package/skills/time/SKILL.md +5 -2
- package/skills/time/scripts/time_cli.py +61 -22
- package/extensions/pi-gpt-responses-compat/index.ts +0 -25
- package/lib/templates/codex/config-base.toml +0 -103
- package/skills/dev-browser/dist/scripts/start.d.ts.map +0 -1
- package/skills/dev-browser/dist/scripts/start.js.map +0 -1
- package/skills/dev-browser/dist/src/client.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/client.js.map +0 -1
- package/skills/dev-browser/dist/src/entrypoint.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/entrypoint.js.map +0 -1
- package/skills/dev-browser/dist/src/index.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/index.js.map +0 -1
- package/skills/dev-browser/dist/src/page-api.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/page-api.js.map +0 -1
- package/skills/dev-browser/dist/src/relay.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/relay.js.map +0 -1
- package/skills/dev-browser/dist/src/runtime.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/runtime.js.map +0 -1
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +0 -1
- package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/snapshot/index.js.map +0 -1
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/snapshot/inject.js.map +0 -1
- package/skills/dev-browser/dist/src/standalone.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/standalone.js.map +0 -1
- package/skills/dev-browser/dist/src/startup.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/startup.js.map +0 -1
- package/skills/dev-browser/dist/src/target-registry.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/target-registry.js.map +0 -1
- package/skills/dev-browser/dist/src/types.d.ts.map +0 -1
- package/skills/dev-browser/dist/src/types.js.map +0 -1
- package/skills/grok-search/gitignore.template +0 -4
- package/skills/grok-search/scripts/groksearch_entry.py +0 -131
|
@@ -12,9 +12,10 @@ const path = require('path');
|
|
|
12
12
|
const API_BASE = 'https://context7.com/api/v2';
|
|
13
13
|
const STDIN_TIMEOUT_MS = 3000;
|
|
14
14
|
const MAX_STDIN_BYTES = 100 * 1024;
|
|
15
|
+
const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
15
16
|
|
|
16
17
|
function printJson(value) {
|
|
17
|
-
|
|
18
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function buildErrorResult(commandName, error) {
|
|
@@ -39,7 +40,7 @@ function buildErrorResult(commandName, error) {
|
|
|
39
40
|
|
|
40
41
|
function printErrorResult(commandName, error, exitCode = 1) {
|
|
41
42
|
printJson(buildErrorResult(commandName, error));
|
|
42
|
-
process.
|
|
43
|
+
process.exitCode = exitCode;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function printUsage() {
|
|
@@ -58,7 +59,7 @@ Config:
|
|
|
58
59
|
// Load API key from .env file in skill directory or from environment variable
|
|
59
60
|
function loadApiKey() {
|
|
60
61
|
// First try environment variable
|
|
61
|
-
if (process.env
|
|
62
|
+
if (Object.hasOwn(process.env, 'CONTEXT7_API_KEY')) {
|
|
62
63
|
return process.env.CONTEXT7_API_KEY;
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -108,23 +109,77 @@ function makeRequest(path, params = {}) {
|
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
const req = https.get(url, options, (res) => {
|
|
111
|
-
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let byteLength = 0;
|
|
114
|
+
let settled = false;
|
|
115
|
+
|
|
116
|
+
function cleanup() {
|
|
117
|
+
res.removeListener('data', onData);
|
|
118
|
+
res.removeListener('end', onEnd);
|
|
119
|
+
res.removeListener('aborted', onAborted);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fail(error, destroy = false) {
|
|
123
|
+
if (settled) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
settled = true;
|
|
127
|
+
cleanup();
|
|
128
|
+
if (destroy) {
|
|
129
|
+
res.destroy();
|
|
130
|
+
}
|
|
131
|
+
reject(error);
|
|
132
|
+
}
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
function onData(chunk) {
|
|
135
|
+
const buffer = Buffer.from(chunk);
|
|
136
|
+
byteLength += buffer.length;
|
|
137
|
+
if (byteLength > MAX_RESPONSE_BYTES) {
|
|
138
|
+
fail(new Error(`API response exceeded ${MAX_RESPONSE_BYTES} bytes.`), true);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
chunks.push(buffer);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onEnd() {
|
|
145
|
+
if (settled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
settled = true;
|
|
149
|
+
cleanup();
|
|
150
|
+
const data = Buffer.concat(chunks).toString('utf8');
|
|
116
151
|
|
|
117
|
-
res.on('end', () => {
|
|
118
152
|
if (res.statusCode === 200) {
|
|
153
|
+
let parsed;
|
|
119
154
|
try {
|
|
120
|
-
|
|
121
|
-
} catch (
|
|
122
|
-
|
|
155
|
+
parsed = JSON.parse(data);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
reject(new Error(`API returned invalid JSON: ${error.message}`));
|
|
158
|
+
return;
|
|
123
159
|
}
|
|
124
|
-
|
|
125
|
-
|
|
160
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
161
|
+
reject(new Error('API returned an invalid response shape: expected a JSON object or array.'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
resolve(parsed);
|
|
165
|
+
return;
|
|
126
166
|
}
|
|
127
|
-
|
|
167
|
+
|
|
168
|
+
reject(new Error(`API Error ${res.statusCode}: ${data}`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function onError(error) {
|
|
172
|
+
fail(error);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onAborted() {
|
|
176
|
+
fail(new Error('API response aborted before completion.'));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
res.on('data', onData);
|
|
180
|
+
res.on('end', onEnd);
|
|
181
|
+
res.on('error', onError);
|
|
182
|
+
res.on('aborted', onAborted);
|
|
128
183
|
});
|
|
129
184
|
|
|
130
185
|
req.setTimeout(10000, () => {
|
|
@@ -171,6 +226,7 @@ function readStdin() {
|
|
|
171
226
|
|
|
172
227
|
function fail(error) {
|
|
173
228
|
cleanup();
|
|
229
|
+
process.stdin.pause();
|
|
174
230
|
reject(error);
|
|
175
231
|
}
|
|
176
232
|
|
|
@@ -237,7 +293,8 @@ const args = process.argv.slice(3);
|
|
|
237
293
|
async function main() {
|
|
238
294
|
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
239
295
|
printUsage();
|
|
240
|
-
process.
|
|
296
|
+
process.exitCode = command ? 0 : 1;
|
|
297
|
+
return;
|
|
241
298
|
}
|
|
242
299
|
|
|
243
300
|
if (command === 'search') {
|
|
@@ -268,13 +325,14 @@ async function main() {
|
|
|
268
325
|
}
|
|
269
326
|
} else {
|
|
270
327
|
printUsage();
|
|
271
|
-
process.
|
|
328
|
+
process.exitCode = 1;
|
|
272
329
|
}
|
|
273
330
|
}
|
|
274
331
|
|
|
275
332
|
main().catch((error) => {
|
|
276
333
|
if (command === 'search' || command === 'context') {
|
|
277
334
|
printErrorResult(command, error);
|
|
335
|
+
return;
|
|
278
336
|
}
|
|
279
|
-
process.
|
|
337
|
+
process.exitCode = 1;
|
|
280
338
|
});
|
|
@@ -17,6 +17,15 @@ Browser automation that maintains page state across script executions while the
|
|
|
17
17
|
|
|
18
18
|
Two supported startup modes are available on **Linux** and **native Windows**. Run the following commands from the installed `dev-browser` skill directory. Bash, Git Bash, and WSL are not required.
|
|
19
19
|
|
|
20
|
+
Startup never installs packages or browsers and treats the skill directory as read-only. If a preflight reports missing dependencies, run its explicit setup command yourself. Typical setup commands from the installed skill directory are:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
npm ci --omit=dev
|
|
24
|
+
node "node_modules/playwright/cli.js" install chromium
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Playwright command is required only for standalone mode. Startup does not download Chromium automatically.
|
|
28
|
+
|
|
20
29
|
### Standalone Mode (Default)
|
|
21
30
|
|
|
22
31
|
Launches a new Chromium browser for fresh automation sessions.
|
|
@@ -70,10 +79,15 @@ If the extension hasn't connected yet, tell the user to launch and activate it.
|
|
|
70
79
|
|
|
71
80
|
- `standalone mode` owns a temporary Chromium profile only for the server process lifetime and cleans it on exit.
|
|
72
81
|
- `extension mode` depends on the external browser extension and the user's existing Chrome session.
|
|
82
|
+
- Extension mode rejects viewport requests with HTTP 400; viewport sizing is supported only in standalone mode.
|
|
73
83
|
- In `extension mode`, relay readiness means the server is waiting for the extension; it does not imply browser control is available until `Extension connected` appears.
|
|
74
84
|
- `standalone mode` is the default path for deterministic local automation; `extension mode` is for working inside an already-authenticated browser.
|
|
75
85
|
|
|
76
|
-
##
|
|
86
|
+
## Server Boundary
|
|
87
|
+
|
|
88
|
+
- Both server modes and their direct APIs accept only loopback hosts: `localhost`, IPv4 loopback addresses in `127.0.0.0/8`, and `::1` (bracketed IPv6 is accepted in CLI input).
|
|
89
|
+
- Wildcard, LAN, and other hosts are rejected. This loopback-only binding is the primary access boundary.
|
|
90
|
+
- Origin checks are defense-in-depth, not authentication.
|
|
77
91
|
|
|
78
92
|
- State-changing `/pages` requests accept only originless JSON clients; browser-origin and non-JSON mutations are rejected before backend calls.
|
|
79
93
|
- `/cdp` accepts only originless WebSocket clients, which is the normal behavior of the Node client. Browser pages always send an `Origin` and are rejected.
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { formatHttpUrl, parseEntrypointArgs, resolveHostForProbe } from "../src/entrypoint.js";
|
|
6
|
-
import { getInvalidRuntimeDependencies, getLockedRuntimeDependencies,
|
|
7
|
-
import {
|
|
8
|
-
const
|
|
9
|
-
const useShell = shouldUseShellForPackageCommands(process.platform);
|
|
5
|
+
import { assertRuntimeDependenciesAvailable, getInvalidRuntimeDependencies, getLockedRuntimeDependencies, isPlaywrightChromiumInstalled, isTcpPortInUse, resolveSkillDirFromEntrypoint, } from "../src/runtime.js";
|
|
6
|
+
import { assertPlaywrightChromiumAvailable, preflightStandaloneStartup, runEntrypoint, } from "../src/startup.js";
|
|
7
|
+
const skillDir = resolveSkillDirFromEntrypoint(import.meta.url);
|
|
10
8
|
const require = createRequire(import.meta.url);
|
|
11
9
|
async function main() {
|
|
12
10
|
const args = parseEntrypointArgs(process.argv.slice(2));
|
|
13
11
|
await ensureSkillDependencies();
|
|
14
12
|
await runEntrypoint(args, {
|
|
15
|
-
runtimePaths,
|
|
16
13
|
serveStandalone: async (options) => {
|
|
17
14
|
const { serve } = await import("../src/index.js");
|
|
18
15
|
return serve(options);
|
|
@@ -26,15 +23,12 @@ async function main() {
|
|
|
26
23
|
log: (line) => console.log(line),
|
|
27
24
|
ensureBrowser: async () => {
|
|
28
25
|
const { registry } = require("playwright-core/lib/server/registry/index");
|
|
29
|
-
await
|
|
26
|
+
await assertPlaywrightChromiumAvailable({
|
|
30
27
|
isInstalled: () => isPlaywrightChromiumInstalled({
|
|
31
28
|
headless: args.headless,
|
|
32
29
|
findExecutable: (name) => registry.findExecutable(name),
|
|
33
30
|
exists: existsSync,
|
|
34
31
|
}),
|
|
35
|
-
installCommand: getPlaywrightInstallCommand({ skillDir: runtimePaths.skillDir }),
|
|
36
|
-
runCommand,
|
|
37
|
-
log: (line) => console.log(line),
|
|
38
32
|
});
|
|
39
33
|
},
|
|
40
34
|
preflightStandalone: () => preflightStandaloneStartup(args, {
|
|
@@ -59,9 +53,9 @@ async function main() {
|
|
|
59
53
|
});
|
|
60
54
|
}
|
|
61
55
|
async function ensureSkillDependencies() {
|
|
62
|
-
const packageLock = JSON.parse(readFileSync(join(
|
|
56
|
+
const packageLock = JSON.parse(readFileSync(join(skillDir, "package-lock.json"), "utf8"));
|
|
63
57
|
const invalidDependencies = getInvalidRuntimeDependencies({
|
|
64
|
-
skillDir
|
|
58
|
+
skillDir,
|
|
65
59
|
lockedDependencies: getLockedRuntimeDependencies(packageLock),
|
|
66
60
|
readPackageVersion: (path) => {
|
|
67
61
|
try {
|
|
@@ -73,29 +67,8 @@ async function ensureSkillDependencies() {
|
|
|
73
67
|
}
|
|
74
68
|
},
|
|
75
69
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log("dev-browser dependencies missing or out of date. Installing local packages...");
|
|
79
|
-
const installCommand = getRuntimeDependencyInstallCommand();
|
|
80
|
-
await runCommand(installCommand.command, installCommand.args);
|
|
81
|
-
console.log("dev-browser dependencies installed.");
|
|
82
|
-
}
|
|
83
|
-
async function runCommand(command, args) {
|
|
84
|
-
await new Promise((resolve, reject) => {
|
|
85
|
-
const child = spawn(command, args, {
|
|
86
|
-
cwd: runtimePaths.skillDir,
|
|
87
|
-
stdio: "inherit",
|
|
88
|
-
shell: useShell && command === "npm",
|
|
89
|
-
windowsHide: useShell && command === "npm",
|
|
90
|
-
});
|
|
91
|
-
child.on("error", reject);
|
|
92
|
-
child.on("close", (code) => {
|
|
93
|
-
if (code === 0) {
|
|
94
|
-
resolve();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
98
|
-
});
|
|
70
|
+
assertRuntimeDependenciesAvailable({
|
|
71
|
+
invalidDependencies,
|
|
99
72
|
});
|
|
100
73
|
}
|
|
101
74
|
function registerShutdown(stop) {
|
|
@@ -115,4 +88,3 @@ main().catch((error) => {
|
|
|
115
88
|
console.error("Failed to start dev-browser:", error);
|
|
116
89
|
process.exit(1);
|
|
117
90
|
});
|
|
118
|
-
//# sourceMappingURL=start.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ElementHandle, type Page } from "playwright";
|
|
2
2
|
import type { ViewportSize } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Options for waiting for page load
|
|
@@ -29,11 +29,11 @@ export interface WaitForPageLoadResult {
|
|
|
29
29
|
timedOut: boolean;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* Wait for a page to finish loading using document.readyState and
|
|
32
|
+
* Wait for a page to finish loading using document.readyState and Playwright request events.
|
|
33
33
|
*
|
|
34
34
|
* Uses browser-use's approach of:
|
|
35
35
|
* - Checking document.readyState for 'complete'
|
|
36
|
-
* - Monitoring pending network requests via
|
|
36
|
+
* - Monitoring pending network requests via Playwright
|
|
37
37
|
* - Filtering out ads, tracking, and non-critical resources
|
|
38
38
|
* - Graceful timeout handling (continues even if timeout reached)
|
|
39
39
|
*/
|
|
@@ -90,4 +90,3 @@ interface TargetLookupBrowser<TPage> {
|
|
|
90
90
|
export declare function findPageByTargetId<TPage>(browser: TargetLookupBrowser<TPage>, targetId: string): Promise<TPage | null>;
|
|
91
91
|
export declare function connect(serverUrl?: string): Promise<DevBrowserClient>;
|
|
92
92
|
export {};
|
|
93
|
-
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1,11 +1,38 @@
|
|
|
1
|
-
import { chromium } from "playwright";
|
|
1
|
+
import { chromium, } from "playwright";
|
|
2
2
|
import { getSnapshotScript } from "./snapshot/browser-script.js";
|
|
3
|
+
const IGNORED_REQUEST_PATTERNS = [
|
|
4
|
+
"doubleclick.net",
|
|
5
|
+
"googlesyndication.com",
|
|
6
|
+
"googletagmanager.com",
|
|
7
|
+
"google-analytics.com",
|
|
8
|
+
"facebook.net",
|
|
9
|
+
"connect.facebook.net",
|
|
10
|
+
"analytics",
|
|
11
|
+
"/ads/",
|
|
12
|
+
"tracking",
|
|
13
|
+
"pixel",
|
|
14
|
+
"hotjar.com",
|
|
15
|
+
"clarity.ms",
|
|
16
|
+
"mixpanel.com",
|
|
17
|
+
"segment.com",
|
|
18
|
+
"newrelic.com",
|
|
19
|
+
"nr-data.net",
|
|
20
|
+
"/tracker/",
|
|
21
|
+
"/collector/",
|
|
22
|
+
"/beacon/",
|
|
23
|
+
"/telemetry/",
|
|
24
|
+
"/log/",
|
|
25
|
+
"/events/",
|
|
26
|
+
"/track.",
|
|
27
|
+
"/metrics/",
|
|
28
|
+
];
|
|
29
|
+
const NON_CRITICAL_RESOURCE_TYPES = ["img", "image", "icon", "font"];
|
|
3
30
|
/**
|
|
4
|
-
* Wait for a page to finish loading using document.readyState and
|
|
31
|
+
* Wait for a page to finish loading using document.readyState and Playwright request events.
|
|
5
32
|
*
|
|
6
33
|
* Uses browser-use's approach of:
|
|
7
34
|
* - Checking document.readyState for 'complete'
|
|
8
|
-
* - Monitoring pending network requests via
|
|
35
|
+
* - Monitoring pending network requests via Playwright
|
|
9
36
|
* - Filtering out ads, tracking, and non-critical resources
|
|
10
37
|
* - Graceful timeout handling (continues even if timeout reached)
|
|
11
38
|
*/
|
|
@@ -13,123 +40,96 @@ export async function waitForPageLoad(page, options = {}) {
|
|
|
13
40
|
const { timeout = 10000, pollInterval = 50, minimumWait = 100, waitForNetworkIdle = true, } = options;
|
|
14
41
|
const startTime = Date.now();
|
|
15
42
|
let lastState = null;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
timedOut: false,
|
|
35
|
-
};
|
|
43
|
+
const requests = new Map();
|
|
44
|
+
const trackRequest = (request, startedAt = Date.now()) => {
|
|
45
|
+
requests.set(request, {
|
|
46
|
+
url: request.url(),
|
|
47
|
+
resourceType: request.resourceType(),
|
|
48
|
+
startedAt,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const onRequest = (request) => trackRequest(request);
|
|
52
|
+
const onRequestDone = (request) => requests.delete(request);
|
|
53
|
+
page.on("request", onRequest);
|
|
54
|
+
page.on("requestfinished", onRequestDone);
|
|
55
|
+
page.on("requestfailed", onRequestDone);
|
|
56
|
+
try {
|
|
57
|
+
for (const request of await page.requests().catch(() => [])) {
|
|
58
|
+
const timing = request.timing();
|
|
59
|
+
if (request.failure() === null && timing.responseEnd === -1) {
|
|
60
|
+
trackRequest(request, timing.startTime);
|
|
36
61
|
}
|
|
37
62
|
}
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
// Wait minimum time first
|
|
64
|
+
if (minimumWait > 0) {
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, minimumWait));
|
|
66
|
+
}
|
|
67
|
+
// Poll until ready or timeout
|
|
68
|
+
while (Date.now() - startTime < timeout) {
|
|
69
|
+
try {
|
|
70
|
+
lastState = await getPageLoadState(page, requests);
|
|
71
|
+
// Check if document is complete
|
|
72
|
+
const documentReady = lastState.documentReadyState === "complete";
|
|
73
|
+
// Check if network is idle (no pending critical requests)
|
|
74
|
+
const networkIdle = !waitForNetworkIdle || lastState.pendingRequests.length === 0;
|
|
75
|
+
if (documentReady && networkIdle) {
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
readyState: lastState.documentReadyState,
|
|
79
|
+
pendingRequests: lastState.pendingRequests.length,
|
|
80
|
+
waitTimeMs: Date.now() - startTime,
|
|
81
|
+
timedOut: false,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Page may be navigating, continue polling
|
|
87
|
+
}
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
40
89
|
}
|
|
41
|
-
|
|
90
|
+
// Timeout reached - return current state
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
readyState: lastState?.documentReadyState ?? "unknown",
|
|
94
|
+
pendingRequests: getPendingRequests(requests).length,
|
|
95
|
+
waitTimeMs: Date.now() - startTime,
|
|
96
|
+
timedOut: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
page.off("request", onRequest);
|
|
101
|
+
page.off("requestfinished", onRequestDone);
|
|
102
|
+
page.off("requestfailed", onRequestDone);
|
|
42
103
|
}
|
|
43
|
-
// Timeout reached - return current state
|
|
44
|
-
return {
|
|
45
|
-
success: false,
|
|
46
|
-
readyState: lastState?.documentReadyState ?? "unknown",
|
|
47
|
-
pendingRequests: lastState?.pendingRequests.length ?? 0,
|
|
48
|
-
waitTimeMs: Date.now() - startTime,
|
|
49
|
-
timedOut: true,
|
|
50
|
-
};
|
|
51
104
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* Filters out ads, tracking, and non-critical resources that shouldn't block loading.
|
|
55
|
-
*/
|
|
56
|
-
async function getPageLoadState(page) {
|
|
57
|
-
const result = await page.evaluate(() => {
|
|
105
|
+
async function getPageLoadState(page, requests) {
|
|
106
|
+
const documentReadyState = await page.evaluate(() => {
|
|
58
107
|
// Access browser globals via globalThis for TypeScript compatibility
|
|
59
108
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
60
109
|
const g = globalThis;
|
|
61
110
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"facebook.net",
|
|
74
|
-
"connect.facebook.net",
|
|
75
|
-
"analytics",
|
|
76
|
-
"ads",
|
|
77
|
-
"tracking",
|
|
78
|
-
"pixel",
|
|
79
|
-
"hotjar.com",
|
|
80
|
-
"clarity.ms",
|
|
81
|
-
"mixpanel.com",
|
|
82
|
-
"segment.com",
|
|
83
|
-
"newrelic.com",
|
|
84
|
-
"nr-data.net",
|
|
85
|
-
"/tracker/",
|
|
86
|
-
"/collector/",
|
|
87
|
-
"/beacon/",
|
|
88
|
-
"/telemetry/",
|
|
89
|
-
"/log/",
|
|
90
|
-
"/events/",
|
|
91
|
-
"/track.",
|
|
92
|
-
"/metrics/",
|
|
93
|
-
];
|
|
94
|
-
// Non-critical resource types
|
|
95
|
-
const nonCriticalTypes = ["img", "image", "icon", "font"];
|
|
96
|
-
for (const entry of resources) {
|
|
97
|
-
// Resources with responseEnd === 0 are still loading
|
|
98
|
-
if (entry.responseEnd === 0) {
|
|
99
|
-
const url = entry.name;
|
|
100
|
-
// Filter out ads and tracking
|
|
101
|
-
const isAd = adPatterns.some((pattern) => url.includes(pattern));
|
|
102
|
-
if (isAd)
|
|
103
|
-
continue;
|
|
104
|
-
// Filter out data: URLs and very long URLs
|
|
105
|
-
if (url.startsWith("data:") || url.length > 500)
|
|
106
|
-
continue;
|
|
107
|
-
const loadingDuration = now - entry.startTime;
|
|
108
|
-
// Skip requests loading > 10 seconds (likely stuck/polling)
|
|
109
|
-
if (loadingDuration > 10000)
|
|
110
|
-
continue;
|
|
111
|
-
const resourceType = entry.initiatorType || "unknown";
|
|
112
|
-
// Filter out non-critical resources loading > 3 seconds
|
|
113
|
-
if (nonCriticalTypes.includes(resourceType) && loadingDuration > 3000)
|
|
114
|
-
continue;
|
|
115
|
-
// Filter out image URLs even if type is unknown
|
|
116
|
-
const isImageUrl = /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i.test(url);
|
|
117
|
-
if (isImageUrl && loadingDuration > 3000)
|
|
118
|
-
continue;
|
|
119
|
-
pending.push({
|
|
120
|
-
url,
|
|
121
|
-
loadingDurationMs: Math.round(loadingDuration),
|
|
122
|
-
resourceType,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
documentReadyState: doc.readyState,
|
|
128
|
-
documentLoading: doc.readyState !== "complete",
|
|
129
|
-
pendingRequests: pending,
|
|
130
|
-
};
|
|
111
|
+
return g.document.readyState;
|
|
112
|
+
});
|
|
113
|
+
return { documentReadyState, pendingRequests: getPendingRequests(requests) };
|
|
114
|
+
}
|
|
115
|
+
function getPendingRequests(requests) {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
return [...requests.values()].flatMap(({ url, resourceType, startedAt }) => {
|
|
118
|
+
const loadingDurationMs = now - startedAt;
|
|
119
|
+
if (shouldIgnoreRequest(url, resourceType, loadingDurationMs))
|
|
120
|
+
return [];
|
|
121
|
+
return [{ url, resourceType, loadingDurationMs }];
|
|
131
122
|
});
|
|
132
|
-
|
|
123
|
+
}
|
|
124
|
+
function shouldIgnoreRequest(url, resourceType, loadingDurationMs) {
|
|
125
|
+
if (IGNORED_REQUEST_PATTERNS.some((pattern) => url.includes(pattern)))
|
|
126
|
+
return true;
|
|
127
|
+
if (url.startsWith("data:") || url.length > 500)
|
|
128
|
+
return true;
|
|
129
|
+
if (NON_CRITICAL_RESOURCE_TYPES.includes(resourceType) && loadingDurationMs > 3000) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i.test(url) && loadingDurationMs > 3000;
|
|
133
133
|
}
|
|
134
134
|
export async function findPageByTargetId(browser, targetId) {
|
|
135
135
|
for (const context of browser.contexts()) {
|
|
@@ -155,6 +155,7 @@ export async function findPageByTargetId(browser, targetId) {
|
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
157
|
export async function connect(serverUrl = "http://localhost:9222") {
|
|
158
|
+
const normalizedServerUrl = serverUrl.replace(/\/+$/, "");
|
|
158
159
|
let browser = null;
|
|
159
160
|
let connectingPromise = null;
|
|
160
161
|
async function ensureConnected() {
|
|
@@ -169,7 +170,7 @@ export async function connect(serverUrl = "http://localhost:9222") {
|
|
|
169
170
|
// Start new connection with mutex
|
|
170
171
|
connectingPromise = (async () => {
|
|
171
172
|
try {
|
|
172
|
-
const { wsEndpoint } = await fetchServerInfo(
|
|
173
|
+
const { wsEndpoint } = await fetchServerInfo(normalizedServerUrl);
|
|
173
174
|
// Connect to the browser via CDP
|
|
174
175
|
browser = await chromium.connectOverCDP(wsEndpoint);
|
|
175
176
|
return browser;
|
|
@@ -183,7 +184,7 @@ export async function connect(serverUrl = "http://localhost:9222") {
|
|
|
183
184
|
// Helper to get a page by name (used by multiple methods)
|
|
184
185
|
async function getPage(name, options) {
|
|
185
186
|
// Request the page from server (creates if doesn't exist)
|
|
186
|
-
const res = await fetch(`${
|
|
187
|
+
const res = await fetch(`${normalizedServerUrl}/pages`, {
|
|
187
188
|
method: "POST",
|
|
188
189
|
headers: { "Content-Type": "application/json" },
|
|
189
190
|
body: JSON.stringify({ name, viewport: options?.viewport }),
|
|
@@ -204,12 +205,12 @@ export async function connect(serverUrl = "http://localhost:9222") {
|
|
|
204
205
|
return {
|
|
205
206
|
page: getPage,
|
|
206
207
|
async list() {
|
|
207
|
-
const res = await fetch(`${
|
|
208
|
+
const res = await fetch(`${normalizedServerUrl}/pages`);
|
|
208
209
|
const data = (await res.json());
|
|
209
210
|
return data.pages;
|
|
210
211
|
},
|
|
211
212
|
async close(name) {
|
|
212
|
-
const res = await fetch(`${
|
|
213
|
+
const res = await fetch(`${normalizedServerUrl}/pages/${encodeURIComponent(name)}`, {
|
|
213
214
|
method: "DELETE",
|
|
214
215
|
});
|
|
215
216
|
if (!res.ok) {
|
|
@@ -268,7 +269,7 @@ export async function connect(serverUrl = "http://localhost:9222") {
|
|
|
268
269
|
return element;
|
|
269
270
|
},
|
|
270
271
|
async getServerInfo() {
|
|
271
|
-
return fetchServerInfo(
|
|
272
|
+
return fetchServerInfo(normalizedServerUrl);
|
|
272
273
|
},
|
|
273
274
|
};
|
|
274
275
|
}
|
|
@@ -283,7 +284,7 @@ function parseServerInfo(value) {
|
|
|
283
284
|
const info = typeof value === "object" && value !== null
|
|
284
285
|
? value
|
|
285
286
|
: {};
|
|
286
|
-
const mode =
|
|
287
|
+
const mode = info.mode;
|
|
287
288
|
if (mode !== "standalone" && mode !== "extension") {
|
|
288
289
|
throw new Error(`Invalid dev-browser server mode: expected "standalone" or "extension", received ${JSON.stringify(mode)}`);
|
|
289
290
|
}
|
|
@@ -307,4 +308,3 @@ async function waitForExactPage(browser, targetId) {
|
|
|
307
308
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
308
309
|
} while (true);
|
|
309
310
|
}
|
|
310
|
-
//# sourceMappingURL=client.js.map
|
|
@@ -24,6 +24,6 @@ export declare function formatHostForUrl(host: string): string;
|
|
|
24
24
|
export declare function formatHttpUrl(host: string, port: number): string;
|
|
25
25
|
export declare function formatWsUrl(host: string, port: number, path?: string): string;
|
|
26
26
|
export declare function resolveHostForProbe(host: string): string;
|
|
27
|
+
export declare function normalizeLoopbackHost(host: string): string;
|
|
27
28
|
export declare function parseEntrypointArgs(argv: string[], env?: Record<string, string | undefined>): EntrypointArgs;
|
|
28
29
|
export declare function formatReadinessLines(info: ReadinessInfo): string[];
|
|
29
|
-
//# sourceMappingURL=entrypoint.d.ts.map
|