dev3000 0.0.173 → 0.0.175

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 (83) hide show
  1. package/README.md +0 -4
  2. package/dist/cdp-monitor.d.ts +21 -2
  3. package/dist/cdp-monitor.d.ts.map +1 -1
  4. package/dist/cdp-monitor.js +449 -107
  5. package/dist/cdp-monitor.js.map +1 -1
  6. package/dist/cli.js +193 -216
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/crawl.d.ts.map +1 -1
  9. package/dist/commands/crawl.js +4 -43
  10. package/dist/commands/crawl.js.map +1 -1
  11. package/dist/commands/errors.d.ts.map +1 -1
  12. package/dist/commands/errors.js +4 -53
  13. package/dist/commands/errors.js.map +1 -1
  14. package/dist/commands/fix.d.ts.map +1 -1
  15. package/dist/commands/fix.js +5 -74
  16. package/dist/commands/fix.js.map +1 -1
  17. package/dist/commands/logs.d.ts.map +1 -1
  18. package/dist/commands/logs.js +4 -53
  19. package/dist/commands/logs.js.map +1 -1
  20. package/dist/commands/resume.d.ts +11 -0
  21. package/dist/commands/resume.d.ts.map +1 -0
  22. package/dist/commands/resume.js +75 -0
  23. package/dist/commands/resume.js.map +1 -0
  24. package/dist/commands/skill-runner.d.ts +14 -0
  25. package/dist/commands/skill-runner.d.ts.map +1 -0
  26. package/dist/commands/skill-runner.js +494 -0
  27. package/dist/commands/skill-runner.js.map +1 -0
  28. package/dist/dev-environment.d.ts +26 -3
  29. package/dist/dev-environment.d.ts.map +1 -1
  30. package/dist/dev-environment.js +285 -118
  31. package/dist/dev-environment.js.map +1 -1
  32. package/dist/skills/d3k/internal-skill.md +145 -0
  33. package/dist/skills/index.test.ts +28 -1
  34. package/dist/skills/index.ts +58 -7
  35. package/dist/tui-interface-opentui.d.ts +2 -0
  36. package/dist/tui-interface-opentui.d.ts.map +1 -1
  37. package/dist/tui-interface-opentui.js +17 -3
  38. package/dist/tui-interface-opentui.js.map +1 -1
  39. package/dist/utils/agent-browser.d.ts.map +1 -1
  40. package/dist/utils/agent-browser.js +6 -3
  41. package/dist/utils/agent-browser.js.map +1 -1
  42. package/dist/utils/agent-detection.d.ts +1 -0
  43. package/dist/utils/agent-detection.d.ts.map +1 -1
  44. package/dist/utils/agent-detection.js +11 -0
  45. package/dist/utils/agent-detection.js.map +1 -1
  46. package/dist/utils/agent-selection.js +4 -4
  47. package/dist/utils/agent-selection.js.map +1 -1
  48. package/dist/utils/browser-command-argv.d.ts +1 -1
  49. package/dist/utils/browser-command-argv.d.ts.map +1 -1
  50. package/dist/utils/browser-command-argv.js +1 -1
  51. package/dist/utils/browser-command-argv.js.map +1 -1
  52. package/dist/utils/project-metadata.d.ts +4 -0
  53. package/dist/utils/project-metadata.d.ts.map +1 -0
  54. package/dist/utils/project-metadata.js +48 -0
  55. package/dist/utils/project-metadata.js.map +1 -0
  56. package/dist/utils/project-name.d.ts +2 -0
  57. package/dist/utils/project-name.d.ts.map +1 -1
  58. package/dist/utils/project-name.js +6 -0
  59. package/dist/utils/project-name.js.map +1 -1
  60. package/dist/utils/session.d.ts +14 -0
  61. package/dist/utils/session.d.ts.map +1 -0
  62. package/dist/utils/session.js +65 -0
  63. package/dist/utils/session.js.map +1 -0
  64. package/dist/utils/version-check.js +2 -2
  65. package/dist/utils/version-check.js.map +1 -1
  66. package/package.json +9 -21
  67. package/dist/commands/cloud-check-pr.d.ts +0 -9
  68. package/dist/commands/cloud-check-pr.d.ts.map +0 -1
  69. package/dist/commands/cloud-check-pr.js +0 -243
  70. package/dist/commands/cloud-check-pr.js.map +0 -1
  71. package/dist/commands/cloud-fix.d.ts +0 -13
  72. package/dist/commands/cloud-fix.d.ts.map +0 -1
  73. package/dist/commands/cloud-fix.js +0 -79
  74. package/dist/commands/cloud-fix.js.map +0 -1
  75. package/dist/commands/find-component.d.ts +0 -8
  76. package/dist/commands/find-component.d.ts.map +0 -1
  77. package/dist/commands/find-component.js +0 -182
  78. package/dist/commands/find-component.js.map +0 -1
  79. package/dist/skills/d3k/SKILL.md +0 -126
  80. package/dist/skills/index.d.ts +0 -46
  81. package/dist/skills/index.d.ts.map +0 -1
  82. package/dist/skills/index.js +0 -174
  83. package/dist/skills/index.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "fs";
