devharness 0.8.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 +185 -0
- package/build/atomic-write.d.ts +28 -0
- package/build/atomic-write.d.ts.map +1 -0
- package/build/atomic-write.js +126 -0
- package/build/atomic-write.js.map +1 -0
- package/build/cdp-manager.d.ts +524 -0
- package/build/cdp-manager.d.ts.map +1 -0
- package/build/cdp-manager.js +1828 -0
- package/build/cdp-manager.js.map +1 -0
- package/build/cdp-manager.test.d.ts +6 -0
- package/build/cdp-manager.test.d.ts.map +1 -0
- package/build/cdp-manager.test.js +546 -0
- package/build/cdp-manager.test.js.map +1 -0
- package/build/chrome-launcher.d.ts +400 -0
- package/build/chrome-launcher.d.ts.map +1 -0
- package/build/chrome-launcher.js +1071 -0
- package/build/chrome-launcher.js.map +1 -0
- package/build/chrome-launcher.test.d.ts +11 -0
- package/build/chrome-launcher.test.d.ts.map +1 -0
- package/build/chrome-launcher.test.js +204 -0
- package/build/chrome-launcher.test.js.map +1 -0
- package/build/clickable-cache.d.ts +73 -0
- package/build/clickable-cache.d.ts.map +1 -0
- package/build/clickable-cache.js +90 -0
- package/build/clickable-cache.js.map +1 -0
- package/build/command-recorder.d.ts +401 -0
- package/build/command-recorder.d.ts.map +1 -0
- package/build/command-recorder.js +701 -0
- package/build/command-recorder.js.map +1 -0
- package/build/config.d.ts +390 -0
- package/build/config.d.ts.map +1 -0
- package/build/config.js +826 -0
- package/build/config.js.map +1 -0
- package/build/config.test.d.ts +2 -0
- package/build/config.test.d.ts.map +1 -0
- package/build/config.test.js +112 -0
- package/build/config.test.js.map +1 -0
- package/build/connection-manager.d.ts +159 -0
- package/build/connection-manager.d.ts.map +1 -0
- package/build/connection-manager.js +432 -0
- package/build/connection-manager.js.map +1 -0
- package/build/connection-manager.test.d.ts +11 -0
- package/build/connection-manager.test.d.ts.map +1 -0
- package/build/connection-manager.test.js +114 -0
- package/build/connection-manager.test.js.map +1 -0
- package/build/console-monitor.d.ts +150 -0
- package/build/console-monitor.d.ts.map +1 -0
- package/build/console-monitor.js +454 -0
- package/build/console-monitor.js.map +1 -0
- package/build/dashboard/bundle.js +1 -0
- package/build/dashboard/dashboard-client.d.ts +42 -0
- package/build/dashboard/dashboard-client.d.ts.map +1 -0
- package/build/dashboard/dashboard-client.js +184 -0
- package/build/dashboard/dashboard-client.js.map +1 -0
- package/build/dashboard/dashboard-hub.d.ts +50 -0
- package/build/dashboard/dashboard-hub.d.ts.map +1 -0
- package/build/dashboard/dashboard-hub.js +654 -0
- package/build/dashboard/dashboard-hub.js.map +1 -0
- package/build/dashboard/hub-lock.d.ts +16 -0
- package/build/dashboard/hub-lock.d.ts.map +1 -0
- package/build/dashboard/hub-lock.js +77 -0
- package/build/dashboard/hub-lock.js.map +1 -0
- package/build/dashboard/index.d.ts +25 -0
- package/build/dashboard/index.d.ts.map +1 -0
- package/build/dashboard/index.js +77 -0
- package/build/dashboard/index.js.map +1 -0
- package/build/dashboard/types.d.ts +83 -0
- package/build/dashboard/types.d.ts.map +1 -0
- package/build/dashboard/types.js +6 -0
- package/build/dashboard/types.js.map +1 -0
- package/build/debug-logger.d.ts +85 -0
- package/build/debug-logger.d.ts.map +1 -0
- package/build/debug-logger.js +195 -0
- package/build/debug-logger.js.map +1 -0
- package/build/debugger-aware-wrapper.d.ts +37 -0
- package/build/debugger-aware-wrapper.d.ts.map +1 -0
- package/build/debugger-aware-wrapper.js +128 -0
- package/build/debugger-aware-wrapper.js.map +1 -0
- package/build/dom-change-monitor.d.ts +92 -0
- package/build/dom-change-monitor.d.ts.map +1 -0
- package/build/dom-change-monitor.js +437 -0
- package/build/dom-change-monitor.js.map +1 -0
- package/build/element-collector.d.ts +16 -0
- package/build/element-collector.d.ts.map +1 -0
- package/build/element-collector.js +207 -0
- package/build/element-collector.js.map +1 -0
- package/build/error-helpers.d.ts +43 -0
- package/build/error-helpers.d.ts.map +1 -0
- package/build/error-helpers.js +143 -0
- package/build/error-helpers.js.map +1 -0
- package/build/formatters/console-formatter.d.ts +160 -0
- package/build/formatters/console-formatter.d.ts.map +1 -0
- package/build/formatters/console-formatter.js +338 -0
- package/build/formatters/console-formatter.js.map +1 -0
- package/build/helpers/parser-plugins.d.ts +44 -0
- package/build/helpers/parser-plugins.d.ts.map +1 -0
- package/build/helpers/parser-plugins.js +102 -0
- package/build/helpers/parser-plugins.js.map +1 -0
- package/build/helpers/paths.d.ts +79 -0
- package/build/helpers/paths.d.ts.map +1 -0
- package/build/helpers/paths.js +168 -0
- package/build/helpers/paths.js.map +1 -0
- package/build/helpers/process-liveness.d.ts +24 -0
- package/build/helpers/process-liveness.d.ts.map +1 -0
- package/build/helpers/process-liveness.js +36 -0
- package/build/helpers/process-liveness.js.map +1 -0
- package/build/helpers/process-liveness.test.d.ts +2 -0
- package/build/helpers/process-liveness.test.d.ts.map +1 -0
- package/build/helpers/process-liveness.test.js +40 -0
- package/build/helpers/process-liveness.test.js.map +1 -0
- package/build/helpers/sequence-tree.d.ts +27 -0
- package/build/helpers/sequence-tree.d.ts.map +1 -0
- package/build/helpers/sequence-tree.js +80 -0
- package/build/helpers/sequence-tree.js.map +1 -0
- package/build/helpers/sequence-tree.test.d.ts +2 -0
- package/build/helpers/sequence-tree.test.d.ts.map +1 -0
- package/build/helpers/sequence-tree.test.js +122 -0
- package/build/helpers/sequence-tree.test.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2007 -0
- package/build/index.js.map +1 -0
- package/build/interaction-recorder.d.ts +214 -0
- package/build/interaction-recorder.d.ts.map +1 -0
- package/build/interaction-recorder.js +2007 -0
- package/build/interaction-recorder.js.map +1 -0
- package/build/issue-tracker.d.ts +107 -0
- package/build/issue-tracker.d.ts.map +1 -0
- package/build/issue-tracker.js +582 -0
- package/build/issue-tracker.js.map +1 -0
- package/build/issue-tracker.test.d.ts +2 -0
- package/build/issue-tracker.test.d.ts.map +1 -0
- package/build/issue-tracker.test.js +88 -0
- package/build/issue-tracker.test.js.map +1 -0
- package/build/launch-chrome-port.test.d.ts +14 -0
- package/build/launch-chrome-port.test.d.ts.map +1 -0
- package/build/launch-chrome-port.test.js +97 -0
- package/build/launch-chrome-port.test.js.map +1 -0
- package/build/log-processor/orchestrator.d.ts +32 -0
- package/build/log-processor/orchestrator.d.ts.map +1 -0
- package/build/log-processor/orchestrator.js +27 -0
- package/build/log-processor/orchestrator.js.map +1 -0
- package/build/logpoint-execution-tracker.d.ts +57 -0
- package/build/logpoint-execution-tracker.d.ts.map +1 -0
- package/build/logpoint-execution-tracker.js +112 -0
- package/build/logpoint-execution-tracker.js.map +1 -0
- package/build/mcp-supervisor.d.ts +3 -0
- package/build/mcp-supervisor.d.ts.map +1 -0
- package/build/mcp-supervisor.js +211 -0
- package/build/mcp-supervisor.js.map +1 -0
- package/build/messages.d.ts +165 -0
- package/build/messages.d.ts.map +1 -0
- package/build/messages.js +574 -0
- package/build/messages.js.map +1 -0
- package/build/network-monitor.d.ts +172 -0
- package/build/network-monitor.d.ts.map +1 -0
- package/build/network-monitor.js +350 -0
- package/build/network-monitor.js.map +1 -0
- package/build/overlays.d.ts +49 -0
- package/build/overlays.d.ts.map +1 -0
- package/build/overlays.js +409 -0
- package/build/overlays.js.map +1 -0
- package/build/persistent-profiles.test.d.ts +15 -0
- package/build/persistent-profiles.test.d.ts.map +1 -0
- package/build/persistent-profiles.test.js +470 -0
- package/build/persistent-profiles.test.js.map +1 -0
- package/build/port-reserver.d.ts +32 -0
- package/build/port-reserver.d.ts.map +1 -0
- package/build/port-reserver.js +118 -0
- package/build/port-reserver.js.map +1 -0
- package/build/puppeteer-manager.d.ts +47 -0
- package/build/puppeteer-manager.d.ts.map +1 -0
- package/build/puppeteer-manager.js +116 -0
- package/build/puppeteer-manager.js.map +1 -0
- package/build/recording-coverage.test.d.ts +12 -0
- package/build/recording-coverage.test.d.ts.map +1 -0
- package/build/recording-coverage.test.js +72 -0
- package/build/recording-coverage.test.js.map +1 -0
- package/build/reference-validator.d.ts +47 -0
- package/build/reference-validator.d.ts.map +1 -0
- package/build/reference-validator.js +86 -0
- package/build/reference-validator.js.map +1 -0
- package/build/reference-validator.test.d.ts +5 -0
- package/build/reference-validator.test.d.ts.map +1 -0
- package/build/reference-validator.test.js +243 -0
- package/build/reference-validator.test.js.map +1 -0
- package/build/replay-cursor.d.ts +27 -0
- package/build/replay-cursor.d.ts.map +1 -0
- package/build/replay-cursor.js +240 -0
- package/build/replay-cursor.js.map +1 -0
- package/build/runners/docker-compose-runner.d.ts +69 -0
- package/build/runners/docker-compose-runner.d.ts.map +1 -0
- package/build/runners/docker-compose-runner.js +438 -0
- package/build/runners/docker-compose-runner.js.map +1 -0
- package/build/runners/docker-runner.d.ts +65 -0
- package/build/runners/docker-runner.d.ts.map +1 -0
- package/build/runners/docker-runner.js +414 -0
- package/build/runners/docker-runner.js.map +1 -0
- package/build/runners/index.d.ts +19 -0
- package/build/runners/index.d.ts.map +1 -0
- package/build/runners/index.js +35 -0
- package/build/runners/index.js.map +1 -0
- package/build/runners/native-runner.d.ts +95 -0
- package/build/runners/native-runner.d.ts.map +1 -0
- package/build/runners/native-runner.js +370 -0
- package/build/runners/native-runner.js.map +1 -0
- package/build/runners/types.d.ts +147 -0
- package/build/runners/types.d.ts.map +1 -0
- package/build/runners/types.js +25 -0
- package/build/runners/types.js.map +1 -0
- package/build/self-restart.d.ts +12 -0
- package/build/self-restart.d.ts.map +1 -0
- package/build/self-restart.js +41 -0
- package/build/self-restart.js.map +1 -0
- package/build/self-restart.test.d.ts +2 -0
- package/build/self-restart.test.d.ts.map +1 -0
- package/build/self-restart.test.js +34 -0
- package/build/self-restart.test.js.map +1 -0
- package/build/server-claims.d.ts +161 -0
- package/build/server-claims.d.ts.map +1 -0
- package/build/server-claims.js +382 -0
- package/build/server-claims.js.map +1 -0
- package/build/server-claims.test.d.ts +2 -0
- package/build/server-claims.test.d.ts.map +1 -0
- package/build/server-claims.test.js +197 -0
- package/build/server-claims.test.js.map +1 -0
- package/build/server-manager.d.ts +438 -0
- package/build/server-manager.d.ts.map +1 -0
- package/build/server-manager.js +1674 -0
- package/build/server-manager.js.map +1 -0
- package/build/server-manager.test.d.ts +17 -0
- package/build/server-manager.test.d.ts.map +1 -0
- package/build/server-manager.test.js +122 -0
- package/build/server-manager.test.js.map +1 -0
- package/build/server-ownership.test.d.ts +2 -0
- package/build/server-ownership.test.d.ts.map +1 -0
- package/build/server-ownership.test.js +238 -0
- package/build/server-ownership.test.js.map +1 -0
- package/build/server-watcher.d.ts +21 -0
- package/build/server-watcher.d.ts.map +1 -0
- package/build/server-watcher.js +105 -0
- package/build/server-watcher.js.map +1 -0
- package/build/session-detector.d.ts +47 -0
- package/build/session-detector.d.ts.map +1 -0
- package/build/session-detector.js +229 -0
- package/build/session-detector.js.map +1 -0
- package/build/session-detector.test.d.ts +9 -0
- package/build/session-detector.test.d.ts.map +1 -0
- package/build/session-detector.test.js +45 -0
- package/build/session-detector.test.js.map +1 -0
- package/build/sourcemap-handler.d.ts +114 -0
- package/build/sourcemap-handler.d.ts.map +1 -0
- package/build/sourcemap-handler.js +479 -0
- package/build/sourcemap-handler.js.map +1 -0
- package/build/supervisor/child-manager.d.ts +43 -0
- package/build/supervisor/child-manager.d.ts.map +1 -0
- package/build/supervisor/child-manager.js +148 -0
- package/build/supervisor/child-manager.js.map +1 -0
- package/build/supervisor/child-manager.test.d.ts +2 -0
- package/build/supervisor/child-manager.test.d.ts.map +1 -0
- package/build/supervisor/child-manager.test.js +107 -0
- package/build/supervisor/child-manager.test.js.map +1 -0
- package/build/supervisor/client-watcher.d.ts +48 -0
- package/build/supervisor/client-watcher.d.ts.map +1 -0
- package/build/supervisor/client-watcher.js +186 -0
- package/build/supervisor/client-watcher.js.map +1 -0
- package/build/supervisor/client-watcher.test.d.ts +2 -0
- package/build/supervisor/client-watcher.test.d.ts.map +1 -0
- package/build/supervisor/client-watcher.test.js +121 -0
- package/build/supervisor/client-watcher.test.js.map +1 -0
- package/build/supervisor/idle-config.d.ts +30 -0
- package/build/supervisor/idle-config.d.ts.map +1 -0
- package/build/supervisor/idle-config.js +76 -0
- package/build/supervisor/idle-config.js.map +1 -0
- package/build/supervisor/idle-config.test.d.ts +2 -0
- package/build/supervisor/idle-config.test.d.ts.map +1 -0
- package/build/supervisor/idle-config.test.js +119 -0
- package/build/supervisor/idle-config.test.js.map +1 -0
- package/build/supervisor/ndjson-reader.d.ts +47 -0
- package/build/supervisor/ndjson-reader.d.ts.map +1 -0
- package/build/supervisor/ndjson-reader.js +75 -0
- package/build/supervisor/ndjson-reader.js.map +1 -0
- package/build/supervisor/ndjson-reader.test.d.ts +2 -0
- package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
- package/build/supervisor/ndjson-reader.test.js +92 -0
- package/build/supervisor/ndjson-reader.test.js.map +1 -0
- package/build/supervisor/pidfile.d.ts +11 -0
- package/build/supervisor/pidfile.d.ts.map +1 -0
- package/build/supervisor/pidfile.js +29 -0
- package/build/supervisor/pidfile.js.map +1 -0
- package/build/supervisor/pidfile.test.d.ts +8 -0
- package/build/supervisor/pidfile.test.d.ts.map +1 -0
- package/build/supervisor/pidfile.test.js +46 -0
- package/build/supervisor/pidfile.test.js.map +1 -0
- package/build/supervisor/restart-coordinator.d.ts +73 -0
- package/build/supervisor/restart-coordinator.d.ts.map +1 -0
- package/build/supervisor/restart-coordinator.js +300 -0
- package/build/supervisor/restart-coordinator.js.map +1 -0
- package/build/supervisor/restart-coordinator.test.d.ts +2 -0
- package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
- package/build/supervisor/restart-coordinator.test.js +434 -0
- package/build/supervisor/restart-coordinator.test.js.map +1 -0
- package/build/test-support/fake-execute-tool-call.d.ts +11 -0
- package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
- package/build/test-support/fake-execute-tool-call.js +19 -0
- package/build/test-support/fake-execute-tool-call.js.map +1 -0
- package/build/tool-error.d.ts +14 -0
- package/build/tool-error.d.ts.map +1 -0
- package/build/tool-error.js +18 -0
- package/build/tool-error.js.map +1 -0
- package/build/tool-response.d.ts +293 -0
- package/build/tool-response.d.ts.map +1 -0
- package/build/tool-response.js +365 -0
- package/build/tool-response.js.map +1 -0
- package/build/tools/assert-tools.d.ts +62 -0
- package/build/tools/assert-tools.d.ts.map +1 -0
- package/build/tools/assert-tools.js +330 -0
- package/build/tools/assert-tools.js.map +1 -0
- package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
- package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
- package/build/tools/breakpoint-await-abort.test.js +124 -0
- package/build/tools/breakpoint-await-abort.test.js.map +1 -0
- package/build/tools/breakpoint-tools.d.ts +99 -0
- package/build/tools/breakpoint-tools.d.ts.map +1 -0
- package/build/tools/breakpoint-tools.js +1197 -0
- package/build/tools/breakpoint-tools.js.map +1 -0
- package/build/tools/config-tools.d.ts +58 -0
- package/build/tools/config-tools.d.ts.map +1 -0
- package/build/tools/config-tools.js +180 -0
- package/build/tools/config-tools.js.map +1 -0
- package/build/tools/connection-routing.test.d.ts +12 -0
- package/build/tools/connection-routing.test.d.ts.map +1 -0
- package/build/tools/connection-routing.test.js +206 -0
- package/build/tools/connection-routing.test.js.map +1 -0
- package/build/tools/console-tools.d.ts +84 -0
- package/build/tools/console-tools.d.ts.map +1 -0
- package/build/tools/console-tools.js +241 -0
- package/build/tools/console-tools.js.map +1 -0
- package/build/tools/content-tools.d.ts +74 -0
- package/build/tools/content-tools.d.ts.map +1 -0
- package/build/tools/content-tools.js +582 -0
- package/build/tools/content-tools.js.map +1 -0
- package/build/tools/dashboard-tools.d.ts +38 -0
- package/build/tools/dashboard-tools.d.ts.map +1 -0
- package/build/tools/dashboard-tools.js +123 -0
- package/build/tools/dashboard-tools.js.map +1 -0
- package/build/tools/dom-tools.d.ts +41 -0
- package/build/tools/dom-tools.d.ts.map +1 -0
- package/build/tools/dom-tools.js +277 -0
- package/build/tools/dom-tools.js.map +1 -0
- package/build/tools/download-tools.d.ts +55 -0
- package/build/tools/download-tools.d.ts.map +1 -0
- package/build/tools/download-tools.js +280 -0
- package/build/tools/download-tools.js.map +1 -0
- package/build/tools/execution-tools.d.ts +40 -0
- package/build/tools/execution-tools.d.ts.map +1 -0
- package/build/tools/execution-tools.js +121 -0
- package/build/tools/execution-tools.js.map +1 -0
- package/build/tools/input-abort.test.d.ts +2 -0
- package/build/tools/input-abort.test.d.ts.map +1 -0
- package/build/tools/input-abort.test.js +227 -0
- package/build/tools/input-abort.test.js.map +1 -0
- package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
- package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
- package/build/tools/input-ambiguous-selector.test.js +34 -0
- package/build/tools/input-ambiguous-selector.test.js.map +1 -0
- package/build/tools/input-tools.d.ts +168 -0
- package/build/tools/input-tools.d.ts.map +1 -0
- package/build/tools/input-tools.js +1397 -0
- package/build/tools/input-tools.js.map +1 -0
- package/build/tools/inspection-tools.d.ts +112 -0
- package/build/tools/inspection-tools.d.ts.map +1 -0
- package/build/tools/inspection-tools.js +637 -0
- package/build/tools/inspection-tools.js.map +1 -0
- package/build/tools/interpolation.d.ts +30 -0
- package/build/tools/interpolation.d.ts.map +1 -0
- package/build/tools/interpolation.js +143 -0
- package/build/tools/interpolation.js.map +1 -0
- package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
- package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
- package/build/tools/issues-abort-hygiene.test.js +79 -0
- package/build/tools/issues-abort-hygiene.test.js.map +1 -0
- package/build/tools/issues-tools.d.ts +98 -0
- package/build/tools/issues-tools.d.ts.map +1 -0
- package/build/tools/issues-tools.js +821 -0
- package/build/tools/issues-tools.js.map +1 -0
- package/build/tools/issues-tools.test.d.ts +8 -0
- package/build/tools/issues-tools.test.d.ts.map +1 -0
- package/build/tools/issues-tools.test.js +184 -0
- package/build/tools/issues-tools.test.js.map +1 -0
- package/build/tools/modal-tools.d.ts +73 -0
- package/build/tools/modal-tools.d.ts.map +1 -0
- package/build/tools/modal-tools.js +161 -0
- package/build/tools/modal-tools.js.map +1 -0
- package/build/tools/network-tools.d.ts +72 -0
- package/build/tools/network-tools.d.ts.map +1 -0
- package/build/tools/network-tools.js +432 -0
- package/build/tools/network-tools.js.map +1 -0
- package/build/tools/page-tools.d.ts +78 -0
- package/build/tools/page-tools.d.ts.map +1 -0
- package/build/tools/page-tools.js +255 -0
- package/build/tools/page-tools.js.map +1 -0
- package/build/tools/plugin-tools.d.ts +16 -0
- package/build/tools/plugin-tools.d.ts.map +1 -0
- package/build/tools/plugin-tools.js +17 -0
- package/build/tools/plugin-tools.js.map +1 -0
- package/build/tools/replay-abort.test.d.ts +2 -0
- package/build/tools/replay-abort.test.d.ts.map +1 -0
- package/build/tools/replay-abort.test.js +259 -0
- package/build/tools/replay-abort.test.js.map +1 -0
- package/build/tools/replay-add-conditional.test.d.ts +2 -0
- package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
- package/build/tools/replay-add-conditional.test.js +205 -0
- package/build/tools/replay-add-conditional.test.js.map +1 -0
- package/build/tools/replay-background-run.test.d.ts +2 -0
- package/build/tools/replay-background-run.test.d.ts.map +1 -0
- package/build/tools/replay-background-run.test.js +176 -0
- package/build/tools/replay-background-run.test.js.map +1 -0
- package/build/tools/replay-capture-variables.test.d.ts +2 -0
- package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
- package/build/tools/replay-capture-variables.test.js +331 -0
- package/build/tools/replay-capture-variables.test.js.map +1 -0
- package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
- package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
- package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
- package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
- package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
- package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
- package/build/tools/replay-conditional-timeout.test.js +126 -0
- package/build/tools/replay-conditional-timeout.test.js.map +1 -0
- package/build/tools/replay-declare.test.d.ts +2 -0
- package/build/tools/replay-declare.test.d.ts.map +1 -0
- package/build/tools/replay-declare.test.js +106 -0
- package/build/tools/replay-declare.test.js.map +1 -0
- package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
- package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
- package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
- package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
- package/build/tools/replay-declared-profile.test.d.ts +2 -0
- package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
- package/build/tools/replay-declared-profile.test.js +116 -0
- package/build/tools/replay-declared-profile.test.js.map +1 -0
- package/build/tools/replay-executor.d.ts +556 -0
- package/build/tools/replay-executor.d.ts.map +1 -0
- package/build/tools/replay-executor.js +2347 -0
- package/build/tools/replay-executor.js.map +1 -0
- package/build/tools/replay-folder-save.test.d.ts +2 -0
- package/build/tools/replay-folder-save.test.d.ts.map +1 -0
- package/build/tools/replay-folder-save.test.js +63 -0
- package/build/tools/replay-folder-save.test.js.map +1 -0
- package/build/tools/replay-formatters.d.ts +148 -0
- package/build/tools/replay-formatters.d.ts.map +1 -0
- package/build/tools/replay-formatters.js +856 -0
- package/build/tools/replay-formatters.js.map +1 -0
- package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
- package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
- package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
- package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
- package/build/tools/replay-multi-connection.test.d.ts +2 -0
- package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
- package/build/tools/replay-multi-connection.test.js +547 -0
- package/build/tools/replay-multi-connection.test.js.map +1 -0
- package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
- package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
- package/build/tools/replay-output-and-record-options.test.js +240 -0
- package/build/tools/replay-output-and-record-options.test.js.map +1 -0
- package/build/tools/replay-production-error-shape.test.d.ts +7 -0
- package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
- package/build/tools/replay-production-error-shape.test.js +93 -0
- package/build/tools/replay-production-error-shape.test.js.map +1 -0
- package/build/tools/replay-rebase.test.d.ts +2 -0
- package/build/tools/replay-rebase.test.d.ts.map +1 -0
- package/build/tools/replay-rebase.test.js +58 -0
- package/build/tools/replay-rebase.test.js.map +1 -0
- package/build/tools/replay-retry-classification.test.d.ts +9 -0
- package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
- package/build/tools/replay-retry-classification.test.js +57 -0
- package/build/tools/replay-retry-classification.test.js.map +1 -0
- package/build/tools/replay-run-registry.d.ts +55 -0
- package/build/tools/replay-run-registry.d.ts.map +1 -0
- package/build/tools/replay-run-registry.js +65 -0
- package/build/tools/replay-run-registry.js.map +1 -0
- package/build/tools/replay-sequence-reload.test.d.ts +2 -0
- package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
- package/build/tools/replay-sequence-reload.test.js +82 -0
- package/build/tools/replay-sequence-reload.test.js.map +1 -0
- package/build/tools/replay-socket-scope.test.d.ts +2 -0
- package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
- package/build/tools/replay-socket-scope.test.js +75 -0
- package/build/tools/replay-socket-scope.test.js.map +1 -0
- package/build/tools/replay-step-connection.test.d.ts +2 -0
- package/build/tools/replay-step-connection.test.d.ts.map +1 -0
- package/build/tools/replay-step-connection.test.js +292 -0
- package/build/tools/replay-step-connection.test.js.map +1 -0
- package/build/tools/replay-step-timeout.test.d.ts +2 -0
- package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
- package/build/tools/replay-step-timeout.test.js +204 -0
- package/build/tools/replay-step-timeout.test.js.map +1 -0
- package/build/tools/replay-tags.test.d.ts +2 -0
- package/build/tools/replay-tags.test.d.ts.map +1 -0
- package/build/tools/replay-tags.test.js +99 -0
- package/build/tools/replay-tags.test.js.map +1 -0
- package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
- package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
- package/build/tools/replay-teardown-and-foreach.test.js +318 -0
- package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
- package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
- package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
- package/build/tools/replay-tool-name-validation.test.js +280 -0
- package/build/tools/replay-tool-name-validation.test.js.map +1 -0
- package/build/tools/replay-tools.d.ts +288 -0
- package/build/tools/replay-tools.d.ts.map +1 -0
- package/build/tools/replay-tools.js +2994 -0
- package/build/tools/replay-tools.js.map +1 -0
- package/build/tools/request-abort.test.d.ts +2 -0
- package/build/tools/request-abort.test.d.ts.map +1 -0
- package/build/tools/request-abort.test.js +130 -0
- package/build/tools/request-abort.test.js.map +1 -0
- package/build/tools/request-tools.d.ts +56 -0
- package/build/tools/request-tools.d.ts.map +1 -0
- package/build/tools/request-tools.js +174 -0
- package/build/tools/request-tools.js.map +1 -0
- package/build/tools/screenshot-tools.d.ts +135 -0
- package/build/tools/screenshot-tools.d.ts.map +1 -0
- package/build/tools/screenshot-tools.js +636 -0
- package/build/tools/screenshot-tools.js.map +1 -0
- package/build/tools/server-tools.d.ts +88 -0
- package/build/tools/server-tools.d.ts.map +1 -0
- package/build/tools/server-tools.js +485 -0
- package/build/tools/server-tools.js.map +1 -0
- package/build/tools/source-tools.d.ts +46 -0
- package/build/tools/source-tools.d.ts.map +1 -0
- package/build/tools/source-tools.js +47 -0
- package/build/tools/source-tools.js.map +1 -0
- package/build/tools/storage-tools.d.ts +141 -0
- package/build/tools/storage-tools.d.ts.map +1 -0
- package/build/tools/storage-tools.js +894 -0
- package/build/tools/storage-tools.js.map +1 -0
- package/build/tools/storage-tools.test.d.ts +13 -0
- package/build/tools/storage-tools.test.d.ts.map +1 -0
- package/build/tools/storage-tools.test.js +707 -0
- package/build/tools/storage-tools.test.js.map +1 -0
- package/build/tools/tab-tools.d.ts +42 -0
- package/build/tools/tab-tools.d.ts.map +1 -0
- package/build/tools/tab-tools.js +296 -0
- package/build/tools/tab-tools.js.map +1 -0
- package/build/tools/wait-tools.d.ts +85 -0
- package/build/tools/wait-tools.d.ts.map +1 -0
- package/build/tools/wait-tools.js +236 -0
- package/build/tools/wait-tools.js.map +1 -0
- package/build/tools/wait-tools.test.d.ts +9 -0
- package/build/tools/wait-tools.test.d.ts.map +1 -0
- package/build/tools/wait-tools.test.js +193 -0
- package/build/tools/wait-tools.test.js.map +1 -0
- package/build/types.d.ts +133 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +5 -0
- package/build/types.js.map +1 -0
- package/build/ui-verifier.d.ts +88 -0
- package/build/ui-verifier.d.ts.map +1 -0
- package/build/ui-verifier.js +540 -0
- package/build/ui-verifier.js.map +1 -0
- package/build/utils/abort.d.ts +68 -0
- package/build/utils/abort.d.ts.map +1 -0
- package/build/utils/abort.js +159 -0
- package/build/utils/abort.js.map +1 -0
- package/build/utils/abort.test.d.ts +2 -0
- package/build/utils/abort.test.d.ts.map +1 -0
- package/build/utils/abort.test.js +174 -0
- package/build/utils/abort.test.js.map +1 -0
- package/build/utils/modal-detection-core.d.ts +150 -0
- package/build/utils/modal-detection-core.d.ts.map +1 -0
- package/build/utils/modal-detection-core.js +332 -0
- package/build/utils/modal-detection-core.js.map +1 -0
- package/build/utils/modal-detection-core.test.d.ts +5 -0
- package/build/utils/modal-detection-core.test.d.ts.map +1 -0
- package/build/utils/modal-detection-core.test.js +492 -0
- package/build/utils/modal-detection-core.test.js.map +1 -0
- package/build/utils/modal-detector.d.ts +29 -0
- package/build/utils/modal-detector.d.ts.map +1 -0
- package/build/utils/modal-detector.js +281 -0
- package/build/utils/modal-detector.js.map +1 -0
- package/build/utils/modal-dismissal.d.ts +32 -0
- package/build/utils/modal-dismissal.d.ts.map +1 -0
- package/build/utils/modal-dismissal.js +190 -0
- package/build/utils/modal-dismissal.js.map +1 -0
- package/build/utils/port-check.d.ts +31 -0
- package/build/utils/port-check.d.ts.map +1 -0
- package/build/utils/port-check.js +87 -0
- package/build/utils/port-check.js.map +1 -0
- package/build/utils/port-check.test.d.ts +2 -0
- package/build/utils/port-check.test.d.ts.map +1 -0
- package/build/utils/port-check.test.js +66 -0
- package/build/utils/port-check.test.js.map +1 -0
- package/build/utils/selector-resolver.d.ts +66 -0
- package/build/utils/selector-resolver.d.ts.map +1 -0
- package/build/utils/selector-resolver.js +200 -0
- package/build/utils/selector-resolver.js.map +1 -0
- package/build/validation-helpers.d.ts +41 -0
- package/build/validation-helpers.d.ts.map +1 -0
- package/build/validation-helpers.js +236 -0
- package/build/validation-helpers.js.map +1 -0
- package/build/window-sizing.d.ts +26 -0
- package/build/window-sizing.d.ts.map +1 -0
- package/build/window-sizing.js +87 -0
- package/build/window-sizing.js.map +1 -0
- package/build/window-sizing.test.d.ts +8 -0
- package/build/window-sizing.test.d.ts.map +1 -0
- package/build/window-sizing.test.js +113 -0
- package/build/window-sizing.test.js.map +1 -0
- package/docs/README.md +74 -0
- package/docs/automation.md +562 -0
- package/docs/debugging.md +229 -0
- package/docs/installation.md +133 -0
- package/docs/instructions.md +227 -0
- package/docs/mcp-instructions.md +15 -0
- package/docs/message-system.md +327 -0
- package/docs/messages.md +3480 -0
- package/docs/parser-plugins.md +132 -0
- package/docs/replay.md +1411 -0
- package/docs/troubleshooting.md +272 -0
- package/docs/working_with_llm_notes.md +5 -0
- package/package.json +87 -0
- package/skills/devharness/SKILL.md +195 -0
- package/skills/devharness/references/sequences.md +613 -0
- package/skills/devharness/references/tool-categories.md +99 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,2007 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Early stderr logging for debugging startup issues
|
|
3
|
+
console.error(`[cdp-tools] Process starting (PID: ${process.pid})`);
|
|
4
|
+
// Capture startup time immediately before any imports
|
|
5
|
+
const STARTUP_TIME = performance.now();
|
|
6
|
+
/**
|
|
7
|
+
* devharness
|
|
8
|
+
* MCP server providing Chrome DevTools Protocol debugging capabilities to AI assistants
|
|
9
|
+
*/
|
|
10
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { CDPManager } from './cdp-manager.js';
|
|
15
|
+
import { SourceMapHandler } from './sourcemap-handler.js';
|
|
16
|
+
import { ChromeLauncher, InvalidProfileNameError, ProfileInUseError, ProfileLockedError, normalizeProfileName, resolveLaunchPort, decideProfileReuse } from './chrome-launcher.js';
|
|
17
|
+
import { PuppeteerManager } from './puppeteer-manager.js';
|
|
18
|
+
import { ConsoleMonitor } from './console-monitor.js';
|
|
19
|
+
import { NetworkMonitor } from './network-monitor.js';
|
|
20
|
+
import { ConnectionManager } from './connection-manager.js';
|
|
21
|
+
import { LogpointExecutionTracker } from './logpoint-execution-tracker.js';
|
|
22
|
+
import { PortReserver } from './port-reserver.js';
|
|
23
|
+
import { validateParams, createTool } from './validation-helpers.js';
|
|
24
|
+
import { ClickableCache } from './clickable-cache.js';
|
|
25
|
+
import { CommandRecorder } from './command-recorder.js';
|
|
26
|
+
import { createBreakpointTools } from './tools/breakpoint-tools.js';
|
|
27
|
+
import { createExecutionTools } from './tools/execution-tools.js';
|
|
28
|
+
import { createInspectionTools } from './tools/inspection-tools.js';
|
|
29
|
+
import { createSourceTools } from './tools/source-tools.js';
|
|
30
|
+
import { createConsoleTools } from './tools/console-tools.js';
|
|
31
|
+
import { createNetworkTools } from './tools/network-tools.js';
|
|
32
|
+
import { createPageTools } from './tools/page-tools.js';
|
|
33
|
+
import { createDOMTools } from './tools/dom-tools.js';
|
|
34
|
+
import { createScreenshotTools } from './tools/screenshot-tools.js';
|
|
35
|
+
import { createInputTools } from './tools/input-tools.js';
|
|
36
|
+
import { createContentTools } from './tools/content-tools.js';
|
|
37
|
+
import { createStorageTools } from './tools/storage-tools.js';
|
|
38
|
+
import { createTabTools } from './tools/tab-tools.js';
|
|
39
|
+
import { createDownloadTools } from './tools/download-tools.js';
|
|
40
|
+
import { createRequestTools } from './tools/request-tools.js';
|
|
41
|
+
import { createAssertTools } from './tools/assert-tools.js';
|
|
42
|
+
import { createWaitTools } from './tools/wait-tools.js';
|
|
43
|
+
import { createModalTools } from './tools/modal-tools.js';
|
|
44
|
+
import { createReplayTools } from './tools/replay-tools.js';
|
|
45
|
+
import { createServerTools } from './tools/server-tools.js';
|
|
46
|
+
import { createConfigTools } from './tools/config-tools.js';
|
|
47
|
+
import { createPluginTools } from './tools/plugin-tools.js';
|
|
48
|
+
import { createIssuesTools } from './tools/issues-tools.js';
|
|
49
|
+
import { createDashboardTools, setDashboardInstance, getDashboardInstance, setSessionInfo, getDuplicateSessionInfo } from './tools/dashboard-tools.js';
|
|
50
|
+
import { initializeDashboard, shutdownDashboard } from './dashboard/index.js';
|
|
51
|
+
import { Orchestrator } from './log-processor/orchestrator.js';
|
|
52
|
+
import { mkdirSync, existsSync, readFileSync, statSync } from 'fs';
|
|
53
|
+
import { homedir } from 'os';
|
|
54
|
+
import { ServerManager, detectAutoRestartCommand } from './server-manager.js';
|
|
55
|
+
import { configManager } from './config.js';
|
|
56
|
+
import { ToolError } from './tool-error.js';
|
|
57
|
+
import { checkPortFailures, checkBreakpointPause, checkBugBlocking, checkPendingStartups, checkDuplicateSession, prependToResponse, appendToResponse, buildStatusSuffix } from './tool-response.js';
|
|
58
|
+
import { createSuccessResponse, createErrorResponse } from './messages.js';
|
|
59
|
+
import { setChromeLauncher } from './error-helpers.js';
|
|
60
|
+
import { createServer } from 'net';
|
|
61
|
+
import { readFile } from 'fs/promises';
|
|
62
|
+
import { join, dirname } from 'path';
|
|
63
|
+
import { fileURLToPath } from 'url';
|
|
64
|
+
import { debugLog, enableDebugLogging, disableDebugLogging, isDebugEnabled, enableHistoryLogging, setStartupMetrics } from './debug-logger.js';
|
|
65
|
+
import { validateReference, requireValidReference, deriveConnectionReference, UNNAMED_CONNECTION, InvalidReferenceError } from './reference-validator.js';
|
|
66
|
+
import { initializePaths } from './helpers/paths.js';
|
|
67
|
+
import { cleanupStaleTempFiles, cleanupStaleTempFilesSync } from './atomic-write.js';
|
|
68
|
+
import { createSessionDetector } from './session-detector.js';
|
|
69
|
+
import { serverClaims } from './server-claims.js';
|
|
70
|
+
import { sizeWindowToViewport } from './window-sizing.js';
|
|
71
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
72
|
+
const __dirname = dirname(__filename);
|
|
73
|
+
/**
|
|
74
|
+
* This package's version, read from package.json rather than duplicated here
|
|
75
|
+
* so it cannot drift. Used to report the server version and to detect an
|
|
76
|
+
* installed skill left behind by an older release (see getSkillInstallState).
|
|
77
|
+
*/
|
|
78
|
+
const SERVER_VERSION = (() => {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version ?? '0.0.0';
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return '0.0.0';
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
/**
|
|
87
|
+
* Which build is actually answering, so a session can tell whether the code it
|
|
88
|
+
* is calling is the code it just compiled.
|
|
89
|
+
*
|
|
90
|
+
* A rebuild signals the supervisor named in this project's pidfile, which is
|
|
91
|
+
* not necessarily the supervisor serving this session - when it isn't, the
|
|
92
|
+
* build reports success and the old code keeps answering. There was no way to
|
|
93
|
+
* notice: behaviour was read from a stale build for several iterations and a
|
|
94
|
+
* fix that already worked was called broken (issue #135).
|
|
95
|
+
*
|
|
96
|
+
* `buildMtime` is read once at startup, so it dates the running code rather
|
|
97
|
+
* than whatever is on disk now - which is the whole point of the comparison.
|
|
98
|
+
*/
|
|
99
|
+
const BUILD_IDENTITY = (() => {
|
|
100
|
+
const entryPath = __filename;
|
|
101
|
+
try {
|
|
102
|
+
return { entryPath, buildMtime: statSync(entryPath).mtime.toISOString() };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { entryPath, buildMtime: 'unknown' };
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
/**
|
|
109
|
+
* Find an available port starting from the given port
|
|
110
|
+
*/
|
|
111
|
+
async function findAvailablePort(startPort) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const server = createServer();
|
|
114
|
+
// Explicitly bind to IPv4 localhost to match Chrome's behavior
|
|
115
|
+
server.listen(startPort, '127.0.0.1', () => {
|
|
116
|
+
const port = server.address().port;
|
|
117
|
+
console.error(`[cdp-tools] findAvailablePort: Port ${port} is available`);
|
|
118
|
+
server.close(() => resolve(port));
|
|
119
|
+
});
|
|
120
|
+
server.on('error', (err) => {
|
|
121
|
+
if (err.code === 'EADDRINUSE') {
|
|
122
|
+
// Port in use, try next one
|
|
123
|
+
console.error(`[cdp-tools] findAvailablePort: Port ${startPort} is in use, trying ${startPort + 1}`);
|
|
124
|
+
resolve(findAvailablePort(startPort + 1));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
reject(err);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Find starting port from environment variable or auto-assign
|
|
134
|
+
*/
|
|
135
|
+
async function findStartingPort() {
|
|
136
|
+
const envPort = process.env.MCP_DEBUG_PORT;
|
|
137
|
+
const startingPort = configManager.getChromeConfig().startingDebugPort;
|
|
138
|
+
if (envPort) {
|
|
139
|
+
const port = parseInt(envPort, 10);
|
|
140
|
+
if (isNaN(port) || port < 1024 || port > 65535) {
|
|
141
|
+
console.error(`Invalid MCP_DEBUG_PORT: ${envPort}. Using auto-assigned port.`);
|
|
142
|
+
return findAvailablePort(startingPort);
|
|
143
|
+
}
|
|
144
|
+
return port;
|
|
145
|
+
}
|
|
146
|
+
return findAvailablePort(startingPort);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Locations a skills-aware client (Claude Code and others following the
|
|
150
|
+
* agentskills.io convention) would scan for the bundled cdp-tools skill.
|
|
151
|
+
* Checked at both project- and user-level, and both the client-native
|
|
152
|
+
* `.claude/skills/` path and the cross-client `.agents/skills/` convention.
|
|
153
|
+
*/
|
|
154
|
+
function findSkillInstallCandidates() {
|
|
155
|
+
return [
|
|
156
|
+
join(process.cwd(), '.claude', 'skills', 'cdp-tools'),
|
|
157
|
+
join(process.cwd(), '.agents', 'skills', 'cdp-tools'),
|
|
158
|
+
join(homedir(), '.claude', 'skills', 'cdp-tools'),
|
|
159
|
+
join(homedir(), '.agents', 'skills', 'cdp-tools'),
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
/** Version stamped into a SKILL.md frontmatter, if it has one. */
|
|
163
|
+
function readSkillVersion(skillFile) {
|
|
164
|
+
try {
|
|
165
|
+
const head = readFileSync(skillFile, 'utf-8').slice(0, 2000);
|
|
166
|
+
return head.match(/^version:\s*(.+)$/m)?.[1].trim() ?? null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Whether an installed copy of the skill exists and whether it matches this
|
|
174
|
+
* package.
|
|
175
|
+
*
|
|
176
|
+
* Presence alone is not enough. The documented install is a symlink into the
|
|
177
|
+
* package, which tracks upgrades for free - but nothing stops a client or user
|
|
178
|
+
* from *copying* the directory instead, and a copy is frozen forever: the file
|
|
179
|
+
* exists, so a presence-only check suppresses the nudge permanently and the
|
|
180
|
+
* user silently runs an old skill against a newer tool surface. That is not
|
|
181
|
+
* hypothetical - this package shipped a catalog describing a pre-grouping API
|
|
182
|
+
* long after those tools were consolidated away. Comparing the stamped version
|
|
183
|
+
* catches the copy case without needing any install machinery of our own.
|
|
184
|
+
*/
|
|
185
|
+
function getSkillInstallState() {
|
|
186
|
+
let stale = null;
|
|
187
|
+
for (const dir of findSkillInstallCandidates()) {
|
|
188
|
+
const skillFile = join(dir, 'SKILL.md');
|
|
189
|
+
if (!existsSync(skillFile))
|
|
190
|
+
continue;
|
|
191
|
+
const installedVersion = readSkillVersion(skillFile);
|
|
192
|
+
// A symlink into this package reads back our own file, so this matches and
|
|
193
|
+
// we stop. Any installed copy that predates the version stamp reports null,
|
|
194
|
+
// which is itself evidence of staleness.
|
|
195
|
+
if (installedVersion === SERVER_VERSION)
|
|
196
|
+
return { status: 'current' };
|
|
197
|
+
stale ??= { path: skillFile, installedVersion };
|
|
198
|
+
}
|
|
199
|
+
return stale ? { status: 'stale', ...stale } : { status: 'absent' };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Load the MCP `instructions` payload from docs/mcp-instructions.md.
|
|
203
|
+
*
|
|
204
|
+
* Kept short and separate from docs/instructions.md (the full human-readable
|
|
205
|
+
* reference): many MCP clients inject this field into the model's context
|
|
206
|
+
* unconditionally at session start, so it shouldn't carry the full tool
|
|
207
|
+
* catalog. The full reference is instead shipped as a progressive-disclosure
|
|
208
|
+
* Agent Skill (skills/devharness/) that skills-aware clients load on demand.
|
|
209
|
+
*
|
|
210
|
+
* If that skill isn't installed anywhere a client would scan for it - or an
|
|
211
|
+
* installed copy is stamped with an older version of this package - append a
|
|
212
|
+
* nudge asking the model to offer setting it up, but only ask, never symlink
|
|
213
|
+
* it in unprompted. Once a matching version is installed this stops appearing.
|
|
214
|
+
*/
|
|
215
|
+
async function loadInstructions() {
|
|
216
|
+
try {
|
|
217
|
+
const instructionsPath = join(__dirname, '..', 'docs', 'mcp-instructions.md');
|
|
218
|
+
const instructions = await readFile(instructionsPath, 'utf-8');
|
|
219
|
+
const skillState = getSkillInstallState();
|
|
220
|
+
if (skillState.status === 'current') {
|
|
221
|
+
return instructions;
|
|
222
|
+
}
|
|
223
|
+
const skillSourcePath = join(__dirname, '..', 'skills', 'cdp-tools');
|
|
224
|
+
const install = `\`\`\`bash\nmkdir -p .claude/skills\nrm -rf .claude/skills/devharness\nln -s "${skillSourcePath}" .claude/skills/devharness\n\`\`\``;
|
|
225
|
+
const nudge = skillState.status === 'absent'
|
|
226
|
+
? `\n\n---\n\n**Setup nudge (one-time):** No cdp-tools Agent Skill was found in this project or your home directory (checked \`.claude/skills/\`, \`.agents/skills/\`). Ask the user if they'd like it installed - if they agree, run:\n\n${install}\n\nDo not create the symlink without asking first. This message stops appearing once the skill is installed anywhere it would be scanned.\n`
|
|
227
|
+
: `\n\n---\n\n**Stale skill:** The cdp-tools Agent Skill at \`${skillState.path}\` is from version ${skillState.installedVersion ?? 'an unstamped release'}, but this server is ${SERVER_VERSION}. It was copied rather than symlinked, so it no longer tracks upgrades and may describe tools or actions that have since changed. Ask the user whether to replace it with a symlink that stays current:\n\n${install}\n\nDo not delete or replace their file without asking first - and note the skill is read by the client at session start, so it only takes effect in a new session.\n`;
|
|
228
|
+
return instructions + nudge;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error('[cdp-tools] Failed to load instructions file:', error instanceof Error ? error.message : error);
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Initialize global managers
|
|
236
|
+
const sourceMapHandler = new SourceMapHandler();
|
|
237
|
+
const chromeLauncher = new ChromeLauncher({
|
|
238
|
+
// Resolved lazily so a live config reload of chrome.persistentProfileRoot
|
|
239
|
+
// (global ~/.cdp-tools/profiles by default, or a project-local override)
|
|
240
|
+
// is picked up without restarting the server.
|
|
241
|
+
persistentProfileRoot: () => configManager.getPersistentProfileRoot(),
|
|
242
|
+
});
|
|
243
|
+
const connectionManager = new ConnectionManager();
|
|
244
|
+
const logpointTracker = new LogpointExecutionTracker();
|
|
245
|
+
const clickableCache = new ClickableCache();
|
|
246
|
+
const commandRecorder = new CommandRecorder();
|
|
247
|
+
const portReserver = new PortReserver();
|
|
248
|
+
const serverManager = new ServerManager();
|
|
249
|
+
// Configure connection manager to kill Chrome when last connection closes
|
|
250
|
+
connectionManager.setChromeLauncher(chromeLauncher);
|
|
251
|
+
// Let ServerManager's watch mode check whether a connection at a given
|
|
252
|
+
// inspector port is paused at a breakpoint, so it can defer a file-change
|
|
253
|
+
// restart until the debugger resumes (see requestWatchRestart()). Watched
|
|
254
|
+
// processes are always local, so 'localhost' matches how connectDebugger
|
|
255
|
+
// registers them by default.
|
|
256
|
+
serverManager.setPauseChecker((port) => {
|
|
257
|
+
return connectionManager.findConnectionByPort('localhost', port)?.cdpManager.isPaused() ?? false;
|
|
258
|
+
});
|
|
259
|
+
// Set ChromeLauncher reference for error-helpers (used to verify Chrome is running)
|
|
260
|
+
setChromeLauncher(chromeLauncher);
|
|
261
|
+
// Set up Chrome exit callback to clean up connections and reserve a new port
|
|
262
|
+
chromeLauncher.setOnExitCallback(async (event) => {
|
|
263
|
+
const { port } = event;
|
|
264
|
+
await debugLog('index', `Chrome exited on port ${port} (reason: ${event.reason})`);
|
|
265
|
+
// Clean up all connections for the dead Chrome instance
|
|
266
|
+
// Note: ChromeLauncher only launches on localhost, so this is always correct
|
|
267
|
+
const connectionsToClose = connectionManager.getConnectionsForBrowser('localhost', port);
|
|
268
|
+
for (const conn of connectionsToClose) {
|
|
269
|
+
try {
|
|
270
|
+
await connectionManager.closeConnection(conn.id);
|
|
271
|
+
await debugLog('index', `Closed connection ${conn.id} after Chrome exit`);
|
|
272
|
+
}
|
|
273
|
+
catch (closeError) {
|
|
274
|
+
await debugLog('index', `Failed to close connection ${conn.id}: ${closeError}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Reserve a new port for future launches
|
|
278
|
+
try {
|
|
279
|
+
const startingPort = configManager.getChromeConfig().startingDebugPort;
|
|
280
|
+
const newPort = await findAvailablePort(startingPort);
|
|
281
|
+
await portReserver.reserve(newPort);
|
|
282
|
+
configManager.setCurrentPort(newPort);
|
|
283
|
+
await debugLog('index', `Reserved new port ${newPort}`);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await debugLog('index', `Failed to reserve new port after Chrome exit: ${error}`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
/**
|
|
290
|
+
* Create and configure the MCP server with instructions
|
|
291
|
+
*/
|
|
292
|
+
async function createMCPServer() {
|
|
293
|
+
const instructions = await loadInstructions();
|
|
294
|
+
return new Server({
|
|
295
|
+
name: 'cdp-tools-debugger',
|
|
296
|
+
version: SERVER_VERSION,
|
|
297
|
+
}, {
|
|
298
|
+
capabilities: {
|
|
299
|
+
// listChanged: true so mcp-supervisor.ts's notifications/tools/list_changed
|
|
300
|
+
// (sent after a hot-restart) is spec-compliant to send.
|
|
301
|
+
tools: { listChanged: true },
|
|
302
|
+
},
|
|
303
|
+
instructions,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Wait for Chrome debugging port to become ready
|
|
308
|
+
* Polls the /json/version endpoint until Chrome is inspectable
|
|
309
|
+
*/
|
|
310
|
+
async function waitForChromeReady(port, maxAttempts = 10) {
|
|
311
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
312
|
+
try {
|
|
313
|
+
const controller = new AbortController();
|
|
314
|
+
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
|
315
|
+
const response = await fetch(`http://localhost:${port}/json/version`, {
|
|
316
|
+
signal: controller.signal
|
|
317
|
+
});
|
|
318
|
+
clearTimeout(timeoutId);
|
|
319
|
+
if (response.ok) {
|
|
320
|
+
// Chrome is ready and inspectable
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
// Chrome not ready yet, continue polling
|
|
326
|
+
}
|
|
327
|
+
// Exponential backoff: 500ms + (attempt * 200ms)
|
|
328
|
+
await new Promise(resolve => setTimeout(resolve, 500 + i * 200));
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Chrome debugging port ${port} failed to become inspectable within timeout. Try increasing the wait time or check if Chrome started correctly.`);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check if Chrome is running and accessible on the specified port
|
|
334
|
+
* Returns true if Chrome is responding to debug protocol requests
|
|
335
|
+
* Returns false if port is reserved (chrome-not-running) or connection fails
|
|
336
|
+
*/
|
|
337
|
+
async function isChromeRunning(port) {
|
|
338
|
+
try {
|
|
339
|
+
const controller = new AbortController();
|
|
340
|
+
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
|
341
|
+
const response = await fetch(`http://localhost:${port}/json/version`, {
|
|
342
|
+
signal: controller.signal
|
|
343
|
+
});
|
|
344
|
+
clearTimeout(timeoutId);
|
|
345
|
+
// Check if this is the port reserver responding
|
|
346
|
+
const text = await response.text();
|
|
347
|
+
if (text.trim() === 'chrome-not-running') {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
// Otherwise, check if we got a valid Chrome response
|
|
351
|
+
return response.ok;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Stamp a launchChrome response with who owns the resulting connection.
|
|
359
|
+
*
|
|
360
|
+
* A replay run cannot tell from the text whether it CREATED a browser or was
|
|
361
|
+
* handed one that already existed, and guessing either way is destructive: kill
|
|
362
|
+
* a borrowed browser and the user loses state they cannot recover, keep an
|
|
363
|
+
* owned one and every run leaks a process (issue #103).
|
|
364
|
+
*/
|
|
365
|
+
function withLaunchMeta(response, reference, reused) {
|
|
366
|
+
response._meta = { ...(response._meta || {}), launchChrome: { reference, reused } };
|
|
367
|
+
return response;
|
|
368
|
+
}
|
|
369
|
+
// Connection management tools
|
|
370
|
+
const connectionTools = {
|
|
371
|
+
launchChrome: createTool('Launch Chrome with debugging', z.object({
|
|
372
|
+
url: z.string().optional().describe('URL to open (default: blank page)'),
|
|
373
|
+
autoConnect: z.boolean().optional().default(true).describe('Automatically connect debugger after launch'),
|
|
374
|
+
port: z.number().optional().describe('The debugging port (optional, defaults to this session\'s reserved port). Use this to launch multiple Chrome instances on different ports. Always honoured when given - with forceNewInstance the call errors if that exact port is already taken instead of moving to another port.'),
|
|
375
|
+
forceNewInstance: z.boolean().optional().describe('Always spawn a fresh Chrome process instead of reusing/tabbing into an existing instance. Without `port`, a free port is chosen automatically; with `port`, that port is used and the call errors if it is already in use. Errors if `reference` is already bound to a live connection.'),
|
|
376
|
+
headless: z.boolean().optional().default(false).describe('Launch in headless mode (no visible window, prevents focus stealing). Default: false'),
|
|
377
|
+
reference: z.string().optional().describe('Connection reference name (3 descriptive words). If not provided, defaults to "unnamed-connection-default". Use this to identify the connection when calling other tools.'),
|
|
378
|
+
width: z.number().optional().describe('Viewport width in CSS px. Sizes the real OS window, not an emulated viewport, so the page keeps tracking window resizes. Bigger than the display is clamped and reported. Headless emulates instead.'),
|
|
379
|
+
height: z.number().optional().describe('Viewport height in CSS px. Sized like `width`.'),
|
|
380
|
+
profile: z.string().optional().describe('Named persistent Chrome profile, e.g. "device-a". Naming a profile makes it persistent: it maps to a stable user-data-dir under ~/.cdp-tools/profiles (override per project with chrome.persistentProfileRoot) and is never deleted, so cookies, localStorage and IndexedDB - including non-extractable CryptoKeys - survive across runs. Created on first use. Does NOT pin a port; port selection is unchanged. Wipe it with config({action:"resetProfile", profile:"device-a"}). Only one live Chrome may hold a given profile at a time.'),
|
|
381
|
+
chromeArgs: z.array(z.string()).optional().describe('Extra Chrome command-line flags to pass through at launch, e.g. ["--use-fake-device-for-media-stream", "--use-fake-ui-for-media-stream"]. Merged after the managed defaults. The CDP_TOOLS_EXTRA_CHROME_ARGS env var (space-separated) is also always merged. Only applies when this call actually launches Chrome (ignored when an existing instance on the port is reused).'),
|
|
382
|
+
}).strict(), async (args) => {
|
|
383
|
+
// Validate reference FIRST, before launching Chrome
|
|
384
|
+
const userReference = args.reference;
|
|
385
|
+
if (userReference) {
|
|
386
|
+
requireValidReference(userReference); // Throws InvalidReferenceError if invalid
|
|
387
|
+
}
|
|
388
|
+
// Validate the profile name before anything else - an invalid name must
|
|
389
|
+
// not reach the filesystem, and naming a profile implies persistence
|
|
390
|
+
// (there is no separate persist flag).
|
|
391
|
+
let profileName;
|
|
392
|
+
if (args.profile !== undefined) {
|
|
393
|
+
try {
|
|
394
|
+
profileName = normalizeProfileName(args.profile);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
if (error instanceof InvalidProfileNameError) {
|
|
398
|
+
return createErrorResponse('CHROME_PROFILE_INVALID_NAME', { profile: args.profile });
|
|
399
|
+
}
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
// NOTE: the "profile already held" check deliberately happens further
|
|
403
|
+
// down, once we know this call would actually have to spawn a second
|
|
404
|
+
// Chrome. Checking here broke the standard idempotent call pattern
|
|
405
|
+
// `launchChrome({ profile, reference })`: re-calling it to make sure
|
|
406
|
+
// the browser is up always errored instead of reusing the very
|
|
407
|
+
// connection that holds the profile.
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Profile gate for every point where we may hand back an existing Chrome
|
|
411
|
+
* or spawn a new one. `existingPort` is the instance we would reuse
|
|
412
|
+
* (omit when this call would spawn). Returns an error response, or null
|
|
413
|
+
* to carry on. The decision itself is decideProfileReuse() in
|
|
414
|
+
* chrome-launcher.ts so its ordering is unit-testable.
|
|
415
|
+
*/
|
|
416
|
+
const profileGate = async (existingPort) => {
|
|
417
|
+
if (!profileName) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const decision = decideProfileReuse({
|
|
421
|
+
wantedProfileDir: chromeLauncher.getPersistentProfilePath(profileName),
|
|
422
|
+
existing: existingPort !== undefined
|
|
423
|
+
? { port: existingPort, profileDir: chromeLauncher.getProfileDir(existingPort) }
|
|
424
|
+
: undefined,
|
|
425
|
+
holderPort: chromeLauncher.findPortForProfile(profileName),
|
|
426
|
+
});
|
|
427
|
+
if (decision.decision === 'in-use') {
|
|
428
|
+
await debugLog('index', `launchChrome: profile "${profileName}" already held by Chrome on port ${decision.port}`);
|
|
429
|
+
return createErrorResponse('CHROME_PROFILE_IN_USE', {
|
|
430
|
+
profile: profileName,
|
|
431
|
+
port: decision.port.toString(),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (decision.decision === 'mismatch') {
|
|
435
|
+
await debugLog('index', `launchChrome: port ${decision.port} already runs profile ${decision.actualProfile ?? 'unknown'}, not "${profileName}"`);
|
|
436
|
+
return createErrorResponse('CHROME_PROFILE_PORT_MISMATCH', {
|
|
437
|
+
profile: profileName,
|
|
438
|
+
port: decision.port.toString(),
|
|
439
|
+
actualProfile: decision.actualProfile ?? 'unknown (Chrome not launched by cdp-tools)',
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
};
|
|
444
|
+
// Is the port occupied by anything other than our own reservation?
|
|
445
|
+
// Our port reserver holds a listening socket on the reserved port and
|
|
446
|
+
// releases it as part of launching, so it must not count as "occupied".
|
|
447
|
+
const isPortHeldByOther = async (candidate) => {
|
|
448
|
+
if (portReserver.isReserved() && portReserver.getPort() === candidate) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
return new Promise((resolve) => {
|
|
452
|
+
const probe = createServer();
|
|
453
|
+
probe.once('error', () => resolve(true));
|
|
454
|
+
probe.once('listening', () => probe.close(() => resolve(false)));
|
|
455
|
+
// Bind IPv4 localhost to match Chrome's binding behaviour
|
|
456
|
+
probe.listen(candidate, '127.0.0.1');
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
// Use reserved port unless explicitly specified. The decision itself lives
|
|
460
|
+
// in resolveLaunchPort() (chrome-launcher.ts) so it can be unit tested.
|
|
461
|
+
const decision = await resolveLaunchPort({
|
|
462
|
+
explicitPort: args.port,
|
|
463
|
+
forceNewInstance: args.forceNewInstance,
|
|
464
|
+
reservedPort: configManager.getCurrentPort(),
|
|
465
|
+
isPortOccupied: async (candidate) => connectionManager.hasBrowser('localhost', candidate) ||
|
|
466
|
+
chromeLauncher.isRunning(candidate) ||
|
|
467
|
+
await isPortHeldByOther(candidate),
|
|
468
|
+
findFreePort: () => findAvailablePort(configManager.getChromeConfig().startingDebugPort),
|
|
469
|
+
});
|
|
470
|
+
if (decision.decision === 'forced-port-in-use') {
|
|
471
|
+
await debugLog('index', `launchChrome: forceNewInstance requested port ${decision.port} but it is already in use`);
|
|
472
|
+
return createErrorResponse('CHROME_FORCED_PORT_IN_USE', { port: decision.port.toString() });
|
|
473
|
+
}
|
|
474
|
+
// A named profile is an identity, and the Chrome already holding it IS
|
|
475
|
+
// that identity - so go to its port rather than the session's reserved
|
|
476
|
+
// one. Without this, `launchChrome({ profile })` for a profile that is up
|
|
477
|
+
// under some other reference resolves to a free port, finds nothing
|
|
478
|
+
// there, and refuses to spawn because the profile is held elsewhere:
|
|
479
|
+
// "already running" reported as a conflict. An explicit port or
|
|
480
|
+
// forceNewInstance is a deliberate override and still wins.
|
|
481
|
+
const profileHolderPort = profileName && !args.port && !args.forceNewInstance
|
|
482
|
+
? chromeLauncher.findPortForProfile(profileName)
|
|
483
|
+
: undefined;
|
|
484
|
+
const port = profileHolderPort ?? decision.port;
|
|
485
|
+
await debugLog('index', `launchChrome called: port=${port}, requested=${args.port}, reserved=${configManager.getCurrentPort()}, profileHolder=${profileHolderPort ?? 'none'}, forceNewInstance=${args.forceNewInstance}, url=${args.url}, autoConnect=${args.autoConnect}, reference=${args.reference}`);
|
|
486
|
+
const url = args.url;
|
|
487
|
+
const autoConnect = args.autoConnect ?? true;
|
|
488
|
+
// Check if a connection with this reference already exists - reuse it instead of creating a new tab
|
|
489
|
+
// Use validated lookup to auto-cleanup dead connections (e.g., if Chrome was killed externally)
|
|
490
|
+
// Under forceNewInstance we still run this lookup, but a live match is an
|
|
491
|
+
// error rather than a reuse: a fresh process bound to an already-bound
|
|
492
|
+
// reference would leave two Chromes answering to the same name (bug-005).
|
|
493
|
+
if (userReference) {
|
|
494
|
+
const existingConnection = await connectionManager.findConnectionByReferenceValidated(userReference);
|
|
495
|
+
if (existingConnection) {
|
|
496
|
+
const sanitizedRef = validateReference(userReference).sanitized;
|
|
497
|
+
if (args.forceNewInstance) {
|
|
498
|
+
await debugLog('index', `launchChrome: forceNewInstance with reference "${sanitizedRef}" already bound to a live connection - refusing to double-bind`);
|
|
499
|
+
return createErrorResponse('CHROME_REFERENCE_ALREADY_BOUND', { reference: sanitizedRef });
|
|
500
|
+
}
|
|
501
|
+
// Reuse is only correct when that connection is running the profile
|
|
502
|
+
// the caller asked for - otherwise we would hand back a different
|
|
503
|
+
// browser identity under the same reference.
|
|
504
|
+
const profileBlocked = await profileGate(existingConnection.port);
|
|
505
|
+
if (profileBlocked) {
|
|
506
|
+
return profileBlocked;
|
|
507
|
+
}
|
|
508
|
+
await debugLog('index', `Connection with reference "${sanitizedRef}" already exists, reusing`);
|
|
509
|
+
// Set as active connection
|
|
510
|
+
connectionManager.setActiveConnection(existingConnection.id);
|
|
511
|
+
updateActiveManagers(existingConnection.id);
|
|
512
|
+
// Get current page info
|
|
513
|
+
let title = 'Unknown';
|
|
514
|
+
let pageUrl = 'about:blank';
|
|
515
|
+
if (existingConnection.puppeteerManager) {
|
|
516
|
+
const page = existingConnection.puppeteerManager.getPage();
|
|
517
|
+
pageUrl = page.url();
|
|
518
|
+
title = await page.title();
|
|
519
|
+
}
|
|
520
|
+
// `reused: true` is how a replay run tells a browser it BORROWED from
|
|
521
|
+
// one it created: the reference already existed, so it belongs to
|
|
522
|
+
// whoever made it and must survive killChromeOnFinish.
|
|
523
|
+
return withLaunchMeta(createSuccessResponse('CHROME_CONNECTION_REUSED', {
|
|
524
|
+
reference: sanitizedRef,
|
|
525
|
+
title,
|
|
526
|
+
url: pageUrl
|
|
527
|
+
}), sanitizedRef, true);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
// Check if Chrome is already running on this port
|
|
532
|
+
// We check both connectionManager (tracked connections) and chromeLauncher (actual process)
|
|
533
|
+
// This handles the case where a tab was closed but Chrome is still running
|
|
534
|
+
const browserAlreadyExists = connectionManager.hasBrowser('localhost', port) || chromeLauncher.isRunning(port);
|
|
535
|
+
await debugLog('index', `browserAlreadyExists: ${browserAlreadyExists} (hasBrowser: ${connectionManager.hasBrowser('localhost', port)}, isRunning: ${chromeLauncher.isRunning(port)})`);
|
|
536
|
+
let isNewBrowser = false;
|
|
537
|
+
// Reusing the Chrome already on this port would silently hand back a
|
|
538
|
+
// different profile than the caller asked for, so only allow it when
|
|
539
|
+
// that instance is genuinely running the requested profile. When there
|
|
540
|
+
// is nothing on the port we would spawn, so a profile held by another
|
|
541
|
+
// live instance is the real conflict (a second Chrome on one
|
|
542
|
+
// user-data-dir is handed off to the first process and ours dies).
|
|
543
|
+
const profileBlocked = await profileGate(browserAlreadyExists ? port : undefined);
|
|
544
|
+
if (profileBlocked) {
|
|
545
|
+
return profileBlocked;
|
|
546
|
+
}
|
|
547
|
+
if (!browserAlreadyExists) {
|
|
548
|
+
await debugLog('index', `Launching new Chrome instance on port ${port}...`);
|
|
549
|
+
// Launch new Chrome instance (will release port reservation)
|
|
550
|
+
// Don't pass URL to launch if auto-connect is enabled - let Puppeteer handle navigation
|
|
551
|
+
// This prevents race condition where Chrome starts loading before monitors are set up
|
|
552
|
+
const launchUrl = autoConnect ? undefined : url;
|
|
553
|
+
const result = await chromeLauncher.launch(port, launchUrl, portReserver, args.headless, args.chromeArgs ?? [], profileName);
|
|
554
|
+
await debugLog('index', `Chrome launched successfully: ${JSON.stringify(result)}`);
|
|
555
|
+
isNewBrowser = true;
|
|
556
|
+
}
|
|
557
|
+
// Auto-connect if requested
|
|
558
|
+
let connectionId;
|
|
559
|
+
let runtimeType;
|
|
560
|
+
let title = 'New Tab';
|
|
561
|
+
let pageUrl = 'about:blank';
|
|
562
|
+
let consoleStats = '';
|
|
563
|
+
let viewportSet;
|
|
564
|
+
let viewportClamped;
|
|
565
|
+
if (autoConnect) {
|
|
566
|
+
try {
|
|
567
|
+
// Chrome is already ready (launch() waits for port binding)
|
|
568
|
+
// Add small delay for new browser to ensure full initialization
|
|
569
|
+
if (isNewBrowser) {
|
|
570
|
+
await debugLog('index', `Waiting 500ms for new Chrome browser to stabilize...`);
|
|
571
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
572
|
+
}
|
|
573
|
+
// Create connection managers for this tab
|
|
574
|
+
const cdpManager = new CDPManager(sourceMapHandler);
|
|
575
|
+
const puppeteerManager = new PuppeteerManager();
|
|
576
|
+
const consoleMonitor = new ConsoleMonitor();
|
|
577
|
+
const networkMonitor = new NetworkMonitor();
|
|
578
|
+
// For existing browser, connect Puppeteer first and create/reuse a tab
|
|
579
|
+
// This ensures we have a target to connect CDP to (handles case where all tabs were closed)
|
|
580
|
+
let targetId;
|
|
581
|
+
if (browserAlreadyExists) {
|
|
582
|
+
await debugLog('index', `Browser exists, connecting Puppeteer and creating new tab first...`);
|
|
583
|
+
await puppeteerManager.connect('localhost', port);
|
|
584
|
+
// Get all existing pages
|
|
585
|
+
const existingPages = await puppeteerManager.getPages();
|
|
586
|
+
await debugLog('index', `Found ${existingPages.length} existing pages`);
|
|
587
|
+
// Create a new page for this connection
|
|
588
|
+
const page = await puppeteerManager.newPage();
|
|
589
|
+
// Close any existing blank pages to avoid clutter
|
|
590
|
+
for (const existingPage of existingPages) {
|
|
591
|
+
try {
|
|
592
|
+
const pageUrl = existingPage.url();
|
|
593
|
+
if (pageUrl === 'about:blank' || pageUrl === 'chrome://newtab/') {
|
|
594
|
+
await debugLog('index', `Closing blank page: ${pageUrl}`);
|
|
595
|
+
await existingPage.close();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
// Page might already be closed, ignore
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Get the target ID of the new page so we can connect CDP to it specifically
|
|
603
|
+
const target = page.target();
|
|
604
|
+
targetId = target._targetId || target._targetInfo?.targetId;
|
|
605
|
+
await debugLog('index', `Created new tab with targetId: ${targetId}`);
|
|
606
|
+
}
|
|
607
|
+
// Connect to CDP (with specific target if we created a new tab)
|
|
608
|
+
await cdpManager.connect('localhost', port, targetId);
|
|
609
|
+
runtimeType = cdpManager.getRuntimeType();
|
|
610
|
+
// Set up pause/resume callbacks to control port monitoring
|
|
611
|
+
const portMonitor = serverManager.getPortMonitor();
|
|
612
|
+
cdpManager.setPauseCallback(() => portMonitor.pauseMonitoring());
|
|
613
|
+
cdpManager.setResumeCallback(() => portMonitor.resumeMonitoring());
|
|
614
|
+
// Connect Puppeteer for Chrome (if not already connected)
|
|
615
|
+
if (runtimeType === 'chrome' && !browserAlreadyExists) {
|
|
616
|
+
await puppeteerManager.connect('localhost', port);
|
|
617
|
+
}
|
|
618
|
+
if (runtimeType === 'chrome') {
|
|
619
|
+
// Start monitoring console and network
|
|
620
|
+
const page = puppeteerManager.getPage();
|
|
621
|
+
consoleMonitor.startMonitoring(page);
|
|
622
|
+
networkMonitor.startMonitoring(page);
|
|
623
|
+
// Register logpoint tracker callback on this connection's console monitor
|
|
624
|
+
consoleMonitor.onMessage((message) => {
|
|
625
|
+
logpointTracker.handleConsoleMessage(message);
|
|
626
|
+
});
|
|
627
|
+
if (args.width !== undefined || args.height !== undefined) {
|
|
628
|
+
const current = await page.evaluate(() => {
|
|
629
|
+
const w = globalThis.window;
|
|
630
|
+
return { width: w.innerWidth, height: w.innerHeight };
|
|
631
|
+
});
|
|
632
|
+
const target = {
|
|
633
|
+
width: args.width ?? current.width,
|
|
634
|
+
height: args.height ?? current.height,
|
|
635
|
+
};
|
|
636
|
+
const sized = await sizeWindowToViewport(page, target, args.headless === true);
|
|
637
|
+
viewportSet = sized.viewport;
|
|
638
|
+
viewportClamped = sized.clampedTo;
|
|
639
|
+
await debugLog('index', `Sized ${sized.mode} to ${sized.viewport.width}x${sized.viewport.height}` +
|
|
640
|
+
(sized.clampedTo ? ` (clamped from ${target.width}x${target.height})` : ''));
|
|
641
|
+
}
|
|
642
|
+
// Navigate to URL if provided
|
|
643
|
+
if (url) {
|
|
644
|
+
await debugLog('index', `Navigating to URL: ${url}`);
|
|
645
|
+
await page.goto(url, { waitUntil: 'load', timeout: 30000 });
|
|
646
|
+
await debugLog('index', `Navigation to ${url} completed`);
|
|
647
|
+
}
|
|
648
|
+
// Auto-reload page to capture initial console logs (only if not navigating and has content)
|
|
649
|
+
const currentUrl = page.url();
|
|
650
|
+
if (!url && currentUrl && currentUrl !== 'about:blank') {
|
|
651
|
+
try {
|
|
652
|
+
await page.reload({ waitUntil: 'load', timeout: 5000 });
|
|
653
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
654
|
+
}
|
|
655
|
+
catch (reloadError) {
|
|
656
|
+
console.error(`[cdp-tools] Warning: Page reload failed: ${reloadError.message}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Get page index for tracking
|
|
661
|
+
const pages = await puppeteerManager.getPages();
|
|
662
|
+
const currentPage = puppeteerManager.getPage();
|
|
663
|
+
const pageIndex = pages.findIndex(p => p === currentPage);
|
|
664
|
+
// Register connection with user-provided reference or default
|
|
665
|
+
// Reference was already validated at the start of the handler
|
|
666
|
+
let connectionReference = UNNAMED_CONNECTION;
|
|
667
|
+
if (userReference) {
|
|
668
|
+
// Use the sanitized version (lowercase with hyphens)
|
|
669
|
+
const validation = validateReference(userReference);
|
|
670
|
+
connectionReference = validation.sanitized;
|
|
671
|
+
}
|
|
672
|
+
connectionId = connectionManager.createConnection(cdpManager, puppeteerManager, consoleMonitor, networkMonitor, 'localhost', port, connectionReference, pageIndex);
|
|
673
|
+
// Update active manager references
|
|
674
|
+
updateActiveManagers(connectionId);
|
|
675
|
+
// Get page info for Chrome connections
|
|
676
|
+
if (runtimeType === 'chrome') {
|
|
677
|
+
const page = puppeteerManager.getPage();
|
|
678
|
+
pageUrl = page.url();
|
|
679
|
+
title = await page.title();
|
|
680
|
+
// Get console stats and update cursor so first tool call doesn't re-report these
|
|
681
|
+
const logStats = consoleMonitor.getLogStats();
|
|
682
|
+
if (logStats.totalMessages > 0) {
|
|
683
|
+
const details = [];
|
|
684
|
+
const errorCount = logStats.newErrors;
|
|
685
|
+
const warnCount = logStats.newWarnings;
|
|
686
|
+
const otherCount = logStats.totalMessages - errorCount - warnCount;
|
|
687
|
+
if (errorCount > 0)
|
|
688
|
+
details.push(`${errorCount} err`);
|
|
689
|
+
if (warnCount > 0)
|
|
690
|
+
details.push(`${warnCount} warn`);
|
|
691
|
+
if (otherCount > 0)
|
|
692
|
+
details.push(`${otherCount} log`);
|
|
693
|
+
consoleStats = `\n**Console:** ${details.join('/')}`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
catch (connectError) {
|
|
698
|
+
// Log the auto-connect failure
|
|
699
|
+
await debugLog('index', `Auto-connect failed: ${connectError}`);
|
|
700
|
+
// If auto-connect fails, return detailed error
|
|
701
|
+
const errorMessage = connectError instanceof Error ? connectError.message : String(connectError);
|
|
702
|
+
return createSuccessResponse('CHROME_LAUNCH_AUTO_CONNECT_FAILED', {
|
|
703
|
+
port: port.toString(),
|
|
704
|
+
error: errorMessage,
|
|
705
|
+
suggestion: 'Chrome launched but auto-connect failed. Try manually connecting with connectDebugger().'
|
|
706
|
+
}, {
|
|
707
|
+
port: port,
|
|
708
|
+
isNewBrowser,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Format response based on whether auto-connect was used
|
|
713
|
+
if (autoConnect) {
|
|
714
|
+
const connection = connectionManager.getConnection(connectionId);
|
|
715
|
+
const reference = connection?.reference || UNNAMED_CONNECTION;
|
|
716
|
+
const inactivityTimeoutMinutes = configManager.getChromeConfig().inactivityTimeoutMinutes;
|
|
717
|
+
const inactivityNote = inactivityTimeoutMinutes > 0
|
|
718
|
+
? `\n\nNote: This connection auto-closes after ${inactivityTimeoutMinutes} min of no tool activity against it. Any tool call using this connectionReason resets the timer.`
|
|
719
|
+
: '';
|
|
720
|
+
return withLaunchMeta(createSuccessResponse('CHROME_LAUNCH_SUCCESS', {
|
|
721
|
+
reference,
|
|
722
|
+
title: title || '(no title)',
|
|
723
|
+
url: pageUrl,
|
|
724
|
+
consoleStats: consoleStats || undefined,
|
|
725
|
+
hasUserReference: !!userReference,
|
|
726
|
+
viewport: viewportSet,
|
|
727
|
+
viewportClamped: viewportClamped ? true : undefined,
|
|
728
|
+
inactivityNote,
|
|
729
|
+
}), reference, false);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
return createSuccessResponse('CHROME_LAUNCH_NO_CONNECT', { port: port.toString() }, { port, isNewBrowser });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
// Lost a race for the profile between the pre-check above and the
|
|
737
|
+
// launcher's own guard - report it as the profile conflict it is.
|
|
738
|
+
if (error instanceof ProfileInUseError) {
|
|
739
|
+
return createErrorResponse('CHROME_PROFILE_IN_USE', {
|
|
740
|
+
profile: error.profile,
|
|
741
|
+
port: error.port.toString(),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
// Held by a Chrome from ANOTHER process (the persistent profile root is
|
|
745
|
+
// global) - we know the PID but not its debug port, so CHROME_PROFILE_IN_USE
|
|
746
|
+
// (which talks in ports) does not fit. Reported with the launcher's own
|
|
747
|
+
// explanation until a dedicated template exists.
|
|
748
|
+
if (error instanceof ProfileLockedError) {
|
|
749
|
+
return createErrorResponse('CHROME_SPAWN_FAILED', { error: error.message });
|
|
750
|
+
}
|
|
751
|
+
return createErrorResponse('CHROME_SPAWN_FAILED', { error: `${error}` });
|
|
752
|
+
}
|
|
753
|
+
}),
|
|
754
|
+
killChrome: createTool('Kill Chrome process', z.object({
|
|
755
|
+
reason: z.string().describe('Why Chrome needs to be killed'),
|
|
756
|
+
port: z.number().optional().describe('Port of specific Chrome instance to kill. If not provided, kills all Chrome instances.'),
|
|
757
|
+
}).strict(), async (args) => {
|
|
758
|
+
try {
|
|
759
|
+
const port = args.port;
|
|
760
|
+
await debugLog('index', `killChrome called - reason: ${args.reason}, port: ${port ?? 'all'}`);
|
|
761
|
+
// Set the close reason before killing (used for close event tracking)
|
|
762
|
+
if (port !== undefined) {
|
|
763
|
+
chromeLauncher.setPendingCloseReason(port, 'manual');
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
for (const p of chromeLauncher.getRunningPorts()) {
|
|
767
|
+
chromeLauncher.setPendingCloseReason(p, 'manual');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// Kill Chrome - the exit callback handles connection cleanup and port re-reservation
|
|
771
|
+
await chromeLauncher.kill(port);
|
|
772
|
+
return createSuccessResponse('CHROME_KILLED', {
|
|
773
|
+
port: port ?? 'all',
|
|
774
|
+
reason: args.reason
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
return createErrorResponse('CHROME_KILL_FAILED', { error: `${error}` });
|
|
779
|
+
}
|
|
780
|
+
}),
|
|
781
|
+
resetChromeLauncher: createTool('Reset Chrome launcher', z.object({
|
|
782
|
+
reason: z.string().describe('Why Chrome launcher needs to be reset'),
|
|
783
|
+
}).strict(), async (args) => {
|
|
784
|
+
// Log the reason for audit purposes
|
|
785
|
+
console.error(`[cdp-tools] resetChromeLauncher called - Reason: ${args.reason}`);
|
|
786
|
+
chromeLauncher.reset();
|
|
787
|
+
return createSuccessResponse('CHROME_LAUNCHER_RESET');
|
|
788
|
+
}),
|
|
789
|
+
getChromeStatus: createTool('Get Chrome status', z.object({}).strict(), async () => {
|
|
790
|
+
const status = chromeLauncher.getStatus();
|
|
791
|
+
// Format for template rendering
|
|
792
|
+
const formattedStatus = {
|
|
793
|
+
...status,
|
|
794
|
+
lastCloseEvents: status.lastCloseEvents.map(event => ({
|
|
795
|
+
...event,
|
|
796
|
+
timestamp: event.timestamp.toISOString(),
|
|
797
|
+
hasExitCode: event.exitCode !== null && event.exitCode !== undefined,
|
|
798
|
+
})),
|
|
799
|
+
};
|
|
800
|
+
return createSuccessResponse('CHROME_STATUS', formattedStatus, status);
|
|
801
|
+
}),
|
|
802
|
+
setDebugLogging: createTool('Toggle debug logging', z.object({
|
|
803
|
+
enabled: z.boolean().describe('Set to true to enable debug logging, false to disable'),
|
|
804
|
+
}).strict(), async (args) => {
|
|
805
|
+
if (args.enabled) {
|
|
806
|
+
await enableDebugLogging(); // Now async to log startup metrics
|
|
807
|
+
return createSuccessResponse('DEBUG_LOGGING_ENABLED', {
|
|
808
|
+
message: 'Debug logging enabled. Logs will be written to .cdp-tools/logs/debug.log'
|
|
809
|
+
}, {
|
|
810
|
+
enabled: true,
|
|
811
|
+
message: 'Debug logging enabled. Logs will be written to .cdp-tools/logs/debug.log'
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
disableDebugLogging();
|
|
816
|
+
return createSuccessResponse('DEBUG_LOGGING_DISABLED', {
|
|
817
|
+
message: 'Debug logging disabled'
|
|
818
|
+
}, {
|
|
819
|
+
enabled: false,
|
|
820
|
+
message: 'Debug logging disabled'
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}),
|
|
824
|
+
getDebugLoggingStatus: createTool('Check debug logging status', z.object({}).strict(), async () => {
|
|
825
|
+
const enabled = isDebugEnabled();
|
|
826
|
+
return createSuccessResponse('DEBUG_LOGGING_STATUS', {
|
|
827
|
+
status: enabled ? 'enabled' : 'disabled',
|
|
828
|
+
enabled, // Pass boolean for conditionals
|
|
829
|
+
logFile: '.cdp-tools/logs/debug.log'
|
|
830
|
+
}, {
|
|
831
|
+
enabled,
|
|
832
|
+
logFile: '.cdp-tools/logs/debug.log'
|
|
833
|
+
});
|
|
834
|
+
}),
|
|
835
|
+
connectDebugger: createTool('Connect to debugger', z.object({
|
|
836
|
+
reference: z.string().describe('3 descriptive words describing this debugging activity'),
|
|
837
|
+
host: z.string().optional().default('localhost').describe('The debugger host (default: localhost)'),
|
|
838
|
+
port: z.number().optional().describe('The debugger port (optional, defaults to this session\'s auto-assigned port). Use this to connect to debuggers on different ports (e.g., Node.js on 9229, Chrome on 9222).'),
|
|
839
|
+
}).strict(), async (args) => {
|
|
840
|
+
// Validate reference
|
|
841
|
+
// Validate and get sanitized reference (throws if invalid)
|
|
842
|
+
const reference = requireValidReference(args.reference);
|
|
843
|
+
// Check for duplicate reference - use validated lookup to auto-cleanup dead connections
|
|
844
|
+
const existingConnection = await connectionManager.findConnectionByReferenceValidated(reference);
|
|
845
|
+
if (existingConnection) {
|
|
846
|
+
return createErrorResponse('REFERENCE_IN_USE', {
|
|
847
|
+
reference
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const host = args.host || 'localhost';
|
|
851
|
+
const port = args.port || configManager.getCurrentPort();
|
|
852
|
+
const defaultPort = configManager.getCurrentPort();
|
|
853
|
+
const isDefaultPort = port === defaultPort;
|
|
854
|
+
await debugLog('index', `connectDebugger called: host=${host}, port=${port}, defaultPort=${defaultPort}`);
|
|
855
|
+
try {
|
|
856
|
+
// Check if Chrome/debugger is running before attempting connection
|
|
857
|
+
await debugLog('index', `Checking if Chrome is running on port ${port}...`);
|
|
858
|
+
const isRunning = await isChromeRunning(port);
|
|
859
|
+
await debugLog('index', `isChromeRunning result: ${isRunning}`);
|
|
860
|
+
if (!isRunning) {
|
|
861
|
+
await debugLog('index', `Chrome not running on port ${port}, returning error`);
|
|
862
|
+
// Provide clear error message based on port type
|
|
863
|
+
if (isDefaultPort && host === 'localhost') {
|
|
864
|
+
return createErrorResponse('DEBUGGER_NOT_RUNNING', {
|
|
865
|
+
port: port.toString(),
|
|
866
|
+
message: `Chrome is not running on port ${port}. Use \`launchChrome()\` to start Chrome first.`
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
return createErrorResponse('DEBUGGER_NOT_RUNNING', {
|
|
871
|
+
port: port.toString(),
|
|
872
|
+
message: `No debugger found on ${host}:${port}. For Chrome, use \`launchChrome({ port: ${port} })\`. For Node.js, start with \`node --inspect=${port} app.js\``
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Check if browser already exists on this port
|
|
877
|
+
const browserAlreadyExists = connectionManager.hasBrowser(host, port);
|
|
878
|
+
// Create new managers for this tab/connection
|
|
879
|
+
const cdpManager = new CDPManager(sourceMapHandler);
|
|
880
|
+
const puppeteerManager = new PuppeteerManager();
|
|
881
|
+
const consoleMonitor = new ConsoleMonitor();
|
|
882
|
+
const networkMonitor = new NetworkMonitor();
|
|
883
|
+
// Connect CDP first to detect runtime type
|
|
884
|
+
await cdpManager.connect(host, port);
|
|
885
|
+
const runtimeType = cdpManager.getRuntimeType();
|
|
886
|
+
// If this port belongs to a server cdp-tools is managing, and its start
|
|
887
|
+
// command looks auto-restarting (--watch, nodemon, etc.), warn: pausing
|
|
888
|
+
// at a breakpoint on that process while it can self-restart on file
|
|
889
|
+
// changes is a known-bad combination.
|
|
890
|
+
let autoRestartWarning = '';
|
|
891
|
+
if (runtimeType === 'node') {
|
|
892
|
+
const managedServer = await serverManager.getManagedServerByInspectorPort(port);
|
|
893
|
+
const autoRestartMatch = managedServer ? detectAutoRestartCommand(managedServer.command) : null;
|
|
894
|
+
if (autoRestartMatch) {
|
|
895
|
+
autoRestartWarning = `\n\n**Warning:** Server "${managedServer.id}" on this port matches "${autoRestartMatch}", which auto-restarts its own process on file changes. Pausing at a breakpoint here while it can self-restart is a known-bad combination (can cause EADDRINUSE crash-loops and ambiguous failed-but-still-listening states). Prefer disabling auto-restart while breakpoint debugging and calling server({ action: 'restart' }) explicitly instead.`;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Set up pause/resume callbacks to control port monitoring
|
|
899
|
+
const portMonitor = serverManager.getPortMonitor();
|
|
900
|
+
cdpManager.setPauseCallback(() => portMonitor.pauseMonitoring());
|
|
901
|
+
cdpManager.setResumeCallback(() => portMonitor.resumeMonitoring());
|
|
902
|
+
const features = ['debugging'];
|
|
903
|
+
// Only connect Puppeteer for Chrome (browser automation)
|
|
904
|
+
if (runtimeType === 'chrome') {
|
|
905
|
+
await puppeteerManager.connect(host, port);
|
|
906
|
+
// Create new tab if browser already existed
|
|
907
|
+
if (browserAlreadyExists) {
|
|
908
|
+
await puppeteerManager.newPage();
|
|
909
|
+
}
|
|
910
|
+
// Start monitoring console and network
|
|
911
|
+
const page = puppeteerManager.getPage();
|
|
912
|
+
consoleMonitor.startMonitoring(page);
|
|
913
|
+
networkMonitor.startMonitoring(page);
|
|
914
|
+
// Auto-reload page to capture initial console logs
|
|
915
|
+
// Skip reload for blank pages (nothing to reload)
|
|
916
|
+
const currentUrl = page.url();
|
|
917
|
+
if (currentUrl && currentUrl !== 'about:blank') {
|
|
918
|
+
try {
|
|
919
|
+
// Use 'load' instead of 'networkidle0' for compatibility with file:// URLs
|
|
920
|
+
await page.reload({ waitUntil: 'load', timeout: 5000 });
|
|
921
|
+
// Wait a bit more for all scripts to execute and errors to fire
|
|
922
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
923
|
+
}
|
|
924
|
+
catch (reloadError) {
|
|
925
|
+
// Log warning but don't fail - page might already be loaded
|
|
926
|
+
console.error(`[cdp-tools] Warning: Page reload failed: ${reloadError.message}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
features.push('browser-automation', 'console-monitoring', 'network-monitoring');
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
// For Node.js debugging, set up console monitoring via CDP Runtime.consoleAPICalled
|
|
933
|
+
// Set up value expander to get full object details (passes maxDepth from consoleMonitor)
|
|
934
|
+
consoleMonitor.setValueExpander((objectId, maxDepth) => cdpManager.expandObjectById(objectId, maxDepth));
|
|
935
|
+
cdpManager.setConsoleMessageCallback((message) => {
|
|
936
|
+
consoleMonitor.addCDPConsoleMessage(message);
|
|
937
|
+
});
|
|
938
|
+
consoleMonitor.enableWithoutPage();
|
|
939
|
+
features.push('console-monitoring');
|
|
940
|
+
}
|
|
941
|
+
// Register logpoint tracker callback on this connection's console monitor
|
|
942
|
+
// This ensures logpoint executions are tracked for both Chrome and Node.js connections
|
|
943
|
+
consoleMonitor.onMessage((message) => {
|
|
944
|
+
logpointTracker.handleConsoleMessage(message);
|
|
945
|
+
});
|
|
946
|
+
// Get page index for tracking
|
|
947
|
+
let pageIndex;
|
|
948
|
+
if (runtimeType === 'chrome') {
|
|
949
|
+
const pages = await puppeteerManager.getPages();
|
|
950
|
+
const currentPage = puppeteerManager.getPage();
|
|
951
|
+
pageIndex = pages.findIndex(p => p === currentPage);
|
|
952
|
+
}
|
|
953
|
+
// Register connection with ConnectionManager
|
|
954
|
+
// Note: consoleMonitor is always passed now (works for both Chrome and Node.js)
|
|
955
|
+
const connectionId = connectionManager.createConnection(cdpManager, runtimeType === 'chrome' ? puppeteerManager : undefined, consoleMonitor, // Always include - works for both Chrome (via Puppeteer) and Node.js (via CDP)
|
|
956
|
+
runtimeType === 'chrome' ? networkMonitor : undefined, host, port, reference, // Set reference from parameter
|
|
957
|
+
pageIndex);
|
|
958
|
+
// Update active manager references
|
|
959
|
+
updateActiveManagers(connectionId);
|
|
960
|
+
// Build console stats for Chrome connections
|
|
961
|
+
let consoleStats;
|
|
962
|
+
if (runtimeType === 'chrome') {
|
|
963
|
+
const connection = connectionManager.getConnection(connectionId);
|
|
964
|
+
if (connection?.consoleMonitor) {
|
|
965
|
+
// Get console stats and update cursor so first tool call doesn't re-report these
|
|
966
|
+
const logStats = connection.consoleMonitor.getLogStats();
|
|
967
|
+
if (logStats.totalMessages > 0) {
|
|
968
|
+
const details = [];
|
|
969
|
+
if (logStats.newErrors > 0)
|
|
970
|
+
details.push(`${logStats.newErrors} err`);
|
|
971
|
+
if (logStats.newWarnings > 0)
|
|
972
|
+
details.push(`${logStats.newWarnings} warn`);
|
|
973
|
+
const otherCount = logStats.totalMessages - logStats.newErrors - logStats.newWarnings;
|
|
974
|
+
if (otherCount > 0)
|
|
975
|
+
details.push(`${otherCount} log`);
|
|
976
|
+
consoleStats = details.join('/');
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return createSuccessResponse('DEBUGGER_CONNECT_SUCCESS', {
|
|
981
|
+
runtimeType,
|
|
982
|
+
host,
|
|
983
|
+
port: port.toString(),
|
|
984
|
+
reference,
|
|
985
|
+
features: features.join(', '),
|
|
986
|
+
consoleStats,
|
|
987
|
+
isChrome: runtimeType === 'chrome',
|
|
988
|
+
isNode: runtimeType === 'node',
|
|
989
|
+
autoRestartWarning,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
return createErrorResponse('DEBUGGER_CONNECT_FAILED', {
|
|
994
|
+
host,
|
|
995
|
+
port: port.toString(),
|
|
996
|
+
error: `${error}`
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}),
|
|
1000
|
+
disconnectDebugger: createTool('Disconnect debugger', z.object({
|
|
1001
|
+
reason: z.string().describe('Why the connection needs to be disconnected'),
|
|
1002
|
+
reference: z.string().describe('3 descriptive words of the connection to disconnect'),
|
|
1003
|
+
}).strict(), async (args) => {
|
|
1004
|
+
// Log the reason for audit purposes
|
|
1005
|
+
console.error(`[cdp-tools] disconnectDebugger called - Reason: ${args.reason}, Reference: ${args.reference}`);
|
|
1006
|
+
// Find connection by reference
|
|
1007
|
+
const connection = connectionManager.findConnectionByReference(args.reference);
|
|
1008
|
+
if (!connection) {
|
|
1009
|
+
return createErrorResponse('CONNECTION_NOT_FOUND', {
|
|
1010
|
+
reference: args.reference
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
const success = await connectionManager.closeConnection(connection.id);
|
|
1014
|
+
if (success) {
|
|
1015
|
+
return createSuccessResponse('DEBUGGER_DISCONNECT_SUCCESS', { reference: args.reference });
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
return createErrorResponse('CONNECTION_SWITCH_FAILED', { reference: args.reference });
|
|
1019
|
+
}
|
|
1020
|
+
}),
|
|
1021
|
+
loadSourceMaps: createTool('Load source maps', z.object({
|
|
1022
|
+
directory: z.string().describe('The directory containing .js.map files'),
|
|
1023
|
+
}).strict(), async (args) => {
|
|
1024
|
+
const { directory } = args;
|
|
1025
|
+
try {
|
|
1026
|
+
const registered = await sourceMapHandler.registerSourceMapsFromDirectory(directory);
|
|
1027
|
+
return createSuccessResponse('SOURCE_MAPS_LOADED', {
|
|
1028
|
+
count: registered.toString(),
|
|
1029
|
+
directory
|
|
1030
|
+
}, { registered, note: 'Source maps registered for lazy loading (will be loaded on demand)' });
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
return createErrorResponse('SOURCE_MAPS_FAILED', { error: `${error}` });
|
|
1034
|
+
}
|
|
1035
|
+
}),
|
|
1036
|
+
getDebuggerStatus: createTool('Get debugger status', z.object({
|
|
1037
|
+
reference: z.string().describe('3 descriptive words of the connection to check'),
|
|
1038
|
+
}).strict(), async (args) => {
|
|
1039
|
+
// Find connection by reference
|
|
1040
|
+
const connection = connectionManager.findConnectionByReference(args.reference);
|
|
1041
|
+
if (!connection) {
|
|
1042
|
+
return createErrorResponse('CONNECTION_NOT_FOUND', {
|
|
1043
|
+
reference: args.reference
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const cdpManager = connection.cdpManager;
|
|
1047
|
+
const puppeteerManager = connection.puppeteerManager;
|
|
1048
|
+
const consoleMonitor = connection.consoleMonitor;
|
|
1049
|
+
const networkMonitor = connection.networkMonitor;
|
|
1050
|
+
const connected = cdpManager.isConnected();
|
|
1051
|
+
const runtimeType = cdpManager.getRuntimeType();
|
|
1052
|
+
const paused = cdpManager.isPaused();
|
|
1053
|
+
const breakpointCounts = cdpManager.getBreakpointCounts();
|
|
1054
|
+
const sourceMaps = sourceMapHandler.getLoadedSourceMaps();
|
|
1055
|
+
const puppeteerConnected = puppeteerManager?.isConnected() || false;
|
|
1056
|
+
const statusData = {
|
|
1057
|
+
reference: connection.reference || UNNAMED_CONNECTION,
|
|
1058
|
+
connected,
|
|
1059
|
+
runtimeType,
|
|
1060
|
+
puppeteerConnected,
|
|
1061
|
+
paused,
|
|
1062
|
+
breakpoints: breakpointCounts.breakpoints,
|
|
1063
|
+
logpoints: breakpointCounts.logpoints,
|
|
1064
|
+
totalBreakpoints: breakpointCounts.total,
|
|
1065
|
+
sourceMapCount: sourceMaps.length,
|
|
1066
|
+
consoleMonitoring: consoleMonitor?.isActive() ? 'active' : 'inactive',
|
|
1067
|
+
networkMonitoring: networkMonitor?.isActive() ? 'active' : 'inactive',
|
|
1068
|
+
totalConnections: connectionManager.getConnectionCount(),
|
|
1069
|
+
};
|
|
1070
|
+
return createSuccessResponse('CONNECTION_STATUS', {}, statusData);
|
|
1071
|
+
}),
|
|
1072
|
+
listConnections: createTool('List debugger connections', z.object({}).strict(), async () => {
|
|
1073
|
+
const connections = connectionManager.listConnections();
|
|
1074
|
+
const activeId = connectionManager.getActiveConnectionId();
|
|
1075
|
+
const activeConnection = activeId ? connectionManager.getConnection(activeId) : null;
|
|
1076
|
+
const activeReference = activeConnection?.reference || UNNAMED_CONNECTION;
|
|
1077
|
+
const connectionList = connections.map(conn => ({
|
|
1078
|
+
reference: conn.reference || UNNAMED_CONNECTION,
|
|
1079
|
+
type: conn.type,
|
|
1080
|
+
host: conn.host,
|
|
1081
|
+
port: conn.port,
|
|
1082
|
+
active: conn.id === activeId,
|
|
1083
|
+
connected: conn.cdpManager.isConnected(),
|
|
1084
|
+
paused: conn.cdpManager.isPaused(),
|
|
1085
|
+
createdAt: new Date(conn.createdAt).toISOString(),
|
|
1086
|
+
}));
|
|
1087
|
+
return createSuccessResponse('CONNECTIONS_LIST', {
|
|
1088
|
+
totalConnections: connections.length.toString()
|
|
1089
|
+
}, {
|
|
1090
|
+
activeReference,
|
|
1091
|
+
connections: connectionList,
|
|
1092
|
+
});
|
|
1093
|
+
}),
|
|
1094
|
+
switchConnection: createTool('Switch debugger connection', z.object({
|
|
1095
|
+
reference: z.string().describe('3 descriptive words of the connection to switch to'),
|
|
1096
|
+
}).strict(), async (args) => {
|
|
1097
|
+
// Find connection by reference
|
|
1098
|
+
const connection = connectionManager.findConnectionByReference(args.reference);
|
|
1099
|
+
if (!connection) {
|
|
1100
|
+
return createErrorResponse('CONNECTION_NOT_FOUND', {
|
|
1101
|
+
reference: args.reference
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
const success = connectionManager.setActiveConnection(connection.id);
|
|
1105
|
+
if (success) {
|
|
1106
|
+
// Update active manager references
|
|
1107
|
+
updateActiveManagers(connection.id);
|
|
1108
|
+
return createSuccessResponse('CONNECTION_SWITCH_SUCCESS', { reference: args.reference });
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
return createErrorResponse('CONNECTION_SWITCH_FAILED', { reference: args.reference });
|
|
1112
|
+
}
|
|
1113
|
+
}),
|
|
1114
|
+
};
|
|
1115
|
+
// Active connection manager references (updated when connection is made/switched)
|
|
1116
|
+
let activeCdpManager = null;
|
|
1117
|
+
let activePuppeteerManager = null;
|
|
1118
|
+
let activeConsoleMonitor = null;
|
|
1119
|
+
let activeNetworkMonitor = null;
|
|
1120
|
+
// Session detection state (set in main, used in tool handler)
|
|
1121
|
+
let sessionDetectorInstance = null;
|
|
1122
|
+
let sessionVerifyStarted = false;
|
|
1123
|
+
// Log processor orchestrator (set in main for hub instances)
|
|
1124
|
+
let orchestratorInstance = null;
|
|
1125
|
+
// Helper to update active manager references
|
|
1126
|
+
const updateActiveManagers = (connectionId) => {
|
|
1127
|
+
const connection = connectionManager.getConnection(connectionId);
|
|
1128
|
+
if (connection) {
|
|
1129
|
+
activeCdpManager = connection.cdpManager;
|
|
1130
|
+
activePuppeteerManager = connection.puppeteerManager || null;
|
|
1131
|
+
activeConsoleMonitor = connection.consoleMonitor || null;
|
|
1132
|
+
activeNetworkMonitor = connection.networkMonitor || null;
|
|
1133
|
+
// Update activity timestamp whenever connection is accessed
|
|
1134
|
+
connectionManager.updateActivity(connection.id);
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
/**
|
|
1138
|
+
* Resolve a connection from a connectionReason (task description)
|
|
1139
|
+
* Sanitizes the reason, looks for existing tab, or creates new one
|
|
1140
|
+
*/
|
|
1141
|
+
async function resolveConnectionFromReason(connectionReason) {
|
|
1142
|
+
// Sanitize: lowercase, trim, spaces to hyphens
|
|
1143
|
+
const reference = connectionReason.toLowerCase().trim().replace(/\s+/g, '-');
|
|
1144
|
+
// Find connection by reference only
|
|
1145
|
+
const connection = connectionManager.findConnectionByReference(reference);
|
|
1146
|
+
// If not found, return null to show error
|
|
1147
|
+
if (!connection) {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
// Update activity timestamp when connection is accessed
|
|
1151
|
+
connectionManager.updateActivity(connection.id);
|
|
1152
|
+
return {
|
|
1153
|
+
connection,
|
|
1154
|
+
cdpManager: connection.cdpManager,
|
|
1155
|
+
puppeteerManager: connection.puppeteerManager || null,
|
|
1156
|
+
consoleMonitor: connection.consoleMonitor || null,
|
|
1157
|
+
networkMonitor: connection.networkMonitor || null,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
// Create proxy managers that delegate to active connection
|
|
1161
|
+
const proxyHandlerForManager = {
|
|
1162
|
+
get(target, prop) {
|
|
1163
|
+
// For CDPManager
|
|
1164
|
+
if (target.constructor.name === 'CDPManager' && activeCdpManager) {
|
|
1165
|
+
return activeCdpManager[prop];
|
|
1166
|
+
}
|
|
1167
|
+
// For PuppeteerManager
|
|
1168
|
+
if (target.constructor.name === 'PuppeteerManager' && activePuppeteerManager) {
|
|
1169
|
+
return activePuppeteerManager[prop];
|
|
1170
|
+
}
|
|
1171
|
+
// For ConsoleMonitor
|
|
1172
|
+
if (target.constructor.name === 'ConsoleMonitor' && activeConsoleMonitor) {
|
|
1173
|
+
return activeConsoleMonitor[prop];
|
|
1174
|
+
}
|
|
1175
|
+
// For NetworkMonitor
|
|
1176
|
+
if (target.constructor.name === 'NetworkMonitor' && activeNetworkMonitor) {
|
|
1177
|
+
return activeNetworkMonitor[prop];
|
|
1178
|
+
}
|
|
1179
|
+
return target[prop];
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
// Create proxy managers
|
|
1183
|
+
const proxyCdpManager = new Proxy(new CDPManager(sourceMapHandler), proxyHandlerForManager);
|
|
1184
|
+
const proxyPuppeteerManager = new Proxy(new PuppeteerManager(), proxyHandlerForManager);
|
|
1185
|
+
const proxyConsoleMonitor = new Proxy(new ConsoleMonitor(), proxyHandlerForManager);
|
|
1186
|
+
const proxyNetworkMonitor = new Proxy(new NetworkMonitor(), proxyHandlerForManager);
|
|
1187
|
+
// Register logpoint tracker callbacks
|
|
1188
|
+
proxyConsoleMonitor.onMessage((message) => {
|
|
1189
|
+
logpointTracker.handleConsoleMessage(message);
|
|
1190
|
+
});
|
|
1191
|
+
logpointTracker.setLimitExceededCallback((metadata) => {
|
|
1192
|
+
proxyCdpManager.handleLogpointLimitExceeded({
|
|
1193
|
+
breakpointId: metadata.breakpointId,
|
|
1194
|
+
url: metadata.url,
|
|
1195
|
+
lineNumber: metadata.lineNumber,
|
|
1196
|
+
logMessage: metadata.logMessage,
|
|
1197
|
+
executionCount: metadata.executionCount,
|
|
1198
|
+
maxExecutions: metadata.maxExecutions,
|
|
1199
|
+
logs: metadata.logs,
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
/**
|
|
1203
|
+
* Execute a tool call - used by replay system
|
|
1204
|
+
*/
|
|
1205
|
+
async function executeToolCall(toolName, params, abortSignal) {
|
|
1206
|
+
const tool = allTools[toolName];
|
|
1207
|
+
if (!tool) {
|
|
1208
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
1209
|
+
}
|
|
1210
|
+
const validation = validateParams(params, tool.zodSchema, toolName);
|
|
1211
|
+
if (!validation.success) {
|
|
1212
|
+
throw new Error(`Validation failed: ${JSON.stringify(validation.error)}`);
|
|
1213
|
+
}
|
|
1214
|
+
const result = await tool.handler(validation.data, abortSignal);
|
|
1215
|
+
// If tool returned an error, throw it as a ToolError so it propagates correctly
|
|
1216
|
+
if (result?.isError) {
|
|
1217
|
+
throw new ToolError(result);
|
|
1218
|
+
}
|
|
1219
|
+
return result;
|
|
1220
|
+
}
|
|
1221
|
+
// Combine all tools (conditionally based on config)
|
|
1222
|
+
const allTools = {
|
|
1223
|
+
// Connection tools (Chrome/debugger)
|
|
1224
|
+
...(configManager.isToolEnabled('connection') ? connectionTools : {}),
|
|
1225
|
+
// Tab Management tools
|
|
1226
|
+
...(configManager.isToolEnabled('tab') ? createTabTools(connectionManager, sourceMapHandler, updateActiveManagers, logpointTracker, serverManager) : {}),
|
|
1227
|
+
// CDP Debugging tools
|
|
1228
|
+
...(configManager.isToolEnabled('breakpoint') ? createBreakpointTools(proxyCdpManager, sourceMapHandler, logpointTracker, resolveConnectionFromReason) : {}),
|
|
1229
|
+
...(configManager.isToolEnabled('execution') ? createExecutionTools(proxyCdpManager, resolveConnectionFromReason, connectionManager, (port) => serverManager.retryPendingRestartByInspectorPort(port)) : {}),
|
|
1230
|
+
...(configManager.isToolEnabled('inspection') ? createInspectionTools(proxyCdpManager, sourceMapHandler, resolveConnectionFromReason) : {}),
|
|
1231
|
+
...(configManager.isToolEnabled('source') ? createSourceTools(proxyCdpManager, sourceMapHandler, resolveConnectionFromReason) : {}),
|
|
1232
|
+
// Browser Automation tools
|
|
1233
|
+
...(configManager.isToolEnabled('console') ? createConsoleTools(proxyPuppeteerManager, proxyConsoleMonitor, resolveConnectionFromReason) : {}),
|
|
1234
|
+
...(configManager.isToolEnabled('network') ? createNetworkTools(proxyPuppeteerManager, proxyNetworkMonitor, resolveConnectionFromReason) : {}),
|
|
1235
|
+
...(configManager.isToolEnabled('page') ? createPageTools(proxyPuppeteerManager, proxyCdpManager, proxyConsoleMonitor, proxyNetworkMonitor, connectionManager, resolveConnectionFromReason, clickableCache, executeToolCall) : {}),
|
|
1236
|
+
...(configManager.isToolEnabled('dom') ? createDOMTools(proxyPuppeteerManager, proxyCdpManager, connectionManager, resolveConnectionFromReason) : {}),
|
|
1237
|
+
...(configManager.isToolEnabled('screenshot') ? createScreenshotTools(proxyPuppeteerManager, proxyCdpManager, connectionManager, resolveConnectionFromReason) : {}),
|
|
1238
|
+
...(configManager.isToolEnabled('input') ? createInputTools(proxyPuppeteerManager, proxyCdpManager, connectionManager, resolveConnectionFromReason) : {}),
|
|
1239
|
+
...(configManager.isToolEnabled('content') ? createContentTools(proxyPuppeteerManager, proxyCdpManager, connectionManager, resolveConnectionFromReason, clickableCache) : {}),
|
|
1240
|
+
...(configManager.isToolEnabled('modal') ? createModalTools(resolveConnectionFromReason) : {}),
|
|
1241
|
+
...(configManager.isToolEnabled('storage') ? createStorageTools(proxyPuppeteerManager, proxyCdpManager, resolveConnectionFromReason) : {}),
|
|
1242
|
+
// Download tools
|
|
1243
|
+
...(configManager.isToolEnabled('download') ? createDownloadTools() : {}),
|
|
1244
|
+
// Request tools (HTTP requests as sequence steps, node or browser destination)
|
|
1245
|
+
...(configManager.isToolEnabled('request') ? createRequestTools(resolveConnectionFromReason) : {}),
|
|
1246
|
+
// Assert tool (inline assertions as sequence steps)
|
|
1247
|
+
...(configManager.isToolEnabled('assert') ? createAssertTools(resolveConnectionFromReason) : {}),
|
|
1248
|
+
// Wait tool (wait primitive for sequences - MCP-side condition polling / sleep)
|
|
1249
|
+
...(configManager.isToolEnabled('wait') ? createWaitTools(resolveConnectionFromReason) : {}),
|
|
1250
|
+
// Replay tools
|
|
1251
|
+
...(configManager.isToolEnabled('replay') ? createReplayTools(commandRecorder, executeToolCall, async (connectionReason) => {
|
|
1252
|
+
const resolved = await resolveConnectionFromReason(connectionReason);
|
|
1253
|
+
if (!resolved?.puppeteerManager)
|
|
1254
|
+
return null;
|
|
1255
|
+
return resolved.puppeteerManager.getPage();
|
|
1256
|
+
}, async (connectionReason) => {
|
|
1257
|
+
const resolved = await resolveConnectionFromReason(connectionReason);
|
|
1258
|
+
return resolved?.connection.port ?? null;
|
|
1259
|
+
// Lazy: allTools is defined below this object literal, so the set of valid
|
|
1260
|
+
// tool names can only be read at call time (bug-010). The explicit return
|
|
1261
|
+
// type is required - without it, allTools appears in its own initializer
|
|
1262
|
+
// and TypeScript cannot infer it (TS7022).
|
|
1263
|
+
}, () => Object.keys(allTools)) : {}),
|
|
1264
|
+
// Server management tools
|
|
1265
|
+
...(configManager.isToolEnabled('server') ? createServerTools(serverManager) : {}),
|
|
1266
|
+
// Config management tools (always enabled - not toggleable)
|
|
1267
|
+
...createConfigTools(chromeLauncher, { version: SERVER_VERSION, ...BUILD_IDENTITY }),
|
|
1268
|
+
// Plugin management tools (always enabled - not toggleable)
|
|
1269
|
+
...createPluginTools(() => orchestratorInstance),
|
|
1270
|
+
// Issues tracking tools
|
|
1271
|
+
...(configManager.isToolEnabled('issues') ? createIssuesTools(executeToolCall, async (name) => {
|
|
1272
|
+
// Helper to get sequence path by name
|
|
1273
|
+
const sequences = commandRecorder.listSequences();
|
|
1274
|
+
const sequence = sequences.find(s => s.name === name || s.id === name);
|
|
1275
|
+
if (!sequence)
|
|
1276
|
+
return null;
|
|
1277
|
+
// Try to find saved file
|
|
1278
|
+
const sequencesDir = commandRecorder.getSequencesDir();
|
|
1279
|
+
const filename = name.replace(/[^a-z0-9-_]/gi, '-').toLowerCase() + '.json';
|
|
1280
|
+
const { join } = await import('path');
|
|
1281
|
+
const { existsSync } = await import('fs');
|
|
1282
|
+
const filepath = join(sequencesDir, filename);
|
|
1283
|
+
if (existsSync(filepath))
|
|
1284
|
+
return filepath;
|
|
1285
|
+
return null;
|
|
1286
|
+
}, async (connectionReason) => {
|
|
1287
|
+
const resolved = await resolveConnectionFromReason(connectionReason);
|
|
1288
|
+
if (!resolved?.puppeteerManager)
|
|
1289
|
+
return null;
|
|
1290
|
+
return resolved.puppeteerManager.getPage();
|
|
1291
|
+
}) : {}),
|
|
1292
|
+
// Dashboard tools (lazy-initialized in main())
|
|
1293
|
+
...(configManager.isToolEnabled('dashboard') ? createDashboardTools() : {}),
|
|
1294
|
+
};
|
|
1295
|
+
/**
|
|
1296
|
+
* Register tool handlers on the server
|
|
1297
|
+
*/
|
|
1298
|
+
function registerToolHandlers(server) {
|
|
1299
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1300
|
+
return {
|
|
1301
|
+
tools: Object.entries(allTools).map(([name, tool]) => ({
|
|
1302
|
+
name,
|
|
1303
|
+
description: tool.description,
|
|
1304
|
+
inputSchema: tool.inputSchema,
|
|
1305
|
+
})),
|
|
1306
|
+
};
|
|
1307
|
+
});
|
|
1308
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1309
|
+
const toolName = request.params.name;
|
|
1310
|
+
const tool = allTools[toolName];
|
|
1311
|
+
if (!tool) {
|
|
1312
|
+
return {
|
|
1313
|
+
content: [
|
|
1314
|
+
{
|
|
1315
|
+
type: 'text',
|
|
1316
|
+
text: JSON.stringify({
|
|
1317
|
+
success: false,
|
|
1318
|
+
error: `Unknown tool: ${toolName}`,
|
|
1319
|
+
code: 'UNKNOWN_TOOL',
|
|
1320
|
+
availableTools: Object.keys(allTools).sort()
|
|
1321
|
+
}, null, 2),
|
|
1322
|
+
},
|
|
1323
|
+
],
|
|
1324
|
+
isError: true
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
// Check for tool dependency conflicts (blocks ALL tools except config)
|
|
1328
|
+
if (toolName !== 'config' && configManager.hasDependencyConflicts()) {
|
|
1329
|
+
const conflicts = configManager.getDependencyConflicts();
|
|
1330
|
+
const configPath = configManager.getStatus().loadedFrom || '.cdp-tools/config.json';
|
|
1331
|
+
return {
|
|
1332
|
+
content: [
|
|
1333
|
+
{
|
|
1334
|
+
type: 'text',
|
|
1335
|
+
text: `Tool dependency conflict - all tools blocked.
|
|
1336
|
+
|
|
1337
|
+
${conflicts.join('\n\n')}
|
|
1338
|
+
|
|
1339
|
+
Edit ${configPath} to resolve, then restart the MCP server.`,
|
|
1340
|
+
},
|
|
1341
|
+
],
|
|
1342
|
+
isError: true
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
// All tools now use Zod validation
|
|
1346
|
+
const validation = validateParams(request.params.arguments || {}, tool.zodSchema, toolName);
|
|
1347
|
+
if (!validation.success) {
|
|
1348
|
+
return {
|
|
1349
|
+
content: [
|
|
1350
|
+
{
|
|
1351
|
+
type: 'text',
|
|
1352
|
+
text: JSON.stringify(validation.error, null, 2),
|
|
1353
|
+
},
|
|
1354
|
+
],
|
|
1355
|
+
isError: true
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
// Record command if recording is active (but don't record replay tool calls)
|
|
1359
|
+
// Capture the command index for the repeat hint
|
|
1360
|
+
let commandIndex = null;
|
|
1361
|
+
if (toolName !== 'replay') {
|
|
1362
|
+
await commandRecorder.recordCommand(toolName, validation.data);
|
|
1363
|
+
commandIndex = commandRecorder.getCurrentHistoryIndex();
|
|
1364
|
+
}
|
|
1365
|
+
// Check for failed monitored ports
|
|
1366
|
+
const portMonitor = serverManager.getPortMonitor();
|
|
1367
|
+
const failedPorts = portMonitor.getFailedPorts();
|
|
1368
|
+
const portCheck = checkPortFailures(failedPorts, toolName);
|
|
1369
|
+
if (portCheck.blocked) {
|
|
1370
|
+
return portCheck.response;
|
|
1371
|
+
}
|
|
1372
|
+
// Check for breakpoint pauses (block tools until acknowledged or resumed)
|
|
1373
|
+
const allConnections = connectionManager.getAllConnections();
|
|
1374
|
+
const breakpointCheck = checkBreakpointPause(allConnections, toolName, (port) => serverManager.getPendingRestartByInspectorPort(port), validation.data?.action);
|
|
1375
|
+
if (breakpointCheck.blocked) {
|
|
1376
|
+
return breakpointCheck.response;
|
|
1377
|
+
}
|
|
1378
|
+
// Check for pending startup failures (block tools until acknowledged)
|
|
1379
|
+
const pendingStartupFailures = serverManager.getPendingStartupFailures();
|
|
1380
|
+
const pendingStartupCheck = checkPendingStartups(pendingStartupFailures, toolName);
|
|
1381
|
+
if (pendingStartupCheck.blocked) {
|
|
1382
|
+
return pendingStartupCheck.response;
|
|
1383
|
+
}
|
|
1384
|
+
// Check for blocking bugs from recordings
|
|
1385
|
+
const bugCheck = await checkBugBlocking(toolName, validation.data);
|
|
1386
|
+
if (bugCheck.blocked) {
|
|
1387
|
+
return bugCheck.response;
|
|
1388
|
+
}
|
|
1389
|
+
// Check for duplicate session (multiple MCPs for same Claude session)
|
|
1390
|
+
const duplicateInfo = getDuplicateSessionInfo();
|
|
1391
|
+
const duplicateCheck = checkDuplicateSession(duplicateInfo, toolName);
|
|
1392
|
+
if (duplicateCheck.blocked) {
|
|
1393
|
+
return duplicateCheck.response;
|
|
1394
|
+
}
|
|
1395
|
+
// Pass validated data to handler
|
|
1396
|
+
try {
|
|
1397
|
+
const result = await tool.handler(validation.data, extra?.signal);
|
|
1398
|
+
// Prepend port failure prefix if any
|
|
1399
|
+
if (portCheck.prefix) {
|
|
1400
|
+
prependToResponse(result, portCheck.prefix);
|
|
1401
|
+
if (portCheck.markAsError) {
|
|
1402
|
+
result.isError = true;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// Prepend breakpoint pause prefix if any (for allowed tools when paused)
|
|
1406
|
+
if (breakpointCheck.prefix) {
|
|
1407
|
+
prependToResponse(result, breakpointCheck.prefix);
|
|
1408
|
+
}
|
|
1409
|
+
// Collect status lines to append to response
|
|
1410
|
+
const statusItems = [];
|
|
1411
|
+
// Append server log status to all tool responses
|
|
1412
|
+
const serverLogStats = serverManager.getLogStats();
|
|
1413
|
+
if (serverLogStats.length > 0) {
|
|
1414
|
+
const parts = serverLogStats
|
|
1415
|
+
.filter(s => s.newStderr > 0 || s.newStdout > 0)
|
|
1416
|
+
.map(s => `${s.serverId} (${s.newStderr} err/${s.newStdout} out)`);
|
|
1417
|
+
if (parts.length > 0) {
|
|
1418
|
+
statusItems.push({ label: 'Server Logs', value: parts.join(' | ') });
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
// Append console log status if tool used a connectionReason
|
|
1422
|
+
const connectionReason = validation.data?.connectionReason;
|
|
1423
|
+
if (connectionReason) {
|
|
1424
|
+
const connection = connectionManager.findConnectionByReference(connectionReason);
|
|
1425
|
+
if (connection?.consoleMonitor) {
|
|
1426
|
+
const logStats = connection.consoleMonitor.getLogStats();
|
|
1427
|
+
if (logStats.newMessages > 0) {
|
|
1428
|
+
const details = [];
|
|
1429
|
+
if (logStats.newErrors > 0)
|
|
1430
|
+
details.push(`${logStats.newErrors} err`);
|
|
1431
|
+
if (logStats.newWarnings > 0)
|
|
1432
|
+
details.push(`${logStats.newWarnings} warn`);
|
|
1433
|
+
const otherCount = logStats.newMessages - logStats.newErrors - logStats.newWarnings;
|
|
1434
|
+
if (otherCount > 0)
|
|
1435
|
+
details.push(`${otherCount} log`);
|
|
1436
|
+
statusItems.push({ label: 'Console', value: details.join('/') });
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
// Add replay hint with history index
|
|
1441
|
+
if (commandIndex !== null) {
|
|
1442
|
+
statusItems.push({
|
|
1443
|
+
label: 'Repeat',
|
|
1444
|
+
value: `\`replay({ action: 'repeat', indices: [${commandIndex}] })\``
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
// Append all status lines if any
|
|
1448
|
+
const statusSuffix = buildStatusSuffix(statusItems);
|
|
1449
|
+
if (statusSuffix) {
|
|
1450
|
+
appendToResponse(result, statusSuffix);
|
|
1451
|
+
}
|
|
1452
|
+
// Always append PID for session file verification
|
|
1453
|
+
// This gets logged by Claude, allowing us to identify our session file
|
|
1454
|
+
// See: src/session-detector.ts for how this is used
|
|
1455
|
+
appendToResponse(result, `\npid:${process.pid}`);
|
|
1456
|
+
// Report action to dashboard (if enabled)
|
|
1457
|
+
const dashboardInst = getDashboardInstance();
|
|
1458
|
+
if (dashboardInst) {
|
|
1459
|
+
if (dashboardInst.hub) {
|
|
1460
|
+
// We're the hub - update our own state
|
|
1461
|
+
const connections = connectionManager.getAllConnections().map(conn => ({
|
|
1462
|
+
reference: conn.reference || conn.id,
|
|
1463
|
+
type: conn.type,
|
|
1464
|
+
state: conn.cdpManager.isPaused() ? 'paused' :
|
|
1465
|
+
(Date.now() - conn.lastActivityAt < 30000) ? 'active' : 'idle',
|
|
1466
|
+
createdAt: conn.createdAt,
|
|
1467
|
+
lastActivityAt: conn.lastActivityAt,
|
|
1468
|
+
}));
|
|
1469
|
+
dashboardInst.hub.updateSelf(connections, {
|
|
1470
|
+
tool: toolName,
|
|
1471
|
+
timestamp: Date.now(),
|
|
1472
|
+
connectionReference: connectionReason,
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
else if (dashboardInst.client) {
|
|
1476
|
+
// We're a client - report to hub
|
|
1477
|
+
dashboardInst.client.reportAction(toolName, connectionReason);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// Start session verification after first tool use
|
|
1481
|
+
// The PID we just appended to the response will be logged by Claude.
|
|
1482
|
+
// Now we watch session files and look for that PID to identify our session.
|
|
1483
|
+
if (sessionDetectorInstance && !sessionVerifyStarted) {
|
|
1484
|
+
sessionVerifyStarted = true;
|
|
1485
|
+
sessionDetectorInstance.verify(process.pid);
|
|
1486
|
+
}
|
|
1487
|
+
return result;
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
// Check for ToolError and return its response directly
|
|
1491
|
+
if (error instanceof ToolError) {
|
|
1492
|
+
return error.response;
|
|
1493
|
+
}
|
|
1494
|
+
// Check for InvalidReferenceError and return its formatted response
|
|
1495
|
+
if (error instanceof InvalidReferenceError) {
|
|
1496
|
+
return error.response;
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
content: [
|
|
1500
|
+
{
|
|
1501
|
+
type: 'text',
|
|
1502
|
+
text: error instanceof Error ? error.message : `${error}`,
|
|
1503
|
+
},
|
|
1504
|
+
],
|
|
1505
|
+
isError: true
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
// Start the server
|
|
1511
|
+
/**
|
|
1512
|
+
* CLI mode: `devharness run <sequenceName> [--connectionReason=X] [--headed] [--keep-chrome]`
|
|
1513
|
+
* Runs a saved sequence directly from the shell, no MCP client needed.
|
|
1514
|
+
* Pre-launches Chrome itself (headless by default, forceNewInstance) so
|
|
1515
|
+
* replay run's own auto-launch (always headed) never triggers.
|
|
1516
|
+
*/
|
|
1517
|
+
async function runCliSequence(argv) {
|
|
1518
|
+
const sequenceName = argv[0];
|
|
1519
|
+
if (!sequenceName || sequenceName.startsWith('--')) {
|
|
1520
|
+
console.error('Usage: devharness run <sequenceName> [--connectionReason=X] [--headed] [--keep-chrome]');
|
|
1521
|
+
process.exit(1);
|
|
1522
|
+
}
|
|
1523
|
+
const flags = new Set(argv.slice(1)
|
|
1524
|
+
.filter(a => a.startsWith('--') && !a.includes('='))
|
|
1525
|
+
.map(a => a.slice(2)));
|
|
1526
|
+
const kv = {};
|
|
1527
|
+
for (const arg of argv.slice(1)) {
|
|
1528
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
1529
|
+
const [key, ...rest] = arg.slice(2).split('=');
|
|
1530
|
+
kv[key] = rest.join('=');
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const headed = flags.has('headed');
|
|
1534
|
+
const keepChrome = flags.has('keep-chrome');
|
|
1535
|
+
const connectionReason = kv.connectionReason || deriveConnectionReference(sequenceName);
|
|
1536
|
+
initializePaths();
|
|
1537
|
+
await configManager.load();
|
|
1538
|
+
// Reserve a Chrome debug port (same retry loop the MCP server bootstrap uses)
|
|
1539
|
+
let reservationSucceeded = false;
|
|
1540
|
+
let attempts = 0;
|
|
1541
|
+
const maxAttempts = 10;
|
|
1542
|
+
while (!reservationSucceeded && attempts < maxAttempts) {
|
|
1543
|
+
const port = await findStartingPort();
|
|
1544
|
+
configManager.setCurrentPort(port);
|
|
1545
|
+
try {
|
|
1546
|
+
await portReserver.reserve(port);
|
|
1547
|
+
reservationSucceeded = true;
|
|
1548
|
+
}
|
|
1549
|
+
catch {
|
|
1550
|
+
attempts++;
|
|
1551
|
+
if (attempts >= maxAttempts) {
|
|
1552
|
+
console.error(`[cdp-tools] Failed to reserve a port after ${maxAttempts} attempts`);
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
process.env.MCP_DEBUG_PORT = String(port + 1);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
const launchResult = await executeToolCall('launchChrome', {
|
|
1560
|
+
reference: connectionReason,
|
|
1561
|
+
headless: !headed,
|
|
1562
|
+
forceNewInstance: true,
|
|
1563
|
+
});
|
|
1564
|
+
if (launchResult?.isError) {
|
|
1565
|
+
console.error(launchResult.content?.[0]?.text || 'Failed to launch Chrome');
|
|
1566
|
+
process.exit(1);
|
|
1567
|
+
}
|
|
1568
|
+
const runResult = await executeToolCall('replay', {
|
|
1569
|
+
action: 'run',
|
|
1570
|
+
// Blocking: the CLI's exit code comes from the run result, and the
|
|
1571
|
+
// process exits right after - a background run would die mid-flight.
|
|
1572
|
+
wait: true,
|
|
1573
|
+
name: sequenceName,
|
|
1574
|
+
connectionReason,
|
|
1575
|
+
killChromeOnFinish: !keepChrome,
|
|
1576
|
+
// A CLI run has nobody to answer a prompt, so a parameterised sequence
|
|
1577
|
+
// must keep its recorded values. Leaving this undefined made every such
|
|
1578
|
+
// sequence exit 1 having executed nothing - a "failure" that is really
|
|
1579
|
+
// the run asking a question into a pipe. Same reason runAll passes it.
|
|
1580
|
+
variables: {},
|
|
1581
|
+
});
|
|
1582
|
+
console.log(runResult?.content?.[0]?.text || '');
|
|
1583
|
+
process.exit(runResult?._meta?.replay?.success === true ? 0 : 1);
|
|
1584
|
+
}
|
|
1585
|
+
catch (error) {
|
|
1586
|
+
console.error(error?.message || String(error));
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
async function main() {
|
|
1591
|
+
// CLI mode bypasses the MCP stdio server entirely - session detection, the
|
|
1592
|
+
// dashboard hub, and the log-processor orchestrator are all multi-session-
|
|
1593
|
+
// coordination features irrelevant to a one-shot process.
|
|
1594
|
+
if (process.argv[2] === 'run') {
|
|
1595
|
+
await runCliSequence(process.argv.slice(3));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
console.error(`[cdp-tools] main() called (PID: ${process.pid})`);
|
|
1599
|
+
// Initialize path configuration early (before any file operations)
|
|
1600
|
+
const pathConfig = initializePaths();
|
|
1601
|
+
console.error(`[cdp-tools] Path config: global=${pathConfig.globalBase}, workingDir=${pathConfig.workingDirBase ?? 'none (using global fallback)'}`);
|
|
1602
|
+
// Clean up stale temp files from previous crashed/killed processes
|
|
1603
|
+
// Run in background - don't block startup
|
|
1604
|
+
Promise.all([
|
|
1605
|
+
cleanupStaleTempFiles(pathConfig.globalBase),
|
|
1606
|
+
pathConfig.workingDirBase ? cleanupStaleTempFiles(pathConfig.workingDirBase) : Promise.resolve({ cleaned: [], errors: [] })
|
|
1607
|
+
]).then(([globalResult, localResult]) => {
|
|
1608
|
+
const totalCleaned = globalResult.cleaned.length + localResult.cleaned.length;
|
|
1609
|
+
if (totalCleaned > 0) {
|
|
1610
|
+
console.error(`[cdp-tools] Cleaned ${totalCleaned} stale temp file(s)`);
|
|
1611
|
+
}
|
|
1612
|
+
}).catch(() => {
|
|
1613
|
+
// Ignore cleanup errors - best effort only
|
|
1614
|
+
});
|
|
1615
|
+
// Pick up sequences edited on disk mid-session, the way a managed dev server
|
|
1616
|
+
// picks up its own sources. Attaches only to directories that already exist;
|
|
1617
|
+
// a later save or load starts it.
|
|
1618
|
+
commandRecorder.startSequenceWatch();
|
|
1619
|
+
// Start non-blocking session detection (polls for file modified after MCP start)
|
|
1620
|
+
const cwd = process.cwd();
|
|
1621
|
+
const mcpStartTime = Date.now();
|
|
1622
|
+
// SessionInfo detected asynchronously - may be undefined until callback fires
|
|
1623
|
+
let detectedSessionInfo;
|
|
1624
|
+
// Dashboard instance - initialized after session is detected
|
|
1625
|
+
let dashboardInstance = null;
|
|
1626
|
+
// Set up session detector (starts monitoring immediately)
|
|
1627
|
+
sessionDetectorInstance = createSessionDetector(cwd);
|
|
1628
|
+
// Helper to convert connections to dashboard format
|
|
1629
|
+
const getConnectionsForDashboard = () => {
|
|
1630
|
+
return connectionManager.getAllConnections().map(conn => ({
|
|
1631
|
+
reference: conn.reference || conn.id,
|
|
1632
|
+
type: conn.type,
|
|
1633
|
+
state: conn.cdpManager.isPaused() ? 'paused' :
|
|
1634
|
+
(Date.now() - conn.lastActivityAt < 30000) ? 'active' : 'idle',
|
|
1635
|
+
createdAt: conn.createdAt,
|
|
1636
|
+
lastActivityAt: conn.lastActivityAt,
|
|
1637
|
+
}));
|
|
1638
|
+
};
|
|
1639
|
+
const sessionStartTime = Date.now() - (performance.now() - STARTUP_TIME);
|
|
1640
|
+
// Helper to start orchestrator when becoming hub
|
|
1641
|
+
const startOrchestrator = async (hub) => {
|
|
1642
|
+
if (orchestratorInstance || !sessionDetectorInstance)
|
|
1643
|
+
return;
|
|
1644
|
+
try {
|
|
1645
|
+
const configDir = join(cwd, '.cdp-tools', 'config');
|
|
1646
|
+
mkdirSync(join(configDir, 'classifiers'), { recursive: true });
|
|
1647
|
+
mkdirSync(join(configDir, 'extractors'), { recursive: true });
|
|
1648
|
+
mkdirSync(join(configDir, 'state-machines'), { recursive: true });
|
|
1649
|
+
mkdirSync(join(configDir, 'dashboard'), { recursive: true });
|
|
1650
|
+
orchestratorInstance = new Orchestrator({
|
|
1651
|
+
source: {
|
|
1652
|
+
mode: 'live',
|
|
1653
|
+
sessionDetector: sessionDetectorInstance
|
|
1654
|
+
},
|
|
1655
|
+
configDir
|
|
1656
|
+
});
|
|
1657
|
+
await orchestratorInstance.start();
|
|
1658
|
+
hub.connectLogProcessor(orchestratorInstance);
|
|
1659
|
+
// Start custom dashboard route loader
|
|
1660
|
+
const dashboardConfigDir = join(configDir, 'dashboard');
|
|
1661
|
+
await hub.startRouteLoader(dashboardConfigDir);
|
|
1662
|
+
await debugLog('log-processor', 'Orchestrator started and connected to dashboard hub');
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
await debugLog('log-processor', `Failed to start orchestrator: ${error}`);
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
// Failover callback - when hub dies, try to become the new hub
|
|
1669
|
+
const handleHubDown = async () => {
|
|
1670
|
+
await debugLog('dashboard', 'Attempting to become new hub...');
|
|
1671
|
+
const currentSession = detectedSessionInfo;
|
|
1672
|
+
const newInstance = await initializeDashboard(process.cwd(), sessionStartTime, getConnectionsForDashboard, currentSession?.sessionId || `pid-${process.pid}`, currentSession?.shortId || `pid-${process.pid}`, handleHubDown // Pass callback again for the new client
|
|
1673
|
+
);
|
|
1674
|
+
if (newInstance) {
|
|
1675
|
+
dashboardInstance = newInstance;
|
|
1676
|
+
setDashboardInstance(newInstance);
|
|
1677
|
+
await debugLog('dashboard', `Failover: now ${newInstance.type} on port ${newInstance.port}`);
|
|
1678
|
+
// If we became the hub, start the orchestrator
|
|
1679
|
+
if (newInstance.hub) {
|
|
1680
|
+
await startOrchestrator(newInstance.hub);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
// Initialize dashboard immediately (don't wait for session detection)
|
|
1685
|
+
if (configManager.isToolEnabled('dashboard')) {
|
|
1686
|
+
dashboardInstance = await initializeDashboard(process.cwd(), sessionStartTime, getConnectionsForDashboard, `pid-${process.pid}`, // Placeholder until session detected
|
|
1687
|
+
`pid-${process.pid}`, handleHubDown);
|
|
1688
|
+
if (dashboardInstance) {
|
|
1689
|
+
setDashboardInstance(dashboardInstance);
|
|
1690
|
+
await debugLog('dashboard', `Initialized as ${dashboardInstance.type} on port ${dashboardInstance.port}`);
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
await debugLog('dashboard', `Initialization failed`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
// Subscribe to session changes - update session info when detected
|
|
1697
|
+
sessionDetectorInstance.session$.subscribe(async (sessionInfo) => {
|
|
1698
|
+
await debugLog('session-detector', `Session ID: ${sessionInfo.shortId} (${sessionInfo.sessionId})`);
|
|
1699
|
+
detectedSessionInfo = sessionInfo;
|
|
1700
|
+
setSessionInfo(sessionInfo);
|
|
1701
|
+
// Update dashboard with real session info
|
|
1702
|
+
await debugLog('dashboard', `Updating session info: instance=${!!dashboardInstance}, hub=${!!dashboardInstance?.hub}, client=${!!dashboardInstance?.client}`);
|
|
1703
|
+
if (dashboardInstance?.hub) {
|
|
1704
|
+
dashboardInstance.hub.updateSessionInfo(sessionInfo.sessionId, sessionInfo.shortId);
|
|
1705
|
+
await debugLog('dashboard', `Hub session info updated to ${sessionInfo.shortId}`);
|
|
1706
|
+
}
|
|
1707
|
+
else if (dashboardInstance?.client) {
|
|
1708
|
+
dashboardInstance.client.updateSessionInfo(sessionInfo.sessionId, sessionInfo.shortId);
|
|
1709
|
+
await debugLog('dashboard', `Client session info updated to ${sessionInfo.shortId}`);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
// Subscribe to entry count changes - update dashboard
|
|
1713
|
+
sessionDetectorInstance.entryCount$.subscribe(async (count) => {
|
|
1714
|
+
if (dashboardInstance?.hub) {
|
|
1715
|
+
dashboardInstance.hub.updateSessionEntryCount(count);
|
|
1716
|
+
}
|
|
1717
|
+
else if (dashboardInstance?.client) {
|
|
1718
|
+
dashboardInstance.client.updateSessionEntryCount(count);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
// Load configuration early so debug logging is available for orchestrator startup
|
|
1722
|
+
await configManager.load();
|
|
1723
|
+
const debugConfig = configManager.getDebugConfig();
|
|
1724
|
+
if (debugConfig.enabled) {
|
|
1725
|
+
await enableDebugLogging({ clearOnStartup: true });
|
|
1726
|
+
}
|
|
1727
|
+
if (debugConfig.historyLogEnabled) {
|
|
1728
|
+
enableHistoryLogging();
|
|
1729
|
+
}
|
|
1730
|
+
// Hot-reload config.json edits made after startup (see config.ts reload()
|
|
1731
|
+
// for what can/can't apply live - tool enable/disable still needs a restart).
|
|
1732
|
+
configManager.startWatching();
|
|
1733
|
+
// Initialize log processor orchestrator (hub only)
|
|
1734
|
+
if (dashboardInstance?.hub) {
|
|
1735
|
+
await startOrchestrator(dashboardInstance.hub);
|
|
1736
|
+
}
|
|
1737
|
+
// Capture import time (time from script start to main() being called)
|
|
1738
|
+
const importTime = performance.now() - STARTUP_TIME;
|
|
1739
|
+
// Initialize and reserve debug port with retry logic
|
|
1740
|
+
const portReservationStart = performance.now();
|
|
1741
|
+
let reservationSucceeded = false;
|
|
1742
|
+
let attempts = 0;
|
|
1743
|
+
const maxAttempts = 10;
|
|
1744
|
+
while (!reservationSucceeded && attempts < maxAttempts) {
|
|
1745
|
+
const port = await findStartingPort();
|
|
1746
|
+
configManager.setCurrentPort(port);
|
|
1747
|
+
// Reserve the port by binding a socket to it
|
|
1748
|
+
try {
|
|
1749
|
+
await portReserver.reserve(port);
|
|
1750
|
+
console.error(`[cdp-tools] Reserved debug port: ${port}`);
|
|
1751
|
+
reservationSucceeded = true;
|
|
1752
|
+
}
|
|
1753
|
+
catch (error) {
|
|
1754
|
+
attempts++;
|
|
1755
|
+
console.error(`[cdp-tools] Port ${port} reservation failed (attempt ${attempts}/${maxAttempts}), trying next port...`);
|
|
1756
|
+
if (attempts >= maxAttempts) {
|
|
1757
|
+
console.error(`[cdp-tools] Failed to reserve a port after ${maxAttempts} attempts`);
|
|
1758
|
+
process.exit(1);
|
|
1759
|
+
}
|
|
1760
|
+
// Try the next port
|
|
1761
|
+
process.env.MCP_DEBUG_PORT = String(port + 1);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const portReservationTime = performance.now() - portReservationStart;
|
|
1765
|
+
// Create server with instructions
|
|
1766
|
+
const serverCreationStart = performance.now();
|
|
1767
|
+
const server = await createMCPServer();
|
|
1768
|
+
const serverCreationTime = performance.now() - serverCreationStart;
|
|
1769
|
+
// Register tool handlers
|
|
1770
|
+
const toolRegistrationStart = performance.now();
|
|
1771
|
+
registerToolHandlers(server);
|
|
1772
|
+
const toolRegistrationTime = performance.now() - toolRegistrationStart;
|
|
1773
|
+
// Connect to transport
|
|
1774
|
+
console.error(`[cdp-tools] Connecting to transport (PID: ${process.pid})`);
|
|
1775
|
+
const transportStart = performance.now();
|
|
1776
|
+
const transport = new StdioServerTransport();
|
|
1777
|
+
await server.connect(transport);
|
|
1778
|
+
const transportTime = performance.now() - transportStart;
|
|
1779
|
+
console.error(`[cdp-tools] Transport connected (PID: ${process.pid})`);
|
|
1780
|
+
// Note: Config was already loaded earlier (before orchestrator startup) for debug logging
|
|
1781
|
+
// Announce this session BEFORE the manager decides anything: collection asks
|
|
1782
|
+
// whether another live session is working in a server's directory, and a
|
|
1783
|
+
// session that has not registered yet is invisible to that question.
|
|
1784
|
+
serverClaims.collectDeadSessions();
|
|
1785
|
+
await serverClaims.registerSession(process.cwd());
|
|
1786
|
+
// Initialize server manager - recover running servers and start auto-run servers
|
|
1787
|
+
const serverInitResult = await serverManager.initialize();
|
|
1788
|
+
if (serverInitResult.recovered.length > 0) {
|
|
1789
|
+
console.error(`[cdp-tools] Recovered ${serverInitResult.recovered.length} running server(s): ${serverInitResult.recovered.join(', ')}`);
|
|
1790
|
+
}
|
|
1791
|
+
if (serverInitResult.started.length > 0) {
|
|
1792
|
+
console.error(`[cdp-tools] Auto-started ${serverInitResult.started.length} server(s): ${serverInitResult.started.join(', ')}`);
|
|
1793
|
+
}
|
|
1794
|
+
if (serverInitResult.failed.length > 0) {
|
|
1795
|
+
console.error(`[cdp-tools] Failed to auto-start ${serverInitResult.failed.length} server(s): ${serverInitResult.failed.join(', ')}`);
|
|
1796
|
+
}
|
|
1797
|
+
// Dashboard is initialized in session detection callback after sessionId is known
|
|
1798
|
+
console.error(`[cdp-tools] Server ready (PID: ${process.pid})`);
|
|
1799
|
+
// Calculate total startup time and store metrics for later logging
|
|
1800
|
+
const totalStartupTime = performance.now() - STARTUP_TIME;
|
|
1801
|
+
setStartupMetrics({
|
|
1802
|
+
totalMs: Math.round(totalStartupTime),
|
|
1803
|
+
importMs: Math.round(importTime),
|
|
1804
|
+
portReservationMs: Math.round(portReservationTime),
|
|
1805
|
+
portAttempts: attempts + 1,
|
|
1806
|
+
serverCreationMs: Math.round(serverCreationTime),
|
|
1807
|
+
toolRegistrationMs: Math.round(toolRegistrationTime),
|
|
1808
|
+
transportMs: Math.round(transportTime),
|
|
1809
|
+
capturedAt: new Date().toISOString(),
|
|
1810
|
+
});
|
|
1811
|
+
// Start periodic cleanup of inactive connections
|
|
1812
|
+
const chromeConfig = configManager.getChromeConfig();
|
|
1813
|
+
const CLEANUP_INTERVAL = chromeConfig.inactivityPollingMinutes * 60 * 1000;
|
|
1814
|
+
const INACTIVITY_THRESHOLD = chromeConfig.inactivityTimeoutMinutes * 60 * 1000;
|
|
1815
|
+
// Only start cleanup interval if inactivity timeout is enabled (> 0)
|
|
1816
|
+
const cleanupInterval = INACTIVITY_THRESHOLD > 0 ? setInterval(async () => {
|
|
1817
|
+
try {
|
|
1818
|
+
const inactiveConnections = connectionManager.getInactiveConnections(INACTIVITY_THRESHOLD);
|
|
1819
|
+
if (inactiveConnections.length > 0) {
|
|
1820
|
+
await debugLog('index', `Found ${inactiveConnections.length} inactive connection(s) to close`);
|
|
1821
|
+
for (const conn of inactiveConnections) {
|
|
1822
|
+
await debugLog('index', `Closing inactive connection: ${conn.id} (inactive for ${Math.round(conn.inactiveForMs / 1000)}s)`);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
// closeInactiveConnections() now does the activity re-check and Chrome kill itself
|
|
1826
|
+
// (per-connection, before tearing down its monitors, and correctly tagged as
|
|
1827
|
+
// 'inactivity' - see ConnectionManager.closeConnection). What's left here is just a
|
|
1828
|
+
// backstop for Chrome instances with no tracked connection at all (e.g. launched with
|
|
1829
|
+
// autoConnect: false and never connected, or orphaned by some other cleanup path).
|
|
1830
|
+
const closedCount = await connectionManager.closeInactiveConnections(INACTIVITY_THRESHOLD);
|
|
1831
|
+
if (closedCount > 0) {
|
|
1832
|
+
console.error(`[cdp-tools] Closed ${closedCount} inactive connection(s)`);
|
|
1833
|
+
await debugLog('index', `Closed ${closedCount} inactive connection(s)`);
|
|
1834
|
+
}
|
|
1835
|
+
for (const port of chromeLauncher.getRunningPorts()) {
|
|
1836
|
+
if (!connectionManager.hasBrowser('localhost', port)) {
|
|
1837
|
+
console.error(`[cdp-tools] Killing orphaned Chrome on port ${port} (no tracked connections)`);
|
|
1838
|
+
await debugLog('index', `Killing orphaned Chrome on port ${port} (no tracked connections) due to inactivity`);
|
|
1839
|
+
chromeLauncher.setPendingCloseReason(port, 'inactivity');
|
|
1840
|
+
await chromeLauncher.kill(port);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
catch (error) {
|
|
1845
|
+
console.error(`[cdp-tools] Error during cleanup: ${error}`);
|
|
1846
|
+
await debugLog('index', `Error during inactivity cleanup: ${error}`);
|
|
1847
|
+
}
|
|
1848
|
+
}, CLEANUP_INTERVAL) : null;
|
|
1849
|
+
// Cleanup function for graceful shutdown
|
|
1850
|
+
let isCleaningUp = false;
|
|
1851
|
+
/**
|
|
1852
|
+
* Releases what this session privately holds: CDP connections, the Chrome
|
|
1853
|
+
* instances it launched, source maps, its reserved debug port, and the
|
|
1854
|
+
* in-process monitors. All of that dies with this process anyway.
|
|
1855
|
+
*
|
|
1856
|
+
* @param releaseOwnedServers - also stop the managed dev servers this
|
|
1857
|
+
* session owns, meaning the ones no other live session claims (see
|
|
1858
|
+
* server-claims.ts). Only for teardowns where the session is going away -
|
|
1859
|
+
* an idle suspend, or a client that closed. A rebuild restart passes
|
|
1860
|
+
* false: the child is coming straight back and will reattach.
|
|
1861
|
+
*/
|
|
1862
|
+
const cleanup = async (signal, releaseOwnedServers = false) => {
|
|
1863
|
+
if (isCleaningUp) {
|
|
1864
|
+
return; // Prevent multiple cleanup calls
|
|
1865
|
+
}
|
|
1866
|
+
isCleaningUp = true;
|
|
1867
|
+
console.error(`[cdp-tools] Received ${signal}, cleaning up...`);
|
|
1868
|
+
try {
|
|
1869
|
+
if (cleanupInterval)
|
|
1870
|
+
clearInterval(cleanupInterval); // Stop periodic cleanup
|
|
1871
|
+
// Dev servers go first: they are detached, so if anything below hangs
|
|
1872
|
+
// (closing a CDP connection to a dead socket is the usual suspect) and
|
|
1873
|
+
// the supervisor's grace period runs out, the SIGKILL that follows would
|
|
1874
|
+
// leave them running with nobody managing them.
|
|
1875
|
+
if (releaseOwnedServers) {
|
|
1876
|
+
try {
|
|
1877
|
+
const { stopped, keptForOthers } = await serverManager.stopOwnedServers();
|
|
1878
|
+
if (stopped.length > 0) {
|
|
1879
|
+
console.error(`[cdp-tools] Stopped ${stopped.length} owned dev server(s): ${stopped.join(', ')}`);
|
|
1880
|
+
}
|
|
1881
|
+
if (keptForOthers.length > 0) {
|
|
1882
|
+
console.error(`[cdp-tools] Left ${keptForOthers.length} dev server(s) for other sessions: ${keptForOthers.join(', ')}`);
|
|
1883
|
+
}
|
|
1884
|
+
await serverManager.getPortMonitor().stopAll();
|
|
1885
|
+
}
|
|
1886
|
+
catch (error) {
|
|
1887
|
+
console.error(`[cdp-tools] Error releasing dev servers: ${error}`);
|
|
1888
|
+
}
|
|
1889
|
+
// This session is over: nothing of ours may keep pinning a server that
|
|
1890
|
+
// the next session would otherwise collect.
|
|
1891
|
+
serverClaims.releaseAllOwn();
|
|
1892
|
+
serverClaims.unregisterSession();
|
|
1893
|
+
}
|
|
1894
|
+
else {
|
|
1895
|
+
// A rebuild restart: the child is coming straight back, so its claims
|
|
1896
|
+
// stay. Presence is withdrawn and re-announced by the next child.
|
|
1897
|
+
serverClaims.unregisterSession();
|
|
1898
|
+
}
|
|
1899
|
+
await connectionManager.closeAll();
|
|
1900
|
+
sourceMapHandler.clear();
|
|
1901
|
+
await chromeLauncher.kill();
|
|
1902
|
+
await portReserver.release();
|
|
1903
|
+
// Stop session detector if running
|
|
1904
|
+
if (sessionDetectorInstance) {
|
|
1905
|
+
sessionDetectorInstance.stop();
|
|
1906
|
+
}
|
|
1907
|
+
// Stop log processor orchestrator if running
|
|
1908
|
+
if (orchestratorInstance) {
|
|
1909
|
+
orchestratorInstance.stop();
|
|
1910
|
+
}
|
|
1911
|
+
// Stop file watcher if running
|
|
1912
|
+
if (dashboardInstance?._stopFileWatcher) {
|
|
1913
|
+
dashboardInstance._stopFileWatcher();
|
|
1914
|
+
}
|
|
1915
|
+
await shutdownDashboard(dashboardInstance);
|
|
1916
|
+
// Final sync cleanup of temp files before exit
|
|
1917
|
+
const globalCleaned = cleanupStaleTempFilesSync(pathConfig.globalBase, 0);
|
|
1918
|
+
const localCleaned = pathConfig.workingDirBase
|
|
1919
|
+
? cleanupStaleTempFilesSync(pathConfig.workingDirBase, 0)
|
|
1920
|
+
: { cleaned: [] };
|
|
1921
|
+
const totalCleaned = globalCleaned.cleaned.length + localCleaned.cleaned.length;
|
|
1922
|
+
if (totalCleaned > 0) {
|
|
1923
|
+
console.error(`[cdp-tools] Cleaned ${totalCleaned} temp file(s) on shutdown`);
|
|
1924
|
+
}
|
|
1925
|
+
console.error('[cdp-tools] Cleanup complete');
|
|
1926
|
+
}
|
|
1927
|
+
catch (error) {
|
|
1928
|
+
console.error(`[cdp-tools] Cleanup error: ${error}`);
|
|
1929
|
+
}
|
|
1930
|
+
process.exit(0);
|
|
1931
|
+
};
|
|
1932
|
+
// Handle various termination signals
|
|
1933
|
+
process.on('SIGINT', () => cleanup('SIGINT')); // Ctrl+C
|
|
1934
|
+
process.on('SIGTERM', () => cleanup('SIGTERM')); // Graceful shutdown (systemd, Docker, etc.)
|
|
1935
|
+
process.on('SIGHUP', () => cleanup('SIGHUP')); // Terminal hangup
|
|
1936
|
+
// The supervisor's release signal: this session is going away (idle
|
|
1937
|
+
// suspend, or its client closed), so the dev servers it owns go with it.
|
|
1938
|
+
// SIGTERM stays the shallow teardown, because a rebuild restart uses it and
|
|
1939
|
+
// must leave servers running for the next child to reattach to. (SIGUSR2
|
|
1940
|
+
// rather than SIGUSR1, which Node reserves for its own inspector.)
|
|
1941
|
+
process.on('SIGUSR2', () => cleanup('SIGUSR2 (session release)', true));
|
|
1942
|
+
// Handle stdin close - this catches when the parent process (Claude Code) terminates
|
|
1943
|
+
// without sending a signal. MCP uses stdin/stdout for communication, so if stdin closes,
|
|
1944
|
+
// the parent is gone and we should clean up.
|
|
1945
|
+
process.stdin.on('close', () => cleanup('stdin-close'));
|
|
1946
|
+
process.stdin.on('end', () => cleanup('stdin-end'));
|
|
1947
|
+
// Handle normal exit (catch-all)
|
|
1948
|
+
process.on('exit', () => {
|
|
1949
|
+
if (!isCleaningUp) {
|
|
1950
|
+
console.error('[cdp-tools] Process exiting');
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
// Catch uncaught exceptions and unhandled rejections for debugging.
|
|
1954
|
+
//
|
|
1955
|
+
// This handler must NOT re-enter the V8 inspector console path that can itself
|
|
1956
|
+
// throw (issue #74): a page emitting frequent `console.error` could make a
|
|
1957
|
+
// `console.error(error)` here re-trigger `uncaughtException` in a tight ~1ms
|
|
1958
|
+
// loop that saturates a CPU core forever. So we:
|
|
1959
|
+
// 1. write raw to stderr (no `console` / inspector hook),
|
|
1960
|
+
// 2. wrap all logging in try/catch (touching `error.stack` can itself throw
|
|
1961
|
+
// via a custom Error.prepareStackTrace / inspector wrapper),
|
|
1962
|
+
// 3. dedupe and hard-exit if the SAME exception storms, as a last-resort
|
|
1963
|
+
// circuit breaker.
|
|
1964
|
+
const writeErr = (line) => { try {
|
|
1965
|
+
process.stderr.write(line + '\n');
|
|
1966
|
+
}
|
|
1967
|
+
catch { /* never crash the handler */ } };
|
|
1968
|
+
let lastUncaughtMsg = '';
|
|
1969
|
+
let lastUncaughtAt = 0;
|
|
1970
|
+
let uncaughtCount = 0;
|
|
1971
|
+
process.on('uncaughtException', (error, origin) => {
|
|
1972
|
+
const now = Date.now();
|
|
1973
|
+
const msg = (error && error.message) ? error.message : String(error);
|
|
1974
|
+
if (msg === lastUncaughtMsg && now - lastUncaughtAt < 1000) {
|
|
1975
|
+
if (++uncaughtCount > 50) {
|
|
1976
|
+
writeErr('[cdp-tools] Same uncaught exception >50x in <1s — exiting to break the loop.');
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
lastUncaughtMsg = msg;
|
|
1982
|
+
lastUncaughtAt = now;
|
|
1983
|
+
uncaughtCount = 0;
|
|
1984
|
+
writeErr(`[cdp-tools] UNCAUGHT EXCEPTION (${origin}): ${msg}`);
|
|
1985
|
+
try {
|
|
1986
|
+
const s = error?.stack;
|
|
1987
|
+
if (s)
|
|
1988
|
+
writeErr(`[cdp-tools] Stack: ${s}`);
|
|
1989
|
+
}
|
|
1990
|
+
catch { /* stack getter can throw */ }
|
|
1991
|
+
});
|
|
1992
|
+
process.on('unhandledRejection', (reason) => {
|
|
1993
|
+
const msg = (reason && reason.message) ? reason.message : String(reason);
|
|
1994
|
+
writeErr(`[cdp-tools] UNHANDLED REJECTION: ${msg}`);
|
|
1995
|
+
try {
|
|
1996
|
+
const s = reason?.stack;
|
|
1997
|
+
if (s)
|
|
1998
|
+
writeErr(`[cdp-tools] Stack: ${s}`);
|
|
1999
|
+
}
|
|
2000
|
+
catch { /* stack getter can throw */ }
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
main().catch((error) => {
|
|
2004
|
+
console.error('Server error:', error);
|
|
2005
|
+
process.exit(1);
|
|
2006
|
+
});
|
|
2007
|
+
//# sourceMappingURL=index.js.map
|