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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -29
  3. package/extensions/gpt-responses-compat.ts +213 -0
  4. package/extensions/k2think-compat.ts +40 -0
  5. package/lib/cli/args.mjs +5 -19
  6. package/lib/cli/main.mjs +105 -72
  7. package/lib/cli/prompts.mjs +9 -2
  8. package/lib/config/dotenv.mjs +1 -1
  9. package/lib/config/jsonc.mjs +1 -1
  10. package/lib/config/store.mjs +197 -45
  11. package/lib/config/toml.mjs +147 -453
  12. package/lib/installer/assets.mjs +67 -45
  13. package/lib/installer/install.mjs +202 -45
  14. package/lib/installer/links.mjs +124 -101
  15. package/lib/installer/lock.mjs +82 -0
  16. package/lib/installer/state.mjs +14 -21
  17. package/lib/paths.mjs +9 -9
  18. package/lib/providers/claude.mjs +58 -114
  19. package/lib/providers/codex.mjs +101 -198
  20. package/lib/providers/pi.mjs +331 -497
  21. package/lib/providers/skills.mjs +88 -68
  22. package/lib/providers/url.mjs +35 -0
  23. package/lib/templates/workflow/commands/abel-design.md +23 -161
  24. package/lib/templates/workflow/commands/abel-diagnose.md +30 -56
  25. package/lib/templates/workflow/commands/abel-implement.md +30 -150
  26. package/lib/templates/workflow/commands/abel-init.md +1 -1
  27. package/lib/tools/cli-installer.mjs +29 -190
  28. package/package.json +7 -9
  29. package/skills/context7-auto-research/context7-api.cjs +75 -17
  30. package/skills/dev-browser/SKILL.md +15 -1
  31. package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
  32. package/skills/dev-browser/dist/scripts/start.js +8 -36
  33. package/skills/dev-browser/dist/src/client.d.ts +3 -4
  34. package/skills/dev-browser/dist/src/client.js +117 -117
  35. package/skills/dev-browser/dist/src/entrypoint.d.ts +1 -1
  36. package/skills/dev-browser/dist/src/entrypoint.js +38 -31
  37. package/skills/dev-browser/dist/src/index.d.ts +0 -1
  38. package/skills/dev-browser/dist/src/index.js +0 -1
  39. package/skills/dev-browser/dist/src/page-api.d.ts +0 -1
  40. package/skills/dev-browser/dist/src/page-api.js +0 -1
  41. package/skills/dev-browser/dist/src/relay.d.ts +0 -1
  42. package/skills/dev-browser/dist/src/relay.js +53 -7
  43. package/skills/dev-browser/dist/src/runtime.d.ts +3 -14
  44. package/skills/dev-browser/dist/src/runtime.js +5 -15
  45. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -1
  46. package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -1
  47. package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -1
  48. package/skills/dev-browser/dist/src/snapshot/index.js +0 -1
  49. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -1
  50. package/skills/dev-browser/dist/src/snapshot/inject.js +0 -1
  51. package/skills/dev-browser/dist/src/standalone.d.ts +0 -1
  52. package/skills/dev-browser/dist/src/standalone.js +2 -3
  53. package/skills/dev-browser/dist/src/startup.d.ts +3 -12
  54. package/skills/dev-browser/dist/src/startup.js +6 -10
  55. package/skills/dev-browser/dist/src/target-registry.d.ts +0 -1
  56. package/skills/dev-browser/dist/src/target-registry.js +0 -1
  57. package/skills/dev-browser/dist/src/types.d.ts +0 -1
  58. package/skills/dev-browser/dist/src/types.js +0 -1
  59. package/skills/dev-browser/package-lock.json +1 -1
  60. package/skills/dev-browser/package.json +2 -2
  61. package/skills/grok-search/.env.example +1 -1
  62. package/skills/grok-search/SKILL.md +13 -16
  63. package/skills/grok-search/scripts/_dotenv.py +1 -1
  64. package/skills/grok-search/scripts/groksearch_cli.py +271 -195
  65. package/skills/time/SKILL.md +5 -2
  66. package/skills/time/scripts/time_cli.py +61 -22
  67. package/extensions/pi-gpt-responses-compat/index.ts +0 -25
  68. package/lib/templates/codex/config-base.toml +0 -103
  69. package/skills/dev-browser/dist/scripts/start.d.ts.map +0 -1
  70. package/skills/dev-browser/dist/scripts/start.js.map +0 -1
  71. package/skills/dev-browser/dist/src/client.d.ts.map +0 -1
  72. package/skills/dev-browser/dist/src/client.js.map +0 -1
  73. package/skills/dev-browser/dist/src/entrypoint.d.ts.map +0 -1
  74. package/skills/dev-browser/dist/src/entrypoint.js.map +0 -1
  75. package/skills/dev-browser/dist/src/index.d.ts.map +0 -1
  76. package/skills/dev-browser/dist/src/index.js.map +0 -1
  77. package/skills/dev-browser/dist/src/page-api.d.ts.map +0 -1
  78. package/skills/dev-browser/dist/src/page-api.js.map +0 -1
  79. package/skills/dev-browser/dist/src/relay.d.ts.map +0 -1
  80. package/skills/dev-browser/dist/src/relay.js.map +0 -1
  81. package/skills/dev-browser/dist/src/runtime.d.ts.map +0 -1
  82. package/skills/dev-browser/dist/src/runtime.js.map +0 -1
  83. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +0 -1
  84. package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +0 -1
  85. package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +0 -1
  86. package/skills/dev-browser/dist/src/snapshot/index.js.map +0 -1
  87. package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +0 -1
  88. package/skills/dev-browser/dist/src/snapshot/inject.js.map +0 -1
  89. package/skills/dev-browser/dist/src/standalone.d.ts.map +0 -1
  90. package/skills/dev-browser/dist/src/standalone.js.map +0 -1
  91. package/skills/dev-browser/dist/src/startup.d.ts.map +0 -1
  92. package/skills/dev-browser/dist/src/startup.js.map +0 -1
  93. package/skills/dev-browser/dist/src/target-registry.d.ts.map +0 -1
  94. package/skills/dev-browser/dist/src/target-registry.js.map +0 -1
  95. package/skills/dev-browser/dist/src/types.d.ts.map +0 -1
  96. package/skills/dev-browser/dist/src/types.js.map +0 -1
  97. package/skills/grok-search/gitignore.template +0 -4
  98. 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
