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.
Files changed (627) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/build/atomic-write.d.ts +28 -0
  4. package/build/atomic-write.d.ts.map +1 -0
  5. package/build/atomic-write.js +126 -0
  6. package/build/atomic-write.js.map +1 -0
  7. package/build/cdp-manager.d.ts +524 -0
  8. package/build/cdp-manager.d.ts.map +1 -0
  9. package/build/cdp-manager.js +1828 -0
  10. package/build/cdp-manager.js.map +1 -0
  11. package/build/cdp-manager.test.d.ts +6 -0
  12. package/build/cdp-manager.test.d.ts.map +1 -0
  13. package/build/cdp-manager.test.js +546 -0
  14. package/build/cdp-manager.test.js.map +1 -0
  15. package/build/chrome-launcher.d.ts +400 -0
  16. package/build/chrome-launcher.d.ts.map +1 -0
  17. package/build/chrome-launcher.js +1071 -0
  18. package/build/chrome-launcher.js.map +1 -0
  19. package/build/chrome-launcher.test.d.ts +11 -0
  20. package/build/chrome-launcher.test.d.ts.map +1 -0
  21. package/build/chrome-launcher.test.js +204 -0
  22. package/build/chrome-launcher.test.js.map +1 -0
  23. package/build/clickable-cache.d.ts +73 -0
  24. package/build/clickable-cache.d.ts.map +1 -0
  25. package/build/clickable-cache.js +90 -0
  26. package/build/clickable-cache.js.map +1 -0
  27. package/build/command-recorder.d.ts +401 -0
  28. package/build/command-recorder.d.ts.map +1 -0
  29. package/build/command-recorder.js +701 -0
  30. package/build/command-recorder.js.map +1 -0
  31. package/build/config.d.ts +390 -0
  32. package/build/config.d.ts.map +1 -0
  33. package/build/config.js +826 -0
  34. package/build/config.js.map +1 -0
  35. package/build/config.test.d.ts +2 -0
  36. package/build/config.test.d.ts.map +1 -0
  37. package/build/config.test.js +112 -0
  38. package/build/config.test.js.map +1 -0
  39. package/build/connection-manager.d.ts +159 -0
  40. package/build/connection-manager.d.ts.map +1 -0
  41. package/build/connection-manager.js +432 -0
  42. package/build/connection-manager.js.map +1 -0
  43. package/build/connection-manager.test.d.ts +11 -0
  44. package/build/connection-manager.test.d.ts.map +1 -0
  45. package/build/connection-manager.test.js +114 -0
  46. package/build/connection-manager.test.js.map +1 -0
  47. package/build/console-monitor.d.ts +150 -0
  48. package/build/console-monitor.d.ts.map +1 -0
  49. package/build/console-monitor.js +454 -0
  50. package/build/console-monitor.js.map +1 -0
  51. package/build/dashboard/bundle.js +1 -0
  52. package/build/dashboard/dashboard-client.d.ts +42 -0
  53. package/build/dashboard/dashboard-client.d.ts.map +1 -0
  54. package/build/dashboard/dashboard-client.js +184 -0
  55. package/build/dashboard/dashboard-client.js.map +1 -0
  56. package/build/dashboard/dashboard-hub.d.ts +50 -0
  57. package/build/dashboard/dashboard-hub.d.ts.map +1 -0
  58. package/build/dashboard/dashboard-hub.js +654 -0
  59. package/build/dashboard/dashboard-hub.js.map +1 -0
  60. package/build/dashboard/hub-lock.d.ts +16 -0
  61. package/build/dashboard/hub-lock.d.ts.map +1 -0
  62. package/build/dashboard/hub-lock.js +77 -0
  63. package/build/dashboard/hub-lock.js.map +1 -0
  64. package/build/dashboard/index.d.ts +25 -0
  65. package/build/dashboard/index.d.ts.map +1 -0
  66. package/build/dashboard/index.js +77 -0
  67. package/build/dashboard/index.js.map +1 -0
  68. package/build/dashboard/types.d.ts +83 -0
  69. package/build/dashboard/types.d.ts.map +1 -0
  70. package/build/dashboard/types.js +6 -0
  71. package/build/dashboard/types.js.map +1 -0
  72. package/build/debug-logger.d.ts +85 -0
  73. package/build/debug-logger.d.ts.map +1 -0
  74. package/build/debug-logger.js +195 -0
  75. package/build/debug-logger.js.map +1 -0
  76. package/build/debugger-aware-wrapper.d.ts +37 -0
  77. package/build/debugger-aware-wrapper.d.ts.map +1 -0
  78. package/build/debugger-aware-wrapper.js +128 -0
  79. package/build/debugger-aware-wrapper.js.map +1 -0
  80. package/build/dom-change-monitor.d.ts +92 -0
  81. package/build/dom-change-monitor.d.ts.map +1 -0
  82. package/build/dom-change-monitor.js +437 -0
  83. package/build/dom-change-monitor.js.map +1 -0
  84. package/build/element-collector.d.ts +16 -0
  85. package/build/element-collector.d.ts.map +1 -0
  86. package/build/element-collector.js +207 -0
  87. package/build/element-collector.js.map +1 -0
  88. package/build/error-helpers.d.ts +43 -0
  89. package/build/error-helpers.d.ts.map +1 -0
  90. package/build/error-helpers.js +143 -0
  91. package/build/error-helpers.js.map +1 -0
  92. package/build/formatters/console-formatter.d.ts +160 -0
  93. package/build/formatters/console-formatter.d.ts.map +1 -0
  94. package/build/formatters/console-formatter.js +338 -0
  95. package/build/formatters/console-formatter.js.map +1 -0
  96. package/build/helpers/parser-plugins.d.ts +44 -0
  97. package/build/helpers/parser-plugins.d.ts.map +1 -0
  98. package/build/helpers/parser-plugins.js +102 -0
  99. package/build/helpers/parser-plugins.js.map +1 -0
  100. package/build/helpers/paths.d.ts +79 -0
  101. package/build/helpers/paths.d.ts.map +1 -0
  102. package/build/helpers/paths.js +168 -0
  103. package/build/helpers/paths.js.map +1 -0
  104. package/build/helpers/process-liveness.d.ts +24 -0
  105. package/build/helpers/process-liveness.d.ts.map +1 -0
  106. package/build/helpers/process-liveness.js +36 -0
  107. package/build/helpers/process-liveness.js.map +1 -0
  108. package/build/helpers/process-liveness.test.d.ts +2 -0
  109. package/build/helpers/process-liveness.test.d.ts.map +1 -0
  110. package/build/helpers/process-liveness.test.js +40 -0
  111. package/build/helpers/process-liveness.test.js.map +1 -0
  112. package/build/helpers/sequence-tree.d.ts +27 -0
  113. package/build/helpers/sequence-tree.d.ts.map +1 -0
  114. package/build/helpers/sequence-tree.js +80 -0
  115. package/build/helpers/sequence-tree.js.map +1 -0
  116. package/build/helpers/sequence-tree.test.d.ts +2 -0
  117. package/build/helpers/sequence-tree.test.d.ts.map +1 -0
  118. package/build/helpers/sequence-tree.test.js +122 -0
  119. package/build/helpers/sequence-tree.test.js.map +1 -0
  120. package/build/index.d.ts +3 -0
  121. package/build/index.d.ts.map +1 -0
  122. package/build/index.js +2007 -0
  123. package/build/index.js.map +1 -0
  124. package/build/interaction-recorder.d.ts +214 -0
  125. package/build/interaction-recorder.d.ts.map +1 -0
  126. package/build/interaction-recorder.js +2007 -0
  127. package/build/interaction-recorder.js.map +1 -0
  128. package/build/issue-tracker.d.ts +107 -0
  129. package/build/issue-tracker.d.ts.map +1 -0
  130. package/build/issue-tracker.js +582 -0
  131. package/build/issue-tracker.js.map +1 -0
  132. package/build/issue-tracker.test.d.ts +2 -0
  133. package/build/issue-tracker.test.d.ts.map +1 -0
  134. package/build/issue-tracker.test.js +88 -0
  135. package/build/issue-tracker.test.js.map +1 -0
  136. package/build/launch-chrome-port.test.d.ts +14 -0
  137. package/build/launch-chrome-port.test.d.ts.map +1 -0
  138. package/build/launch-chrome-port.test.js +97 -0
  139. package/build/launch-chrome-port.test.js.map +1 -0
  140. package/build/log-processor/orchestrator.d.ts +32 -0
  141. package/build/log-processor/orchestrator.d.ts.map +1 -0
  142. package/build/log-processor/orchestrator.js +27 -0
  143. package/build/log-processor/orchestrator.js.map +1 -0
  144. package/build/logpoint-execution-tracker.d.ts +57 -0
  145. package/build/logpoint-execution-tracker.d.ts.map +1 -0
  146. package/build/logpoint-execution-tracker.js +112 -0
  147. package/build/logpoint-execution-tracker.js.map +1 -0
  148. package/build/mcp-supervisor.d.ts +3 -0
  149. package/build/mcp-supervisor.d.ts.map +1 -0
  150. package/build/mcp-supervisor.js +211 -0
  151. package/build/mcp-supervisor.js.map +1 -0
  152. package/build/messages.d.ts +165 -0
  153. package/build/messages.d.ts.map +1 -0
  154. package/build/messages.js +574 -0
  155. package/build/messages.js.map +1 -0
  156. package/build/network-monitor.d.ts +172 -0
  157. package/build/network-monitor.d.ts.map +1 -0
  158. package/build/network-monitor.js +350 -0
  159. package/build/network-monitor.js.map +1 -0
  160. package/build/overlays.d.ts +49 -0
  161. package/build/overlays.d.ts.map +1 -0
  162. package/build/overlays.js +409 -0
  163. package/build/overlays.js.map +1 -0
  164. package/build/persistent-profiles.test.d.ts +15 -0
  165. package/build/persistent-profiles.test.d.ts.map +1 -0
  166. package/build/persistent-profiles.test.js +470 -0
  167. package/build/persistent-profiles.test.js.map +1 -0
  168. package/build/port-reserver.d.ts +32 -0
  169. package/build/port-reserver.d.ts.map +1 -0
  170. package/build/port-reserver.js +118 -0
  171. package/build/port-reserver.js.map +1 -0
  172. package/build/puppeteer-manager.d.ts +47 -0
  173. package/build/puppeteer-manager.d.ts.map +1 -0
  174. package/build/puppeteer-manager.js +116 -0
  175. package/build/puppeteer-manager.js.map +1 -0
  176. package/build/recording-coverage.test.d.ts +12 -0
  177. package/build/recording-coverage.test.d.ts.map +1 -0
  178. package/build/recording-coverage.test.js +72 -0
  179. package/build/recording-coverage.test.js.map +1 -0
  180. package/build/reference-validator.d.ts +47 -0
  181. package/build/reference-validator.d.ts.map +1 -0
  182. package/build/reference-validator.js +86 -0
  183. package/build/reference-validator.js.map +1 -0
  184. package/build/reference-validator.test.d.ts +5 -0
  185. package/build/reference-validator.test.d.ts.map +1 -0
  186. package/build/reference-validator.test.js +243 -0
  187. package/build/reference-validator.test.js.map +1 -0
  188. package/build/replay-cursor.d.ts +27 -0
  189. package/build/replay-cursor.d.ts.map +1 -0
  190. package/build/replay-cursor.js +240 -0
  191. package/build/replay-cursor.js.map +1 -0
  192. package/build/runners/docker-compose-runner.d.ts +69 -0
  193. package/build/runners/docker-compose-runner.d.ts.map +1 -0
  194. package/build/runners/docker-compose-runner.js +438 -0
  195. package/build/runners/docker-compose-runner.js.map +1 -0
  196. package/build/runners/docker-runner.d.ts +65 -0
  197. package/build/runners/docker-runner.d.ts.map +1 -0
  198. package/build/runners/docker-runner.js +414 -0
  199. package/build/runners/docker-runner.js.map +1 -0
  200. package/build/runners/index.d.ts +19 -0
  201. package/build/runners/index.d.ts.map +1 -0
  202. package/build/runners/index.js +35 -0
  203. package/build/runners/index.js.map +1 -0
  204. package/build/runners/native-runner.d.ts +95 -0
  205. package/build/runners/native-runner.d.ts.map +1 -0
  206. package/build/runners/native-runner.js +370 -0
  207. package/build/runners/native-runner.js.map +1 -0
  208. package/build/runners/types.d.ts +147 -0
  209. package/build/runners/types.d.ts.map +1 -0
  210. package/build/runners/types.js +25 -0
  211. package/build/runners/types.js.map +1 -0
  212. package/build/self-restart.d.ts +12 -0
  213. package/build/self-restart.d.ts.map +1 -0
  214. package/build/self-restart.js +41 -0
  215. package/build/self-restart.js.map +1 -0
  216. package/build/self-restart.test.d.ts +2 -0
  217. package/build/self-restart.test.d.ts.map +1 -0
  218. package/build/self-restart.test.js +34 -0
  219. package/build/self-restart.test.js.map +1 -0
  220. package/build/server-claims.d.ts +161 -0
  221. package/build/server-claims.d.ts.map +1 -0
  222. package/build/server-claims.js +382 -0
  223. package/build/server-claims.js.map +1 -0
  224. package/build/server-claims.test.d.ts +2 -0
  225. package/build/server-claims.test.d.ts.map +1 -0
  226. package/build/server-claims.test.js +197 -0
  227. package/build/server-claims.test.js.map +1 -0
  228. package/build/server-manager.d.ts +438 -0
  229. package/build/server-manager.d.ts.map +1 -0
  230. package/build/server-manager.js +1674 -0
  231. package/build/server-manager.js.map +1 -0
  232. package/build/server-manager.test.d.ts +17 -0
  233. package/build/server-manager.test.d.ts.map +1 -0
  234. package/build/server-manager.test.js +122 -0
  235. package/build/server-manager.test.js.map +1 -0
  236. package/build/server-ownership.test.d.ts +2 -0
  237. package/build/server-ownership.test.d.ts.map +1 -0
  238. package/build/server-ownership.test.js +238 -0
  239. package/build/server-ownership.test.js.map +1 -0
  240. package/build/server-watcher.d.ts +21 -0
  241. package/build/server-watcher.d.ts.map +1 -0
  242. package/build/server-watcher.js +105 -0
  243. package/build/server-watcher.js.map +1 -0
  244. package/build/session-detector.d.ts +47 -0
  245. package/build/session-detector.d.ts.map +1 -0
  246. package/build/session-detector.js +229 -0
  247. package/build/session-detector.js.map +1 -0
  248. package/build/session-detector.test.d.ts +9 -0
  249. package/build/session-detector.test.d.ts.map +1 -0
  250. package/build/session-detector.test.js +45 -0
  251. package/build/session-detector.test.js.map +1 -0
  252. package/build/sourcemap-handler.d.ts +114 -0
  253. package/build/sourcemap-handler.d.ts.map +1 -0
  254. package/build/sourcemap-handler.js +479 -0
  255. package/build/sourcemap-handler.js.map +1 -0
  256. package/build/supervisor/child-manager.d.ts +43 -0
  257. package/build/supervisor/child-manager.d.ts.map +1 -0
  258. package/build/supervisor/child-manager.js +148 -0
  259. package/build/supervisor/child-manager.js.map +1 -0
  260. package/build/supervisor/child-manager.test.d.ts +2 -0
  261. package/build/supervisor/child-manager.test.d.ts.map +1 -0
  262. package/build/supervisor/child-manager.test.js +107 -0
  263. package/build/supervisor/child-manager.test.js.map +1 -0
  264. package/build/supervisor/client-watcher.d.ts +48 -0
  265. package/build/supervisor/client-watcher.d.ts.map +1 -0
  266. package/build/supervisor/client-watcher.js +186 -0
  267. package/build/supervisor/client-watcher.js.map +1 -0
  268. package/build/supervisor/client-watcher.test.d.ts +2 -0
  269. package/build/supervisor/client-watcher.test.d.ts.map +1 -0
  270. package/build/supervisor/client-watcher.test.js +121 -0
  271. package/build/supervisor/client-watcher.test.js.map +1 -0
  272. package/build/supervisor/idle-config.d.ts +30 -0
  273. package/build/supervisor/idle-config.d.ts.map +1 -0
  274. package/build/supervisor/idle-config.js +76 -0
  275. package/build/supervisor/idle-config.js.map +1 -0
  276. package/build/supervisor/idle-config.test.d.ts +2 -0
  277. package/build/supervisor/idle-config.test.d.ts.map +1 -0
  278. package/build/supervisor/idle-config.test.js +119 -0
  279. package/build/supervisor/idle-config.test.js.map +1 -0
  280. package/build/supervisor/ndjson-reader.d.ts +47 -0
  281. package/build/supervisor/ndjson-reader.d.ts.map +1 -0
  282. package/build/supervisor/ndjson-reader.js +75 -0
  283. package/build/supervisor/ndjson-reader.js.map +1 -0
  284. package/build/supervisor/ndjson-reader.test.d.ts +2 -0
  285. package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
  286. package/build/supervisor/ndjson-reader.test.js +92 -0
  287. package/build/supervisor/ndjson-reader.test.js.map +1 -0
  288. package/build/supervisor/pidfile.d.ts +11 -0
  289. package/build/supervisor/pidfile.d.ts.map +1 -0
  290. package/build/supervisor/pidfile.js +29 -0
  291. package/build/supervisor/pidfile.js.map +1 -0
  292. package/build/supervisor/pidfile.test.d.ts +8 -0
  293. package/build/supervisor/pidfile.test.d.ts.map +1 -0
  294. package/build/supervisor/pidfile.test.js +46 -0
  295. package/build/supervisor/pidfile.test.js.map +1 -0
  296. package/build/supervisor/restart-coordinator.d.ts +73 -0
  297. package/build/supervisor/restart-coordinator.d.ts.map +1 -0
  298. package/build/supervisor/restart-coordinator.js +300 -0
  299. package/build/supervisor/restart-coordinator.js.map +1 -0
  300. package/build/supervisor/restart-coordinator.test.d.ts +2 -0
  301. package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
  302. package/build/supervisor/restart-coordinator.test.js +434 -0
  303. package/build/supervisor/restart-coordinator.test.js.map +1 -0
  304. package/build/test-support/fake-execute-tool-call.d.ts +11 -0
  305. package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
  306. package/build/test-support/fake-execute-tool-call.js +19 -0
  307. package/build/test-support/fake-execute-tool-call.js.map +1 -0
  308. package/build/tool-error.d.ts +14 -0
  309. package/build/tool-error.d.ts.map +1 -0
  310. package/build/tool-error.js +18 -0
  311. package/build/tool-error.js.map +1 -0
  312. package/build/tool-response.d.ts +293 -0
  313. package/build/tool-response.d.ts.map +1 -0
  314. package/build/tool-response.js +365 -0
  315. package/build/tool-response.js.map +1 -0
  316. package/build/tools/assert-tools.d.ts +62 -0
  317. package/build/tools/assert-tools.d.ts.map +1 -0
  318. package/build/tools/assert-tools.js +330 -0
  319. package/build/tools/assert-tools.js.map +1 -0
  320. package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
  321. package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
  322. package/build/tools/breakpoint-await-abort.test.js +124 -0
  323. package/build/tools/breakpoint-await-abort.test.js.map +1 -0
  324. package/build/tools/breakpoint-tools.d.ts +99 -0
  325. package/build/tools/breakpoint-tools.d.ts.map +1 -0
  326. package/build/tools/breakpoint-tools.js +1197 -0
  327. package/build/tools/breakpoint-tools.js.map +1 -0
  328. package/build/tools/config-tools.d.ts +58 -0
  329. package/build/tools/config-tools.d.ts.map +1 -0
  330. package/build/tools/config-tools.js +180 -0
  331. package/build/tools/config-tools.js.map +1 -0
  332. package/build/tools/connection-routing.test.d.ts +12 -0
  333. package/build/tools/connection-routing.test.d.ts.map +1 -0
  334. package/build/tools/connection-routing.test.js +206 -0
  335. package/build/tools/connection-routing.test.js.map +1 -0
  336. package/build/tools/console-tools.d.ts +84 -0
  337. package/build/tools/console-tools.d.ts.map +1 -0
  338. package/build/tools/console-tools.js +241 -0
  339. package/build/tools/console-tools.js.map +1 -0
  340. package/build/tools/content-tools.d.ts +74 -0
  341. package/build/tools/content-tools.d.ts.map +1 -0
  342. package/build/tools/content-tools.js +582 -0
  343. package/build/tools/content-tools.js.map +1 -0
  344. package/build/tools/dashboard-tools.d.ts +38 -0
  345. package/build/tools/dashboard-tools.d.ts.map +1 -0
  346. package/build/tools/dashboard-tools.js +123 -0
  347. package/build/tools/dashboard-tools.js.map +1 -0
  348. package/build/tools/dom-tools.d.ts +41 -0
  349. package/build/tools/dom-tools.d.ts.map +1 -0
  350. package/build/tools/dom-tools.js +277 -0
  351. package/build/tools/dom-tools.js.map +1 -0
  352. package/build/tools/download-tools.d.ts +55 -0
  353. package/build/tools/download-tools.d.ts.map +1 -0
  354. package/build/tools/download-tools.js +280 -0
  355. package/build/tools/download-tools.js.map +1 -0
  356. package/build/tools/execution-tools.d.ts +40 -0
  357. package/build/tools/execution-tools.d.ts.map +1 -0
  358. package/build/tools/execution-tools.js +121 -0
  359. package/build/tools/execution-tools.js.map +1 -0
  360. package/build/tools/input-abort.test.d.ts +2 -0
  361. package/build/tools/input-abort.test.d.ts.map +1 -0
  362. package/build/tools/input-abort.test.js +227 -0
  363. package/build/tools/input-abort.test.js.map +1 -0
  364. package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
  365. package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
  366. package/build/tools/input-ambiguous-selector.test.js +34 -0
  367. package/build/tools/input-ambiguous-selector.test.js.map +1 -0
  368. package/build/tools/input-tools.d.ts +168 -0
  369. package/build/tools/input-tools.d.ts.map +1 -0
  370. package/build/tools/input-tools.js +1397 -0
  371. package/build/tools/input-tools.js.map +1 -0
  372. package/build/tools/inspection-tools.d.ts +112 -0
  373. package/build/tools/inspection-tools.d.ts.map +1 -0
  374. package/build/tools/inspection-tools.js +637 -0
  375. package/build/tools/inspection-tools.js.map +1 -0
  376. package/build/tools/interpolation.d.ts +30 -0
  377. package/build/tools/interpolation.d.ts.map +1 -0
  378. package/build/tools/interpolation.js +143 -0
  379. package/build/tools/interpolation.js.map +1 -0
  380. package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
  381. package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
  382. package/build/tools/issues-abort-hygiene.test.js +79 -0
  383. package/build/tools/issues-abort-hygiene.test.js.map +1 -0
  384. package/build/tools/issues-tools.d.ts +98 -0
  385. package/build/tools/issues-tools.d.ts.map +1 -0
  386. package/build/tools/issues-tools.js +821 -0
  387. package/build/tools/issues-tools.js.map +1 -0
  388. package/build/tools/issues-tools.test.d.ts +8 -0
  389. package/build/tools/issues-tools.test.d.ts.map +1 -0
  390. package/build/tools/issues-tools.test.js +184 -0
  391. package/build/tools/issues-tools.test.js.map +1 -0
  392. package/build/tools/modal-tools.d.ts +73 -0
  393. package/build/tools/modal-tools.d.ts.map +1 -0
  394. package/build/tools/modal-tools.js +161 -0
  395. package/build/tools/modal-tools.js.map +1 -0
  396. package/build/tools/network-tools.d.ts +72 -0
  397. package/build/tools/network-tools.d.ts.map +1 -0
  398. package/build/tools/network-tools.js +432 -0
  399. package/build/tools/network-tools.js.map +1 -0
  400. package/build/tools/page-tools.d.ts +78 -0
  401. package/build/tools/page-tools.d.ts.map +1 -0
  402. package/build/tools/page-tools.js +255 -0
  403. package/build/tools/page-tools.js.map +1 -0
  404. package/build/tools/plugin-tools.d.ts +16 -0
  405. package/build/tools/plugin-tools.d.ts.map +1 -0
  406. package/build/tools/plugin-tools.js +17 -0
  407. package/build/tools/plugin-tools.js.map +1 -0
  408. package/build/tools/replay-abort.test.d.ts +2 -0
  409. package/build/tools/replay-abort.test.d.ts.map +1 -0
  410. package/build/tools/replay-abort.test.js +259 -0
  411. package/build/tools/replay-abort.test.js.map +1 -0
  412. package/build/tools/replay-add-conditional.test.d.ts +2 -0
  413. package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
  414. package/build/tools/replay-add-conditional.test.js +205 -0
  415. package/build/tools/replay-add-conditional.test.js.map +1 -0
  416. package/build/tools/replay-background-run.test.d.ts +2 -0
  417. package/build/tools/replay-background-run.test.d.ts.map +1 -0
  418. package/build/tools/replay-background-run.test.js +176 -0
  419. package/build/tools/replay-background-run.test.js.map +1 -0
  420. package/build/tools/replay-capture-variables.test.d.ts +2 -0
  421. package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
  422. package/build/tools/replay-capture-variables.test.js +331 -0
  423. package/build/tools/replay-capture-variables.test.js.map +1 -0
  424. package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
  425. package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
  426. package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
  427. package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
  428. package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
  429. package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
  430. package/build/tools/replay-conditional-timeout.test.js +126 -0
  431. package/build/tools/replay-conditional-timeout.test.js.map +1 -0
  432. package/build/tools/replay-declare.test.d.ts +2 -0
  433. package/build/tools/replay-declare.test.d.ts.map +1 -0
  434. package/build/tools/replay-declare.test.js +106 -0
  435. package/build/tools/replay-declare.test.js.map +1 -0
  436. package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
  437. package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
  438. package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
  439. package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
  440. package/build/tools/replay-declared-profile.test.d.ts +2 -0
  441. package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
  442. package/build/tools/replay-declared-profile.test.js +116 -0
  443. package/build/tools/replay-declared-profile.test.js.map +1 -0
  444. package/build/tools/replay-executor.d.ts +556 -0
  445. package/build/tools/replay-executor.d.ts.map +1 -0
  446. package/build/tools/replay-executor.js +2347 -0
  447. package/build/tools/replay-executor.js.map +1 -0
  448. package/build/tools/replay-folder-save.test.d.ts +2 -0
  449. package/build/tools/replay-folder-save.test.d.ts.map +1 -0
  450. package/build/tools/replay-folder-save.test.js +63 -0
  451. package/build/tools/replay-folder-save.test.js.map +1 -0
  452. package/build/tools/replay-formatters.d.ts +148 -0
  453. package/build/tools/replay-formatters.d.ts.map +1 -0
  454. package/build/tools/replay-formatters.js +856 -0
  455. package/build/tools/replay-formatters.js.map +1 -0
  456. package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
  457. package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
  458. package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
  459. package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
  460. package/build/tools/replay-multi-connection.test.d.ts +2 -0
  461. package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
  462. package/build/tools/replay-multi-connection.test.js +547 -0
  463. package/build/tools/replay-multi-connection.test.js.map +1 -0
  464. package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
  465. package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
  466. package/build/tools/replay-output-and-record-options.test.js +240 -0
  467. package/build/tools/replay-output-and-record-options.test.js.map +1 -0
  468. package/build/tools/replay-production-error-shape.test.d.ts +7 -0
  469. package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
  470. package/build/tools/replay-production-error-shape.test.js +93 -0
  471. package/build/tools/replay-production-error-shape.test.js.map +1 -0
  472. package/build/tools/replay-rebase.test.d.ts +2 -0
  473. package/build/tools/replay-rebase.test.d.ts.map +1 -0
  474. package/build/tools/replay-rebase.test.js +58 -0
  475. package/build/tools/replay-rebase.test.js.map +1 -0
  476. package/build/tools/replay-retry-classification.test.d.ts +9 -0
  477. package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
  478. package/build/tools/replay-retry-classification.test.js +57 -0
  479. package/build/tools/replay-retry-classification.test.js.map +1 -0
  480. package/build/tools/replay-run-registry.d.ts +55 -0
  481. package/build/tools/replay-run-registry.d.ts.map +1 -0
  482. package/build/tools/replay-run-registry.js +65 -0
  483. package/build/tools/replay-run-registry.js.map +1 -0
  484. package/build/tools/replay-sequence-reload.test.d.ts +2 -0
  485. package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
  486. package/build/tools/replay-sequence-reload.test.js +82 -0
  487. package/build/tools/replay-sequence-reload.test.js.map +1 -0
  488. package/build/tools/replay-socket-scope.test.d.ts +2 -0
  489. package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
  490. package/build/tools/replay-socket-scope.test.js +75 -0
  491. package/build/tools/replay-socket-scope.test.js.map +1 -0
  492. package/build/tools/replay-step-connection.test.d.ts +2 -0
  493. package/build/tools/replay-step-connection.test.d.ts.map +1 -0
  494. package/build/tools/replay-step-connection.test.js +292 -0
  495. package/build/tools/replay-step-connection.test.js.map +1 -0
  496. package/build/tools/replay-step-timeout.test.d.ts +2 -0
  497. package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
  498. package/build/tools/replay-step-timeout.test.js +204 -0
  499. package/build/tools/replay-step-timeout.test.js.map +1 -0
  500. package/build/tools/replay-tags.test.d.ts +2 -0
  501. package/build/tools/replay-tags.test.d.ts.map +1 -0
  502. package/build/tools/replay-tags.test.js +99 -0
  503. package/build/tools/replay-tags.test.js.map +1 -0
  504. package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
  505. package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
  506. package/build/tools/replay-teardown-and-foreach.test.js +318 -0
  507. package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
  508. package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
  509. package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
  510. package/build/tools/replay-tool-name-validation.test.js +280 -0
  511. package/build/tools/replay-tool-name-validation.test.js.map +1 -0
  512. package/build/tools/replay-tools.d.ts +288 -0
  513. package/build/tools/replay-tools.d.ts.map +1 -0
  514. package/build/tools/replay-tools.js +2994 -0
  515. package/build/tools/replay-tools.js.map +1 -0
  516. package/build/tools/request-abort.test.d.ts +2 -0
  517. package/build/tools/request-abort.test.d.ts.map +1 -0
  518. package/build/tools/request-abort.test.js +130 -0
  519. package/build/tools/request-abort.test.js.map +1 -0
  520. package/build/tools/request-tools.d.ts +56 -0
  521. package/build/tools/request-tools.d.ts.map +1 -0
  522. package/build/tools/request-tools.js +174 -0
  523. package/build/tools/request-tools.js.map +1 -0
  524. package/build/tools/screenshot-tools.d.ts +135 -0
  525. package/build/tools/screenshot-tools.d.ts.map +1 -0
  526. package/build/tools/screenshot-tools.js +636 -0
  527. package/build/tools/screenshot-tools.js.map +1 -0
  528. package/build/tools/server-tools.d.ts +88 -0
  529. package/build/tools/server-tools.d.ts.map +1 -0
  530. package/build/tools/server-tools.js +485 -0
  531. package/build/tools/server-tools.js.map +1 -0
  532. package/build/tools/source-tools.d.ts +46 -0
  533. package/build/tools/source-tools.d.ts.map +1 -0
  534. package/build/tools/source-tools.js +47 -0
  535. package/build/tools/source-tools.js.map +1 -0
  536. package/build/tools/storage-tools.d.ts +141 -0
  537. package/build/tools/storage-tools.d.ts.map +1 -0
  538. package/build/tools/storage-tools.js +894 -0
  539. package/build/tools/storage-tools.js.map +1 -0
  540. package/build/tools/storage-tools.test.d.ts +13 -0
  541. package/build/tools/storage-tools.test.d.ts.map +1 -0
  542. package/build/tools/storage-tools.test.js +707 -0
  543. package/build/tools/storage-tools.test.js.map +1 -0
  544. package/build/tools/tab-tools.d.ts +42 -0
  545. package/build/tools/tab-tools.d.ts.map +1 -0
  546. package/build/tools/tab-tools.js +296 -0
  547. package/build/tools/tab-tools.js.map +1 -0
  548. package/build/tools/wait-tools.d.ts +85 -0
  549. package/build/tools/wait-tools.d.ts.map +1 -0
  550. package/build/tools/wait-tools.js +236 -0
  551. package/build/tools/wait-tools.js.map +1 -0
  552. package/build/tools/wait-tools.test.d.ts +9 -0
  553. package/build/tools/wait-tools.test.d.ts.map +1 -0
  554. package/build/tools/wait-tools.test.js +193 -0
  555. package/build/tools/wait-tools.test.js.map +1 -0
  556. package/build/types.d.ts +133 -0
  557. package/build/types.d.ts.map +1 -0
  558. package/build/types.js +5 -0
  559. package/build/types.js.map +1 -0
  560. package/build/ui-verifier.d.ts +88 -0
  561. package/build/ui-verifier.d.ts.map +1 -0
  562. package/build/ui-verifier.js +540 -0
  563. package/build/ui-verifier.js.map +1 -0
  564. package/build/utils/abort.d.ts +68 -0
  565. package/build/utils/abort.d.ts.map +1 -0
  566. package/build/utils/abort.js +159 -0
  567. package/build/utils/abort.js.map +1 -0
  568. package/build/utils/abort.test.d.ts +2 -0
  569. package/build/utils/abort.test.d.ts.map +1 -0
  570. package/build/utils/abort.test.js +174 -0
  571. package/build/utils/abort.test.js.map +1 -0
  572. package/build/utils/modal-detection-core.d.ts +150 -0
  573. package/build/utils/modal-detection-core.d.ts.map +1 -0
  574. package/build/utils/modal-detection-core.js +332 -0
  575. package/build/utils/modal-detection-core.js.map +1 -0
  576. package/build/utils/modal-detection-core.test.d.ts +5 -0
  577. package/build/utils/modal-detection-core.test.d.ts.map +1 -0
  578. package/build/utils/modal-detection-core.test.js +492 -0
  579. package/build/utils/modal-detection-core.test.js.map +1 -0
  580. package/build/utils/modal-detector.d.ts +29 -0
  581. package/build/utils/modal-detector.d.ts.map +1 -0
  582. package/build/utils/modal-detector.js +281 -0
  583. package/build/utils/modal-detector.js.map +1 -0
  584. package/build/utils/modal-dismissal.d.ts +32 -0
  585. package/build/utils/modal-dismissal.d.ts.map +1 -0
  586. package/build/utils/modal-dismissal.js +190 -0
  587. package/build/utils/modal-dismissal.js.map +1 -0
  588. package/build/utils/port-check.d.ts +31 -0
  589. package/build/utils/port-check.d.ts.map +1 -0
  590. package/build/utils/port-check.js +87 -0
  591. package/build/utils/port-check.js.map +1 -0
  592. package/build/utils/port-check.test.d.ts +2 -0
  593. package/build/utils/port-check.test.d.ts.map +1 -0
  594. package/build/utils/port-check.test.js +66 -0
  595. package/build/utils/port-check.test.js.map +1 -0
  596. package/build/utils/selector-resolver.d.ts +66 -0
  597. package/build/utils/selector-resolver.d.ts.map +1 -0
  598. package/build/utils/selector-resolver.js +200 -0
  599. package/build/utils/selector-resolver.js.map +1 -0
  600. package/build/validation-helpers.d.ts +41 -0
  601. package/build/validation-helpers.d.ts.map +1 -0
  602. package/build/validation-helpers.js +236 -0
  603. package/build/validation-helpers.js.map +1 -0
  604. package/build/window-sizing.d.ts +26 -0
  605. package/build/window-sizing.d.ts.map +1 -0
  606. package/build/window-sizing.js +87 -0
  607. package/build/window-sizing.js.map +1 -0
  608. package/build/window-sizing.test.d.ts +8 -0
  609. package/build/window-sizing.test.d.ts.map +1 -0
  610. package/build/window-sizing.test.js +113 -0
  611. package/build/window-sizing.test.js.map +1 -0
  612. package/docs/README.md +74 -0
  613. package/docs/automation.md +562 -0
  614. package/docs/debugging.md +229 -0
  615. package/docs/installation.md +133 -0
  616. package/docs/instructions.md +227 -0
  617. package/docs/mcp-instructions.md +15 -0
  618. package/docs/message-system.md +327 -0
  619. package/docs/messages.md +3480 -0
  620. package/docs/parser-plugins.md +132 -0
  621. package/docs/replay.md +1411 -0
  622. package/docs/troubleshooting.md +272 -0
  623. package/docs/working_with_llm_notes.md +5 -0
  624. package/package.json +87 -0
  625. package/skills/devharness/SKILL.md +195 -0
  626. package/skills/devharness/references/sequences.md +613 -0
  627. 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