2
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { dirname, join } from "path";
5
5
  import { fileURLToPath } from "url";
@@ -43,6 +43,190 @@ const EMBEDDED_LOADING_HTML = `<!DOCTYPE html>
43
43
  </div>
44
44
  </body>
45
45
  </html>`;
46
+ const DEFAULT_CDP_COMMAND_TIMEOUT_MS = 10000;
47
+ const DEFAULT_NAVIGATION_TIMEOUT_MS = 60000;
48
+ export const CHROME_CRASH_RESTORE_SUPPRESSION_FLAGS = [
49
+ "--disable-session-crashed-bubble",
50
+ "--disable-restore-session-state",
51
+ "--hide-crash-restore-bubble"
52
+ ];
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ function patchChromePreferences(data) {
57
+ let changed = false;
58
+ const profile = isRecord(data.profile) ? data.profile : {};
59
+ if (data.profile !== profile) {
60
+ data.profile = profile;
61
+ changed = true;
62
+ }
63
+ if (profile.exit_type !== "Normal") {
64
+ profile.exit_type = "Normal";
65
+ changed = true;
66
+ }
67
+ if (profile.exited_cleanly !== true) {
68
+ profile.exited_cleanly = true;
69
+ changed = true;
70
+ }
71
+ return changed;
72
+ }
73
+ function patchChromeLocalState(data) {
74
+ let changed = false;
75
+ if (data.exit_type === "Crashed") {
76
+ data.exit_type = "Normal";
77
+ changed = true;
78
+ }
79
+ if (data.exited_cleanly === false) {
80
+ data.exited_cleanly = true;
81
+ changed = true;
82
+ }
83
+ if (isRecord(data.profile)) {
84
+ if (data.profile.exit_type === "Crashed") {
85
+ data.profile.exit_type = "Normal";
86
+ changed = true;
87
+ }
88
+ if (data.profile.exited_cleanly === false) {
89
+ data.profile.exited_cleanly = true;
90
+ changed = true;
91
+ }
92
+ }
93
+ return changed;
94
+ }
95
+ function patchJsonFile(filePath, patch) {
96
+ if (!existsSync(filePath)) {
97
+ return false;
98
+ }
99
+ try {
100
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
101
+ if (!isRecord(data) || !patch(data)) {
102
+ return false;
103
+ }
104
+ writeFileSync(filePath, JSON.stringify(data));
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ function getChromePreferencesFiles(profileDir) {
112
+ const files = new Set([join(profileDir, "Default", "Preferences")]);
113
+ if (!existsSync(profileDir)) {
114
+ return Array.from(files);
115
+ }
116
+ try {
117
+ for (const entry of readdirSync(profileDir, { withFileTypes: true })) {
118
+ if (entry.isDirectory() && (entry.name === "Default" || entry.name.startsWith("Profile "))) {
119
+ files.add(join(profileDir, entry.name, "Preferences"));
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Ignore unreadable profile directories.
125
+ }
126
+ return Array.from(files);
127
+ }
128
+ export function resetChromeCrashRestoreState(profileDir) {
129
+ let changedFiles = 0;
130
+ for (const preferencesFile of getChromePreferencesFiles(profileDir)) {
131
+ if (patchJsonFile(preferencesFile, patchChromePreferences)) {
132
+ changedFiles++;
133
+ }
134
+ }
135
+ if (patchJsonFile(join(profileDir, "Local State"), patchChromeLocalState)) {
136
+ changedFiles++;
137
+ }
138
+ return changedFiles;
139
+ }
140
+ export function getLoadingHtmlCandidates(currentDir, execPath = process.execPath) {
141
+ const candidates = [join(currentDir, "src/loading.html"), join(currentDir, "loading.html")];
142
+ const packageRoot = dirname(dirname(execPath));
143
+ candidates.push(join(packageRoot, "src/loading.html"));
144
+ candidates.push(join(packageRoot, "loading.html"));
145
+ candidates.push(join(process.cwd(), "src/loading.html"));
146
+ return candidates;
147
+ }
148
+ function isD3kLoadingPageUrl(url) {
149
+ if (!url?.startsWith("file://")) {
150
+ return false;
151
+ }
152
+ try {
153
+ const pathname = decodeURIComponent(new URL(url).pathname);
154
+ return pathname.includes("/dev3000-loading-") && pathname.endsWith("/loading.html");
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ function matchesAppServerPort(url, appServerPort) {
161
+ if (!url || !appServerPort) {
162
+ return false;
163
+ }
164
+ try {
165
+ const parsed = new URL(url);
166
+ const hostname = parsed.hostname;
167
+ const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
168
+ return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") && port === appServerPort;
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ }
174
+ function matchesInitialAppUrl(url, initialAppUrl) {
175
+ if (!url || !initialAppUrl) {
176
+ return false;
177
+ }
178
+ try {
179
+ const actual = new URL(url);
180
+ const expected = new URL(initialAppUrl);
181
+ const actualPort = actual.port || (actual.protocol === "https:" ? "443" : "80");
182
+ const expectedPort = expected.port || (expected.protocol === "https:" ? "443" : "80");
183
+ return (actual.protocol === expected.protocol &&
184
+ actual.hostname === expected.hostname &&
185
+ actualPort === expectedPort &&
186
+ actual.pathname === expected.pathname);
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ }
192
+ function formatTargetForLog(target) {
193
+ return `${target.type || "unknown"}:${target.url || "<no-url>"}`;
194
+ }
195
+ export function selectCDPTarget(targets, options = {}) {
196
+ const debuggableTargets = targets.filter((target) => target.webSocketDebuggerUrl);
197
+ if (debuggableTargets.length === 0) {
198
+ throw new Error(`No debuggable target found in Chrome (found ${targets.length} targets)`);
199
+ }
200
+ const contextualMatches = debuggableTargets
201
+ .map((target) => ({
202
+ target,
203
+ score: (matchesInitialAppUrl(target.url, options.initialAppUrl) ? 100 : 0) +
204
+ (matchesAppServerPort(target.url, options.appServerPort) ? 70 : 0) +
205
+ (isD3kLoadingPageUrl(target.url) ? 90 : 0) +
206
+ (target.type === "page" ? 10 : 0)
207
+ }))
208
+ .sort((left, right) => right.score - left.score);
209
+ const bestContextualScore = contextualMatches[0]?.score || 0;
210
+ const minimumContextualScore = 70;
211
+ if (bestContextualScore >= minimumContextualScore) {
212
+ return contextualMatches[0].target;
213
+ }
214
+ const pageTarget = debuggableTargets.find((target) => target.type === "page");
215
+ if (!options.appServerPort && !options.initialAppUrl) {
216
+ return pageTarget || debuggableTargets[0];
217
+ }
218
+ if (debuggableTargets.length === 1 && debuggableTargets[0]?.url === "about:blank") {
219
+ return debuggableTargets[0];
220
+ }
221
+ const expectedDetails = [
222
+ options.initialAppUrl ? `url ${options.initialAppUrl}` : null,
223
+ options.appServerPort ? `port ${options.appServerPort}` : null,
224
+ "d3k loading page"
225
+ ]
226
+ .filter(Boolean)
227
+ .join(", ");
228
+ throw new Error(`CDP target mismatch on port ${options.appServerPort || "unknown"}; expected ${expectedDetails}, found ${debuggableTargets.map(formatTargetForLog).join(", ")}`);
229
+ }
46
230
  export class CDPMonitor {
47
231
  browser = null;
48
232
  connection = null;
@@ -69,7 +253,9 @@ export class CDPMonitor {
69
253
  framework; // Framework hint from project detection
70
254
  reactTrackingEnabled = false;
71
255
  lastReactSnapshotLogTime = 0;
72
- constructor(profileDir, screenshotDir, logger, debug = false, browserPath, pluginReactScan = false, appServerPort, initialAppUrl, debugPort, headless = false, framework) {
256
+ pendingCommands = new Map();
257
+ navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS;
258
+ constructor(profileDir, screenshotDir, logger, debug = false, browserPath, pluginReactScan = false, appServerPort, initialAppUrl, navigationTimeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS, debugPort, headless = false, framework) {
73
259
  this.profileDir = profileDir;
74
260
  this.screenshotDir = screenshotDir;
75
261
  this.appServerPort = appServerPort;
@@ -78,6 +264,7 @@ export class CDPMonitor {
78
264
  this.browserPath = browserPath;
79
265
  this.pluginReactScan = pluginReactScan;
80
266
  this.initialAppUrl = initialAppUrl;
267
+ this.navigationTimeoutMs = navigationTimeoutMs;
81
268
  this.headless = headless;
82
269
  this.framework = framework;
83
270
  this.reactTrackingEnabled = framework === "nextjs";
@@ -269,11 +456,7 @@ export class CDPMonitor {
269
456
  const currentDir = dirname(currentFile);
270
457
  let loadingHtml;
271
458
  try {
272
- const loadingHtmlCandidates = [
273
- join(currentDir, "src/loading.html"),
274
- join(currentDir, "loading.html"),
275
- join(process.cwd(), "src/loading.html")
276
- ];
459
+ const loadingHtmlCandidates = getLoadingHtmlCandidates(currentDir);
277
460
  const loadingHtmlPath = loadingHtmlCandidates.find((path) => existsSync(path));
278
461
  if (!loadingHtmlPath) {
279
462
  throw new Error("No loading.html found in expected locations");
@@ -347,10 +530,21 @@ export class CDPMonitor {
347
530
  */
348
531
  async killExistingChromeWithProfile() {
349
532
  try {
350
- // Find Chrome processes using this profile directory
533
+ // Build a set of PIDs that must never be killed: this Node process and
534
+ // its parent. d3k's own argv contains the profile path (via --profile-dir),
535
+ // which previously caused a substring match here and made d3k SIGTERM itself.
536
+ const selfPids = new Set();
537
+ if (typeof process.pid === "number")
538
+ selfPids.add(process.pid);
539
+ if (typeof process.ppid === "number")
540
+ selfPids.add(process.ppid);
541
+ // Find Chrome processes using this profile directory. We only match the
542
+ // canonical `--user-data-dir=<profile>` form Chrome consumes; matching the
543
+ // bare path was too loose and caught unrelated processes (including d3k itself).
351
544
  const processes = await this.listProcesses();
352
545
  const pids = processes
353
- .filter((proc) => proc.command.includes(`--user-data-dir=${this.profileDir}`) || proc.command.includes(this.profileDir))
546
+ .filter((proc) => !selfPids.has(proc.pid))
547
+ .filter((proc) => proc.command.includes(`--user-data-dir=${this.profileDir}`))
354
548
  .map((proc) => proc.pid)
355
549
  .filter((pid) => pid !== this.browser?.pid);
356
550
  for (const pid of pids) {
@@ -374,6 +568,10 @@ export class CDPMonitor {
374
568
  async launchChrome() {
375
569
  // Kill any existing Chrome using this profile to prevent CDP conflicts
376
570
  await this.killExistingChromeWithProfile();
571
+ const resetProfileFiles = resetChromeCrashRestoreState(this.profileDir);
572
+ if (resetProfileFiles > 0) {
573
+ this.debugLog(`Reset Chrome crash restore state in ${resetProfileFiles} profile file(s)`);
574
+ }
377
575
  return new Promise((resolve, reject) => {
378
576
  // Use custom browser path if provided, otherwise try different Chrome executables based on platform
379
577
  const chromeCommands = this.browserPath
@@ -383,6 +581,8 @@ export class CDPMonitor {
383
581
  "google-chrome",
384
582
  "chrome",
385
583
  "chromium",
584
+ "brave",
585
+ "brave-browser",
386
586
  "/Applications/Arc.app/Contents/MacOS/Arc",
387
587
  "/Applications/Comet.app/Contents/MacOS/Comet",
388
588
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
@@ -422,8 +622,7 @@ export class CDPMonitor {
422
622
  "--disable-sync",
423
623
  "--metrics-recording-only",
424
624
  "--disable-default-apps",
425
- "--disable-session-crashed-bubble",
426
- "--disable-restore-session-state"
625
+ ...CHROME_CRASH_RESTORE_SUPPRESSION_FLAGS
427
626
  ];
428
627
  if (shouldEnableReactDevTools && reactDevToolsExtensionPath) {
429
628
  chromeArgs.push(`--disable-extensions-except=${reactDevToolsExtensionPath}`);
@@ -442,6 +641,10 @@ export class CDPMonitor {
442
641
  chromeArgs.push("--disable-setuid-sandbox");
443
642
  chromeArgs.push("--disable-gpu");
444
643
  chromeArgs.push("--disable-dev-shm-usage");
644
+ chromeArgs.push("--disable-background-timer-throttling");
645
+ chromeArgs.push("--disable-backgrounding-occluded-windows");
646
+ chromeArgs.push("--disable-renderer-backgrounding");
647
+ chromeArgs.push("--window-size=1920,1080");
445
648
  this.debugLog("Launching Chrome in headless mode");
446
649
  }
447
650
  else {
@@ -538,15 +741,15 @@ export class CDPMonitor {
538
741
  try {
539
742
  // Get the WebSocket URL from Chrome's debug endpoint
540
743
  const targetsResponse = await fetch(`http://localhost:${this.debugPort}/json`);
541
- let targets = await targetsResponse.json();
542
- this.debugLog(`Found ${targets.length} targets: ${JSON.stringify(targets.map((t) => ({ type: t.type, url: t.url })))}`);
744
+ let targets = (await targetsResponse.json());
745
+ this.debugLog(`Found ${targets.length} targets: ${JSON.stringify(targets.map((target) => ({ type: target.type ?? "unknown", url: target.url ?? "" })))}`);
543
746
  if (targets.length === 0) {
544
747
  this.debugLog("No debuggable targets found; creating a blank page target");
545
748
  const newTargetResponse = await fetch(`http://localhost:${this.debugPort}/json/new?about:blank`, {
546
749
  method: "PUT"
547
750
  });
548
751
  if (newTargetResponse.ok) {
549
- const createdTarget = await newTargetResponse.json();
752
+ const createdTarget = (await newTargetResponse.json());
550
753
  targets = [createdTarget];
551
754
  this.debugLog(`Created page target: ${createdTarget.id || "unknown"} - ${createdTarget.url || ""}`);
552
755
  }
@@ -554,19 +757,14 @@ export class CDPMonitor {
554
757
  this.debugLog(`Failed to create page target: HTTP ${newTargetResponse.status}`);
555
758
  }
556
759
  }
557
- // Find the first page target (tab) - prefer 'page' type but accept any target with a webSocketDebuggerUrl
558
- let pageTarget = targets.find((target) => target.type === "page");
559
- // Fallback: if no 'page' type found, try to use any target with a debugger URL
560
- if (!pageTarget && targets.length > 0) {
561
- pageTarget = targets.find((target) => target.webSocketDebuggerUrl);
562
- if (pageTarget) {
563
- this.debugLog(`No 'page' type target found, using target of type '${pageTarget.type}' instead`);
564
- }
565
- }
566
- if (!pageTarget) {
567
- throw new Error(`No debuggable target found in Chrome (found ${targets.length} targets)`);
568
- }
760
+ const pageTarget = selectCDPTarget(targets, {
761
+ appServerPort: this.appServerPort,
762
+ initialAppUrl: this.initialAppUrl
763
+ });
569
764
  const wsUrl = pageTarget.webSocketDebuggerUrl;
765
+ if (!wsUrl) {
766
+ throw new Error(`Selected CDP target does not have a websocket URL: ${formatTargetForLog(pageTarget)}`);
767
+ }
570
768
  this.cdpUrl = wsUrl; // Store the CDP URL
571
769
  this.debugLog(`Found page target: ${pageTarget.title || "Unknown"} - ${pageTarget.url}`);
572
770
  this.debugLog(`Got CDP WebSocket URL: ${wsUrl}`);
@@ -599,6 +797,7 @@ export class CDPMonitor {
599
797
  });
600
798
  ws.on("close", (code, reason) => {
601
799
  this.debugLog(`WebSocket closed with code ${code}, reason: ${reason}`);
800
+ this.rejectPendingCDPCommands(new Error(`CDP connection closed before response (code=${code})`));
602
801
  if (!this.isShuttingDown) {
603
802
  this.logger("browser", `[CDP] Connection lost unexpectedly (code: ${code}, reason: ${reason})`);
604
803
  this.logger("browser", "[CDP] CDP connection lost - check for Chrome crash or server issues");
@@ -682,7 +881,7 @@ export class CDPMonitor {
682
881
  throw err;
683
882
  }
684
883
  }
685
- async sendCDPCommand(method, params = {}) {
884
+ async sendCDPCommand(method, params = {}, timeoutMs = DEFAULT_CDP_COMMAND_TIMEOUT_MS) {
686
885
  if (!this.connection) {
687
886
  throw new Error("No CDP connection available");
688
887
  }
@@ -693,42 +892,32 @@ export class CDPMonitor {
693
892
  method,
694
893
  params
695
894
  };
696
- const messageHandler = (data) => {
697
- try {
698
- const message = JSON.parse(data.toString());
699
- if (message.id === id) {
700
- this.connection?.ws.removeListener("message", messageHandler);
701
- if (message.error) {
702
- reject(new Error(message.error.message));
703
- }
704
- else {
705
- resolve(message.result);
706
- }
707
- }
708
- }
709
- catch (error) {
710
- this.connection?.ws.removeListener("message", messageHandler);
711
- reject(error);
712
- }
713
- };
714
- this.connection?.ws.on("message", messageHandler);
715
- // Command timeout
716
895
  const timeout = setTimeout(() => {
717
- this.connection?.ws.removeListener("message", messageHandler);
896
+ this.pendingCommands.delete(id);
897
+ this.debugLog(`CDP command #${id} timed out after ${timeoutMs}ms: ${method}`);
718
898
  reject(new Error(`CDP command timeout: ${method}`));
719
- }, 10000);
720
- // Clear timeout if command succeeds/fails
721
- const originalResolve = resolve;
722
- const originalReject = reject;
723
- resolve = (value) => {
724
- clearTimeout(timeout);
725
- originalResolve(value);
726
- };
727
- reject = (reason) => {
728
- clearTimeout(timeout);
729
- originalReject(reason);
730
- };
731
- this.connection?.ws.send(JSON.stringify(command));
899
+ }, timeoutMs);
900
+ this.pendingCommands.set(id, {
901
+ method,
902
+ startedAt: Date.now(),
903
+ timeout,
904
+ resolve,
905
+ reject: (error) => reject(error)
906
+ });
907
+ this.debugLog(`Sending CDP command #${id}: ${method} ${JSON.stringify(params)}`);
908
+ this.connection?.ws.send(JSON.stringify(command), (error) => {
909
+ if (!error) {
910
+ return;
911
+ }
912
+ const pending = this.pendingCommands.get(id);
913
+ if (!pending) {
914
+ return;
915
+ }
916
+ clearTimeout(pending.timeout);
917
+ this.pendingCommands.delete(id);
918
+ this.debugLog(`Failed to send CDP command #${id}: ${method}: ${String(error)}`);
919
+ reject(error instanceof Error ? error : new Error(String(error)));
920
+ });
732
921
  });
733
922
  }
734
923
  getBundleTypeLabel(bundleType) {
@@ -808,39 +997,39 @@ export class CDPMonitor {
808
997
  }
809
998
  async enableCDPDomains() {
810
999
  const domains = [
811
- "Runtime", // Console logs, exceptions
812
- "Network", // Network requests/responses
813
- "Page", // Page events, navigation
814
- "DOM", // DOM mutations
815
- "Performance", // Performance metrics
816
- "Security", // Security events
817
- "Log", // Browser console logs
818
- "Target" // Target events (window/tab creation/destruction)
1000
+ { name: "Runtime", required: true },
1001
+ { name: "Network", required: true },
1002
+ { name: "Page", required: true },
1003
+ { name: "DOM", required: true },
1004
+ { name: "Performance", required: false },
1005
+ { name: "Security", required: false },
1006
+ { name: "Log", required: true },
1007
+ { name: "Target", required: false }
819
1008
  // Note: Input domain is for dispatching events, not monitoring them - we use JS injection instead
820
1009
  ];
821
1010
  for (const domain of domains) {
822
1011
  try {
823
- this.debugLog(`Enabling CDP domain: ${domain}`);
824
- await this.sendCDPCommand(`${domain}.enable`);
825
- this.debugLog(`Successfully enabled CDP domain: ${domain}`);
1012
+ this.debugLog(`Enabling CDP domain: ${domain.name}`);
1013
+ await this.sendCDPCommand(`${domain.name}.enable`, {}, 3000);
1014
+ this.debugLog(`Successfully enabled CDP domain: ${domain.name}`);
826
1015
  if (this.debug) {
827
- this.logger("browser", `[CDP] Enabled ${domain} domain`);
1016
+ this.logger("browser", `[CDP] Enabled ${domain.name} domain`);
828
1017
  }
829
1018
  }
830
1019
  catch (error) {
831
- this.debugLog(`Failed to enable CDP domain ${domain}: ${error}`);
832
- // Only log CDP errors when debug mode is enabled
1020
+ this.debugLog(`Failed to enable CDP domain ${domain.name}: ${error}`);
1021
+ if (domain.required) {
1022
+ throw new Error(`Failed to enable required CDP domain ${domain.name}: ${String(error)}`);
1023
+ }
833
1024
  if (this.debug) {
834
- this.logger("browser", `[CDP] Failed to enable ${domain}: ${error}`);
1025
+ this.logger("browser", `[CDP] Failed to enable optional ${domain.name}: ${error}`);
835
1026
  }
836
- // Continue with other domains instead of throwing
837
1027
  }
838
1028
  }
839
- this.debugLog("Enabling runtime for console and exception capture");
840
- await this.sendCDPCommand("Runtime.enable");
1029
+ this.debugLog("Setting async call stack depth for console and exception capture");
841
1030
  await this.sendCDPCommand("Runtime.setAsyncCallStackDepth", {
842
1031
  maxDepth: 32
843
- });
1032
+ }, 3000);
844
1033
  this.debugLog("CDP domains enabled successfully");
845
1034
  // Set viewport for headless mode to ensure consistent CLS measurements
846
1035
  // Without this, headless Chrome defaults to 800x600 which can cause
@@ -859,6 +1048,18 @@ export class CDPMonitor {
859
1048
  catch (error) {
860
1049
  this.debugLog(`Failed to set viewport: ${error}`);
861
1050
  }
1051
+ try {
1052
+ await this.sendCDPCommand("Page.bringToFront", {});
1053
+ }
1054
+ catch (error) {
1055
+ this.debugLog(`Failed to bring headless page to front: ${error}`);
1056
+ }
1057
+ try {
1058
+ await this.sendCDPCommand("Emulation.setFocusEmulationEnabled", { enabled: true });
1059
+ }
1060
+ catch (error) {
1061
+ this.debugLog(`Failed to enable focus emulation: ${error}`);
1062
+ }
862
1063
  }
863
1064
  }
864
1065
  setupEventHandlers() {
@@ -1089,8 +1290,34 @@ export class CDPMonitor {
1089
1290
  onCDPEvent(method, handler) {
1090
1291
  this.eventHandlers.set(method, handler);
1091
1292
  }
1293
+ rejectPendingCDPCommands(error) {
1294
+ for (const [id, pending] of this.pendingCommands) {
1295
+ clearTimeout(pending.timeout);
1296
+ this.pendingCommands.delete(id);
1297
+ pending.reject(error);
1298
+ }
1299
+ }
1092
1300
  handleCDPMessage(message) {
1301
+ if (typeof message.id === "number") {
1302
+ const pending = this.pendingCommands.get(message.id);
1303
+ if (!pending) {
1304
+ this.debugLog(`Received CDP response for unknown command #${message.id}`);
1305
+ return;
1306
+ }
1307
+ clearTimeout(pending.timeout);
1308
+ this.pendingCommands.delete(message.id);
1309
+ const duration = Date.now() - pending.startedAt;
1310
+ if (message.error) {
1311
+ this.debugLog(`Received CDP error for #${message.id} (${pending.method}) after ${duration}ms: ${message.error.message}`);
1312
+ pending.reject(new Error(message.error.message || `CDP command failed: ${pending.method}`));
1313
+ return;
1314
+ }
1315
+ this.debugLog(`Received CDP response for #${message.id} (${pending.method}) after ${duration}ms`);
1316
+ pending.resolve(message.result || {});
1317
+ return;
1318
+ }
1093
1319
  if (message.method) {
1320
+ this.debugLog(`Received CDP event: ${message.method}`);
1094
1321
  const handler = this.eventHandlers.get(message.method);
1095
1322
  if (handler) {
1096
1323
  const event = {
@@ -1103,7 +1330,7 @@ export class CDPMonitor {
1103
1330
  }
1104
1331
  }
1105
1332
  }
1106
- async navigateToUrl(url) {
1333
+ async navigateToUrl(url, timeoutMs = this.navigationTimeoutMs) {
1107
1334
  if (!this.connection) {
1108
1335
  throw new Error("No CDP connection available");
1109
1336
  }
@@ -1113,7 +1340,7 @@ export class CDPMonitor {
1113
1340
  try {
1114
1341
  const result = await this.sendCDPCommand("Page.navigate", {
1115
1342
  url
1116
- });
1343
+ }, timeoutMs);
1117
1344
  const navigationTime = Date.now() - navigationStartTime;
1118
1345
  this.debugLog(`Navigation command sent successfully (${navigationTime}ms)`);
1119
1346
  this.debugLog(`Navigation result: ${JSON.stringify(result)}`);
@@ -1635,37 +1862,148 @@ export class CDPMonitor {
1635
1862
  this.isShuttingDown = true;
1636
1863
  this.debugLog("Shutdown signaled - reconnection attempts will be blocked");
1637
1864
  }
1865
+ async waitForBrowserExit(timeoutMs) {
1866
+ const browser = this.browser;
1867
+ if (!browser) {
1868
+ return true;
1869
+ }
1870
+ if (browser.exitCode !== null || browser.killed) {
1871
+ return true;
1872
+ }
1873
+ return await new Promise((resolve) => {
1874
+ const onExit = () => {
1875
+ clearTimeout(timer);
1876
+ resolve(true);
1877
+ };
1878
+ const timer = setTimeout(() => {
1879
+ browser.removeListener("exit", onExit);
1880
+ resolve(false);
1881
+ }, timeoutMs);
1882
+ browser.once("exit", onExit);
1883
+ });
1884
+ }
1885
+ async sendBrowserCloseCommand() {
1886
+ try {
1887
+ await this.sendCDPCommand("Browser.close", {}, 3000);
1888
+ this.debugLog("Sent Browser.close command");
1889
+ return;
1890
+ }
1891
+ catch (error) {
1892
+ this.debugLog(`Browser.close on page target failed: ${error}`);
1893
+ }
1894
+ const versionResponse = await fetch(`http://localhost:${this.debugPort}/json/version`, {
1895
+ signal: AbortSignal.timeout(1000)
1896
+ });
1897
+ if (!versionResponse.ok) {
1898
+ throw new Error(`Failed to get browser CDP endpoint: HTTP ${versionResponse.status}`);
1899
+ }
1900
+ const version = (await versionResponse.json());
1901
+ if (!version.webSocketDebuggerUrl) {
1902
+ throw new Error("Browser CDP endpoint did not include webSocketDebuggerUrl");
1903
+ }
1904
+ await new Promise((resolve, reject) => {
1905
+ const ws = new WebSocket(version.webSocketDebuggerUrl);
1906
+ let commandSent = false;
1907
+ let settled = false;
1908
+ const settle = (callback) => {
1909
+ if (settled)
1910
+ return;
1911
+ settled = true;
1912
+ clearTimeout(timeout);
1913
+ try {
1914
+ ws.close();
1915
+ }
1916
+ catch {
1917
+ // Ignore close errors.
1918
+ }
1919
+ callback();
1920
+ };
1921
+ const timeout = setTimeout(() => {
1922
+ settle(() => reject(new Error("Browser.close command timed out")));
1923
+ }, 3000);
1924
+ ws.on("open", () => {
1925
+ commandSent = true;
1926
+ ws.send(JSON.stringify({ id: 1, method: "Browser.close", params: {} }), (error) => {
1927
+ if (error) {
1928
+ settle(() => reject(error));
1929
+ }
1930
+ });
1931
+ });
1932
+ ws.on("message", (data) => {
1933
+ try {
1934
+ const message = JSON.parse(data.toString());
1935
+ if (message.id !== 1) {
1936
+ return;
1937
+ }
1938
+ const responseError = message.error;
1939
+ if (responseError) {
1940
+ settle(() => reject(new Error(responseError.message || "Browser.close failed")));
1941
+ return;
1942
+ }
1943
+ settle(resolve);
1944
+ }
1945
+ catch (error) {
1946
+ settle(() => reject(error instanceof Error ? error : new Error(String(error))));
1947
+ }
1948
+ });
1949
+ ws.on("error", (error) => {
1950
+ settle(() => reject(error));
1951
+ });
1952
+ ws.on("close", () => {
1953
+ if (commandSent) {
1954
+ settle(resolve);
1955
+ }
1956
+ else {
1957
+ settle(() => reject(new Error("Browser CDP websocket closed before Browser.close was sent")));
1958
+ }
1959
+ });
1960
+ });
1961
+ this.debugLog("Sent Browser.close command via browser CDP endpoint");
1962
+ }
1638
1963
  async shutdown() {
1639
1964
  this.isShuttingDown = true;
1640
- // Try to close the page first, then the tab
1641
- if (this.connection?.sessionId) {
1965
+ let browserClosedCleanly = false;
1966
+ // Ask the browser process to exit first so Chrome doesn't think it crashed.
1967
+ if (this.connection) {
1642
1968
  try {
1643
- // Try to close the page
1644
- await this.sendCDPCommand("Page.close");
1645
- this.debugLog("Sent Page.close command");
1646
- await new Promise((resolve) => setTimeout(resolve, 100));
1969
+ await this.sendBrowserCloseCommand();
1970
+ browserClosedCleanly = await this.waitForBrowserExit(5000);
1647
1971
  }
1648
1972
  catch (_e) {
1649
- this.debugLog("Page.close failed, trying Target.closeTarget");
1973
+ this.debugLog("Browser.close failed, trying page/tab close fallback");
1650
1974
  }
1651
- try {
1652
- // Get the list of targets to find our specific tab
1653
- const targets = (await this.sendCDPCommand("Target.getTargets"));
1654
- this.debugLog(`Found ${targets.targetInfos?.length || 0} targets`);
1655
- // Find our page target
1656
- const pageTarget = targets.targetInfos?.find((t) => t.type === "page");
1657
- if (pageTarget) {
1658
- this.debugLog(`Closing page target: ${pageTarget.targetId}`);
1659
- await this.sendCDPCommand("Target.closeTarget", {
1660
- targetId: pageTarget.targetId
1661
- });
1662
- this.debugLog("Closed Chrome tab via CDP");
1975
+ if (!browserClosedCleanly) {
1976
+ try {
1977
+ // Try to close the page
1978
+ await this.sendCDPCommand("Page.close");
1979
+ this.debugLog("Sent Page.close command");
1980
+ await new Promise((resolve) => setTimeout(resolve, 100));
1981
+ }
1982
+ catch (_e) {
1983
+ this.debugLog("Page.close failed, trying Target.closeTarget");
1984
+ }
1985
+ try {
1986
+ // Get the list of targets to find our specific tab
1987
+ const targets = (await this.sendCDPCommand("Target.getTargets"));
1988
+ this.debugLog(`Found ${targets.targetInfos?.length || 0} targets`);
1989
+ // Find our page target
1990
+ const pageTarget = targets.targetInfos?.find((t) => t.type === "page");
1991
+ if (pageTarget) {
1992
+ this.debugLog(`Closing page target: ${pageTarget.targetId}`);
1993
+ await this.sendCDPCommand("Target.closeTarget", {
1994
+ targetId: pageTarget.targetId
1995
+ });
1996
+ this.debugLog("Closed Chrome tab via CDP");
1997
+ }
1998
+ // Give it more time for the tab to close
1999
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2000
+ }
2001
+ catch (_e) {
2002
+ this.debugLog("Failed to close tab via CDP, will force close Chrome");
2003
+ }
2004
+ if (!browserClosedCleanly) {
2005
+ browserClosedCleanly = await this.waitForBrowserExit(1500);
1663
2006
  }
1664
- // Give it more time for the tab to close
1665
- await new Promise((resolve) => setTimeout(resolve, 1000));
1666
- }
1667
- catch (_e) {
1668
- this.debugLog("Failed to close tab via CDP, will force close Chrome");
1669
2007
  }
1670
2008
  }
1671
2009
  // Close CDP connection
@@ -1678,7 +2016,11 @@ export class CDPMonitor {
1678
2016
  }
1679
2017
  this.connection = null;
1680
2018
  }
1681
- // Kill only the Chrome processes for THIS instance
2019
+ if (browserClosedCleanly) {
2020
+ this.chromePids.clear();
2021
+ return;
2022
+ }
2023
+ // Kill only the Chrome processes for THIS instance as a fallback.
1682
2024
  await this.killInstanceChromeProcesses();
1683
2025
  }
1684
2026
  async killInstanceChromeProcesses() {