- console.log(JSON.stringify(value, null, 2));
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.exit(exitCode);
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.CONTEXT7_API_KEY) {
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
- let data = '';
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
- res.on('data', (chunk) => {
114
- data += chunk;
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
- resolve(JSON.parse(data));
121
- } catch (e) {
122
- resolve(data);
155
+ parsed = JSON.parse(data);
156
+ } catch (error) {
157
+ reject(new Error(`API returned invalid JSON: ${error.message}`));
158
+ return;
123
159
  }
124
- } else {
125
- reject(new Error(`API Error ${res.statusCode}: ${data}`));
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.exit(command ? 0 : 1);
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.exit(1);
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.exit(1);
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
- ## Relay Origin Boundary
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,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=start.d.ts.map
@@ -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, getPlaywrightInstallCommand, getRuntimeDependencyInstallCommand, isPlaywrightChromiumInstalled, isTcpPortInUse, resolveSkillDirFromEntrypoint, resolveRuntimePaths, shouldUseShellForPackageCommands, } from "../src/runtime.js";
7
- import { ensurePlaywrightChromium, preflightStandaloneStartup, runEntrypoint, } from "../src/startup.js";
8
- const runtimePaths = resolveRuntimePaths(resolveSkillDirFromEntrypoint(import.meta.url));
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 ensurePlaywrightChromium({
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(runtimePaths.skillDir, "package-lock.json"), "utf8"));
56
+ const packageLock = JSON.parse(readFileSync(join(skillDir, "package-lock.json"), "utf8"));
63
57
  const invalidDependencies = getInvalidRuntimeDependencies({
64
- skillDir: runtimePaths.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
- if (invalidDependencies.length === 0)
77
- return;
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 Page, type ElementHandle } from "playwright";
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 performance API.
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 Performance API
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 performance API.
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 Performance API
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
- // Wait minimum time first
17
- if (minimumWait > 0) {
18
- await new Promise((resolve) => setTimeout(resolve, minimumWait));
19
- }
20
- // Poll until ready or timeout
21
- while (Date.now() - startTime < timeout) {
22
- try {
23
- lastState = await getPageLoadState(page);
24
- // Check if document is complete
25
- const documentReady = lastState.documentReadyState === "complete";
26
- // Check if network is idle (no pending critical requests)
27
- const networkIdle = !waitForNetworkIdle || lastState.pendingRequests.length === 0;
28
- if (documentReady && networkIdle) {
29
- return {
30
- success: true,
31
- readyState: lastState.documentReadyState,
32
- pendingRequests: lastState.pendingRequests.length,
33
- waitTimeMs: Date.now() - startTime,
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
- catch {
39
- // Page may be navigating, continue polling
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
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
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
- * Get the current page load state including document ready state and pending requests.
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
- const perf = g.performance;
63
- const doc = g.document;
64
- const now = perf.now();
65
- const resources = perf.getEntriesByType("resource");
66
- const pending = [];
67
- // Common ad/tracking domains and patterns to filter out
68
- const adPatterns = [
69
- "doubleclick.net",
70
- "googlesyndication.com",
71
- "googletagmanager.com",
72
- "google-analytics.com",
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
- return result;
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(serverUrl);
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(`${serverUrl}/pages`, {
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(`${serverUrl}/pages`);
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(`${serverUrl}/pages/${encodeURIComponent(name)}`, {
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(serverUrl);
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 = Object.hasOwn(info, "mode") ? info.mode : "standalone";
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