@detiq/crawler 0.1.3

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 (412) hide show
  1. package/Dockerfile +49 -0
  2. package/README.md +11 -0
  3. package/dist/accessibility-auditor.d.ts +29 -0
  4. package/dist/accessibility-auditor.js +57 -0
  5. package/dist/accessibility-auditor.js.map +7 -0
  6. package/dist/ai-login.d.ts +32 -0
  7. package/dist/ai-login.js +177 -0
  8. package/dist/ai-login.js.map +7 -0
  9. package/dist/auth-token-rotator.d.ts +47 -0
  10. package/dist/auth-token-rotator.js +58 -0
  11. package/dist/auth-token-rotator.js.map +7 -0
  12. package/dist/auth.d.ts +20 -0
  13. package/dist/auth.js +329 -0
  14. package/dist/auth.js.map +7 -0
  15. package/dist/browser-extension.d.ts +37 -0
  16. package/dist/browser-extension.js +113 -0
  17. package/dist/browser-extension.js.map +7 -0
  18. package/dist/captcha-solver.d.ts +16 -0
  19. package/dist/captcha-solver.js +105 -0
  20. package/dist/captcha-solver.js.map +7 -0
  21. package/dist/color-contrast.d.ts +30 -0
  22. package/dist/color-contrast.js +78 -0
  23. package/dist/color-contrast.js.map +7 -0
  24. package/dist/content-chunker.d.ts +18 -0
  25. package/dist/content-chunker.js +152 -0
  26. package/dist/content-chunker.js.map +7 -0
  27. package/dist/content-dedup.d.ts +23 -0
  28. package/dist/content-dedup.js +60 -0
  29. package/dist/content-dedup.js.map +7 -0
  30. package/dist/cookie-consent.d.ts +25 -0
  31. package/dist/cookie-consent.js +78 -0
  32. package/dist/cookie-consent.js.map +7 -0
  33. package/dist/cookie-jar.d.ts +20 -0
  34. package/dist/cookie-jar.js +61 -0
  35. package/dist/cookie-jar.js.map +7 -0
  36. package/dist/crawler.d.ts +179 -0
  37. package/dist/crawler.js +3672 -0
  38. package/dist/crawler.js.map +7 -0
  39. package/dist/custom-js-injector.d.ts +42 -0
  40. package/dist/custom-js-injector.js +50 -0
  41. package/dist/custom-js-injector.js.map +7 -0
  42. package/dist/dark-mode-capture.d.ts +23 -0
  43. package/dist/dark-mode-capture.js +75 -0
  44. package/dist/dark-mode-capture.js.map +7 -0
  45. package/dist/dead-link-detector.d.ts +37 -0
  46. package/dist/dead-link-detector.js +50 -0
  47. package/dist/dead-link-detector.js.map +7 -0
  48. package/dist/element-hash.d.ts +9 -0
  49. package/dist/element-hash.js +9 -0
  50. package/dist/element-hash.js.map +7 -0
  51. package/dist/email-otp.d.ts +42 -0
  52. package/dist/email-otp.js +177 -0
  53. package/dist/email-otp.js.map +7 -0
  54. package/dist/engine-router.d.ts +47 -0
  55. package/dist/engine-router.js +94 -0
  56. package/dist/engine-router.js.map +7 -0
  57. package/dist/export/bulk-export.d.ts +26 -0
  58. package/dist/export/bulk-export.js +76 -0
  59. package/dist/export/bulk-export.js.map +7 -0
  60. package/dist/export/vector-export.d.ts +76 -0
  61. package/dist/export/vector-export.js +85 -0
  62. package/dist/export/vector-export.js.map +7 -0
  63. package/dist/extractors/advanced.d.ts +54 -0
  64. package/dist/extractors/advanced.js +245 -0
  65. package/dist/extractors/advanced.js.map +7 -0
  66. package/dist/extractors/artifact-envelope.d.ts +63 -0
  67. package/dist/extractors/artifact-envelope.js +63 -0
  68. package/dist/extractors/artifact-envelope.js.map +7 -0
  69. package/dist/failure-analysis-agent.d.ts +16 -0
  70. package/dist/failure-analysis-agent.js +40 -0
  71. package/dist/failure-analysis-agent.js.map +7 -0
  72. package/dist/feature-flag-detector.d.ts +30 -0
  73. package/dist/feature-flag-detector.js +55 -0
  74. package/dist/feature-flag-detector.js.map +7 -0
  75. package/dist/form-detector.d.ts +34 -0
  76. package/dist/form-detector.js +58 -0
  77. package/dist/form-detector.js.map +7 -0
  78. package/dist/frontier.d.ts +32 -0
  79. package/dist/frontier.js +83 -0
  80. package/dist/frontier.js.map +7 -0
  81. package/dist/gap-pass.d.ts +35 -0
  82. package/dist/gap-pass.js +81 -0
  83. package/dist/gap-pass.js.map +7 -0
  84. package/dist/graphql-detector.d.ts +39 -0
  85. package/dist/graphql-detector.js +86 -0
  86. package/dist/graphql-detector.js.map +7 -0
  87. package/dist/http-engine.d.ts +80 -0
  88. package/dist/http-engine.js +304 -0
  89. package/dist/http-engine.js.map +7 -0
  90. package/dist/incremental-diff.d.ts +59 -0
  91. package/dist/incremental-diff.js +61 -0
  92. package/dist/incremental-diff.js.map +7 -0
  93. package/dist/index.d.ts +35 -0
  94. package/dist/index.js +61 -0
  95. package/dist/index.js.map +7 -0
  96. package/dist/input-fuzzer.d.ts +30 -0
  97. package/dist/input-fuzzer.js +105 -0
  98. package/dist/input-fuzzer.js.map +7 -0
  99. package/dist/interactive-probe.d.ts +104 -0
  100. package/dist/interactive-probe.js +442 -0
  101. package/dist/interactive-probe.js.map +7 -0
  102. package/dist/job-queue.d.ts +171 -0
  103. package/dist/job-queue.js +2256 -0
  104. package/dist/job-queue.js.map +7 -0
  105. package/dist/js-coverage.d.ts +29 -0
  106. package/dist/js-coverage.js +56 -0
  107. package/dist/js-coverage.js.map +7 -0
  108. package/dist/js-router-detector.d.ts +17 -0
  109. package/dist/js-router-detector.js +90 -0
  110. package/dist/js-router-detector.js.map +7 -0
  111. package/dist/language-detector.d.ts +22 -0
  112. package/dist/language-detector.js +57 -0
  113. package/dist/language-detector.js.map +7 -0
  114. package/dist/link-classifier.d.ts +23 -0
  115. package/dist/link-classifier.js +40 -0
  116. package/dist/link-classifier.js.map +7 -0
  117. package/dist/live-view.d.ts +36 -0
  118. package/dist/live-view.js +100 -0
  119. package/dist/live-view.js.map +7 -0
  120. package/dist/logger.d.ts +2 -0
  121. package/dist/logger.js +10 -0
  122. package/dist/logger.js.map +7 -0
  123. package/dist/markdown-extract.d.ts +8 -0
  124. package/dist/markdown-extract.js +54 -0
  125. package/dist/markdown-extract.js.map +7 -0
  126. package/dist/native-crawler.d.ts +69 -0
  127. package/dist/native-crawler.js +839 -0
  128. package/dist/native-crawler.js.map +7 -0
  129. package/dist/network-observer.d.ts +49 -0
  130. package/dist/network-observer.js +149 -0
  131. package/dist/network-observer.js.map +7 -0
  132. package/dist/network-throttle.d.ts +25 -0
  133. package/dist/network-throttle.js +34 -0
  134. package/dist/network-throttle.js.map +7 -0
  135. package/dist/openapi-generator.d.ts +49 -0
  136. package/dist/openapi-generator.js +89 -0
  137. package/dist/openapi-generator.js.map +7 -0
  138. package/dist/pdf-extractor.d.ts +28 -0
  139. package/dist/pdf-extractor.js +81 -0
  140. package/dist/pdf-extractor.js.map +7 -0
  141. package/dist/performance-metrics.d.ts +53 -0
  142. package/dist/performance-metrics.js +83 -0
  143. package/dist/performance-metrics.js.map +7 -0
  144. package/dist/phash.d.ts +38 -0
  145. package/dist/phash.js +115 -0
  146. package/dist/phash.js.map +7 -0
  147. package/dist/pii-scrubber.d.ts +21 -0
  148. package/dist/pii-scrubber.js +75 -0
  149. package/dist/pii-scrubber.js.map +7 -0
  150. package/dist/playwright-recorder.d.ts +39 -0
  151. package/dist/playwright-recorder.js +123 -0
  152. package/dist/playwright-recorder.js.map +7 -0
  153. package/dist/policies/crawl-safety.d.ts +9 -0
  154. package/dist/policies/crawl-safety.js +37 -0
  155. package/dist/policies/crawl-safety.js.map +7 -0
  156. package/dist/profiles/domain-profile.d.ts +19 -0
  157. package/dist/profiles/domain-profile.js +140 -0
  158. package/dist/profiles/domain-profile.js.map +7 -0
  159. package/dist/proxy.d.ts +50 -0
  160. package/dist/proxy.js +165 -0
  161. package/dist/proxy.js.map +7 -0
  162. package/dist/rate-limiter.d.ts +31 -0
  163. package/dist/rate-limiter.js +64 -0
  164. package/dist/rate-limiter.js.map +7 -0
  165. package/dist/redirect-tracker.d.ts +37 -0
  166. package/dist/redirect-tracker.js +65 -0
  167. package/dist/redirect-tracker.js.map +7 -0
  168. package/dist/redis-frontier.d.ts +48 -0
  169. package/dist/redis-frontier.js +126 -0
  170. package/dist/redis-frontier.js.map +7 -0
  171. package/dist/redis-queue.d.ts +48 -0
  172. package/dist/redis-queue.js +165 -0
  173. package/dist/redis-queue.js.map +7 -0
  174. package/dist/response-cache.d.ts +38 -0
  175. package/dist/response-cache.js +140 -0
  176. package/dist/response-cache.js.map +7 -0
  177. package/dist/responsive-detector.d.ts +29 -0
  178. package/dist/responsive-detector.js +46 -0
  179. package/dist/responsive-detector.js.map +7 -0
  180. package/dist/retry-policy.d.ts +20 -0
  181. package/dist/retry-policy.js +54 -0
  182. package/dist/retry-policy.js.map +7 -0
  183. package/dist/robots-txt.d.ts +9 -0
  184. package/dist/robots-txt.js +77 -0
  185. package/dist/robots-txt.js.map +7 -0
  186. package/dist/screen-reader.d.ts +16 -0
  187. package/dist/screen-reader.js +46 -0
  188. package/dist/screen-reader.js.map +7 -0
  189. package/dist/screenshot-masker.d.ts +28 -0
  190. package/dist/screenshot-masker.js +62 -0
  191. package/dist/screenshot-masker.js.map +7 -0
  192. package/dist/seo-checker.d.ts +33 -0
  193. package/dist/seo-checker.js +91 -0
  194. package/dist/seo-checker.js.map +7 -0
  195. package/dist/session-capture.d.ts +35 -0
  196. package/dist/session-capture.js +267 -0
  197. package/dist/session-capture.js.map +7 -0
  198. package/dist/session-export.d.ts +49 -0
  199. package/dist/session-export.js +117 -0
  200. package/dist/session-export.js.map +7 -0
  201. package/dist/sitemap-writer.d.ts +21 -0
  202. package/dist/sitemap-writer.js +47 -0
  203. package/dist/sitemap-writer.js.map +7 -0
  204. package/dist/sitemap.d.ts +51 -0
  205. package/dist/sitemap.js +152 -0
  206. package/dist/sitemap.js.map +7 -0
  207. package/dist/skyvern.d.ts +56 -0
  208. package/dist/skyvern.js +129 -0
  209. package/dist/skyvern.js.map +7 -0
  210. package/dist/sqs-queue.d.ts +2 -0
  211. package/dist/sqs-queue.js +47 -0
  212. package/dist/sqs-queue.js.map +7 -0
  213. package/dist/sso-token.d.ts +29 -0
  214. package/dist/sso-token.js +76 -0
  215. package/dist/sso-token.js.map +7 -0
  216. package/dist/stealth/human-behavior.d.ts +20 -0
  217. package/dist/stealth/human-behavior.js +168 -0
  218. package/dist/stealth/human-behavior.js.map +7 -0
  219. package/dist/telemetry.d.ts +34 -0
  220. package/dist/telemetry.js +77 -0
  221. package/dist/telemetry.js.map +7 -0
  222. package/dist/test-executor.d.ts +67 -0
  223. package/dist/test-executor.js +311 -0
  224. package/dist/test-executor.js.map +7 -0
  225. package/dist/tls-fingerprint.d.ts +50 -0
  226. package/dist/tls-fingerprint.js +81 -0
  227. package/dist/tls-fingerprint.js.map +7 -0
  228. package/dist/url-pattern-completer.d.ts +28 -0
  229. package/dist/url-pattern-completer.js +87 -0
  230. package/dist/url-pattern-completer.js.map +7 -0
  231. package/dist/visual-diff-verdict.d.ts +40 -0
  232. package/dist/visual-diff-verdict.js +25 -0
  233. package/dist/visual-diff-verdict.js.map +7 -0
  234. package/dist/webhook-notifier.d.ts +30 -0
  235. package/dist/webhook-notifier.js +76 -0
  236. package/dist/webhook-notifier.js.map +7 -0
  237. package/dist/websocket-detector.d.ts +24 -0
  238. package/dist/websocket-detector.js +60 -0
  239. package/dist/websocket-detector.js.map +7 -0
  240. package/dist/worker.d.ts +1 -0
  241. package/dist/worker.js +33 -0
  242. package/dist/worker.js.map +7 -0
  243. package/package.json +45 -0
  244. package/src/accessibility-auditor.ts +102 -0
  245. package/src/ai-login.ts +218 -0
  246. package/src/auth-token-rotator.ts +89 -0
  247. package/src/auth.ts +396 -0
  248. package/src/browser-extension.ts +186 -0
  249. package/src/captcha-solver.ts +143 -0
  250. package/src/color-contrast.ts +116 -0
  251. package/src/content-chunker.ts +170 -0
  252. package/src/content-dedup.ts +99 -0
  253. package/src/cookie-consent.ts +106 -0
  254. package/src/cookie-jar.ts +91 -0
  255. package/src/crawler.ts +4305 -0
  256. package/src/custom-js-injector.ts +93 -0
  257. package/src/dark-mode-capture.ts +110 -0
  258. package/src/dead-link-detector.ts +76 -0
  259. package/src/element-hash.ts +16 -0
  260. package/src/email-otp.ts +252 -0
  261. package/src/engine-router.ts +141 -0
  262. package/src/export/bulk-export.ts +85 -0
  263. package/src/export/vector-export.ts +160 -0
  264. package/src/extractors/advanced.ts +283 -0
  265. package/src/extractors/artifact-envelope.ts +111 -0
  266. package/src/failure-analysis-agent.ts +49 -0
  267. package/src/feature-flag-detector.ts +97 -0
  268. package/src/form-detector.ts +103 -0
  269. package/src/frontier.test.ts +64 -0
  270. package/src/frontier.ts +112 -0
  271. package/src/gap-pass.ts +122 -0
  272. package/src/graphql-detector.ts +129 -0
  273. package/src/http-engine.ts +433 -0
  274. package/src/incremental-diff.ts +121 -0
  275. package/src/index.ts +35 -0
  276. package/src/input-fuzzer.ts +129 -0
  277. package/src/interactive-probe.ts +623 -0
  278. package/src/job-queue.test.ts +120 -0
  279. package/src/job-queue.ts +2701 -0
  280. package/src/js-coverage.ts +87 -0
  281. package/src/js-router-detector.ts +112 -0
  282. package/src/language-detector.ts +82 -0
  283. package/src/link-classifier.ts +65 -0
  284. package/src/live-view.ts +141 -0
  285. package/src/logger.ts +9 -0
  286. package/src/markdown-extract.test.ts +73 -0
  287. package/src/markdown-extract.ts +76 -0
  288. package/src/native-crawler.ts +1106 -0
  289. package/src/network-observer.ts +204 -0
  290. package/src/network-throttle.ts +69 -0
  291. package/src/openapi-generator.ts +135 -0
  292. package/src/pdf-extractor.ts +123 -0
  293. package/src/performance-metrics.ts +134 -0
  294. package/src/phash.ts +151 -0
  295. package/src/pii-scrubber.ts +99 -0
  296. package/src/playwright-recorder.ts +170 -0
  297. package/src/policies/crawl-safety.ts +46 -0
  298. package/src/profiles/domain-profile.ts +167 -0
  299. package/src/proxy.ts +238 -0
  300. package/src/rate-limiter.ts +87 -0
  301. package/src/redirect-tracker.ts +97 -0
  302. package/src/redis-frontier.ts +143 -0
  303. package/src/redis-queue.ts +208 -0
  304. package/src/response-cache.ts +150 -0
  305. package/src/responsive-detector.ts +92 -0
  306. package/src/retry-policy.ts +87 -0
  307. package/src/robots-txt.ts +96 -0
  308. package/src/screen-reader.ts +89 -0
  309. package/src/screenshot-masker.ts +98 -0
  310. package/src/seo-checker.ts +134 -0
  311. package/src/session-capture.ts +325 -0
  312. package/src/session-export.test.ts +75 -0
  313. package/src/session-export.ts +172 -0
  314. package/src/sitemap-writer.ts +65 -0
  315. package/src/sitemap.test.ts +131 -0
  316. package/src/sitemap.ts +224 -0
  317. package/src/skyvern.ts +199 -0
  318. package/src/sqs-queue.ts +49 -0
  319. package/src/sso-token.ts +120 -0
  320. package/src/stealth/human-behavior.ts +191 -0
  321. package/src/telemetry.ts +122 -0
  322. package/src/test-executor.test.ts +72 -0
  323. package/src/test-executor.ts +424 -0
  324. package/src/tls-fingerprint.ts +111 -0
  325. package/src/url-pattern-completer.ts +114 -0
  326. package/src/visual-diff-verdict.test.ts +80 -0
  327. package/src/visual-diff-verdict.ts +57 -0
  328. package/src/webhook-notifier.ts +109 -0
  329. package/src/websocket-detector.ts +83 -0
  330. package/src/worker.ts +48 -0
  331. package/tsconfig.json +13 -0
  332. package/types/accessibility-auditor.d.ts +29 -0
  333. package/types/ai-login.d.ts +32 -0
  334. package/types/auth-token-rotator.d.ts +47 -0
  335. package/types/auth.d.ts +20 -0
  336. package/types/browser-extension.d.ts +37 -0
  337. package/types/bull-worker.d.ts +6 -0
  338. package/types/captcha-solver.d.ts +16 -0
  339. package/types/color-contrast.d.ts +30 -0
  340. package/types/content-chunker.d.ts +18 -0
  341. package/types/content-dedup.d.ts +23 -0
  342. package/types/cookie-consent.d.ts +25 -0
  343. package/types/cookie-jar.d.ts +20 -0
  344. package/types/crawler.d.ts +179 -0
  345. package/types/custom-js-injector.d.ts +42 -0
  346. package/types/dark-mode-capture.d.ts +23 -0
  347. package/types/dead-link-detector.d.ts +37 -0
  348. package/types/element-hash.d.ts +9 -0
  349. package/types/email-otp.d.ts +42 -0
  350. package/types/engine-router.d.ts +47 -0
  351. package/types/export/bulk-export.d.ts +26 -0
  352. package/types/export/vector-export.d.ts +76 -0
  353. package/types/extractors/advanced.d.ts +54 -0
  354. package/types/extractors/artifact-envelope.d.ts +63 -0
  355. package/types/failure-analysis-agent.d.ts +16 -0
  356. package/types/feature-flag-detector.d.ts +30 -0
  357. package/types/form-detector.d.ts +34 -0
  358. package/types/frontier.d.ts +32 -0
  359. package/types/gap-pass.d.ts +35 -0
  360. package/types/graphql-detector.d.ts +39 -0
  361. package/types/http-engine.d.ts +80 -0
  362. package/types/incremental-diff.d.ts +59 -0
  363. package/types/index.d.ts +35 -0
  364. package/types/input-fuzzer.d.ts +30 -0
  365. package/types/interactive-probe.d.ts +104 -0
  366. package/types/job-queue.d.ts +171 -0
  367. package/types/js-coverage.d.ts +29 -0
  368. package/types/js-router-detector.d.ts +17 -0
  369. package/types/language-detector.d.ts +22 -0
  370. package/types/link-classifier.d.ts +23 -0
  371. package/types/live-view.d.ts +36 -0
  372. package/types/logger.d.ts +2 -0
  373. package/types/markdown-extract.d.ts +8 -0
  374. package/types/native-crawler.d.ts +69 -0
  375. package/types/network-observer.d.ts +49 -0
  376. package/types/network-throttle.d.ts +25 -0
  377. package/types/openapi-generator.d.ts +49 -0
  378. package/types/pdf-extractor.d.ts +28 -0
  379. package/types/performance-metrics.d.ts +53 -0
  380. package/types/phash.d.ts +38 -0
  381. package/types/pii-scrubber.d.ts +21 -0
  382. package/types/playwright-recorder.d.ts +39 -0
  383. package/types/policies/crawl-safety.d.ts +9 -0
  384. package/types/profiles/domain-profile.d.ts +19 -0
  385. package/types/proxy.d.ts +50 -0
  386. package/types/rate-limiter.d.ts +31 -0
  387. package/types/redirect-tracker.d.ts +37 -0
  388. package/types/redis-frontier.d.ts +48 -0
  389. package/types/redis-queue.d.ts +48 -0
  390. package/types/response-cache.d.ts +38 -0
  391. package/types/responsive-detector.d.ts +29 -0
  392. package/types/retry-policy.d.ts +20 -0
  393. package/types/robots-txt.d.ts +9 -0
  394. package/types/screen-reader.d.ts +16 -0
  395. package/types/screenshot-masker.d.ts +28 -0
  396. package/types/seo-checker.d.ts +33 -0
  397. package/types/session-capture.d.ts +38 -0
  398. package/types/session-export.d.ts +49 -0
  399. package/types/sitemap-writer.d.ts +21 -0
  400. package/types/sitemap.d.ts +51 -0
  401. package/types/skyvern.d.ts +56 -0
  402. package/types/sqs-queue.d.ts +2 -0
  403. package/types/sso-token.d.ts +29 -0
  404. package/types/stealth/human-behavior.d.ts +20 -0
  405. package/types/telemetry.d.ts +34 -0
  406. package/types/test-executor.d.ts +67 -0
  407. package/types/tls-fingerprint.d.ts +50 -0
  408. package/types/url-pattern-completer.d.ts +28 -0
  409. package/types/visual-diff-verdict.d.ts +40 -0
  410. package/types/webhook-notifier.d.ts +30 -0
  411. package/types/websocket-detector.d.ts +24 -0
  412. package/types/worker.d.ts +1 -0
@@ -0,0 +1,58 @@
1
+ function guessPurpose(fields, action) {
2
+ const names = fields.map((f) => f.name?.toLowerCase() + (f.label ?? "").toLowerCase()).join(" ");
3
+ if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4) return "login";
4
+ if (/password/.test(names) && /confirm|repeat/.test(names)) return "registration";
5
+ if (/search|query|q\b/.test(names) && fields.length <= 3) return "search";
6
+ if (/card|payment|cvv|expir/.test(names)) return "checkout";
7
+ if (/message|subject|contact/.test(names)) return "contact";
8
+ if (/setting|preference|profile/.test(names)) return "settings";
9
+ return "unknown";
10
+ }
11
+ async function detectForms(page) {
12
+ try {
13
+ return await page.$$eval("form", (forms) => forms.map((form, index) => {
14
+ const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]'));
15
+ const fields = [];
16
+ for (const input of inputs) {
17
+ const tag = input.tagName.toLowerCase();
18
+ const type = tag === "button" ? "submit" : input.type || tag;
19
+ if (["submit", "reset", "button", "hidden"].includes(type)) continue;
20
+ let label = "";
21
+ if (input.id) {
22
+ const lbl = form.querySelector(`label[for="${input.id}"]`);
23
+ if (lbl) label = (lbl.textContent ?? "").trim();
24
+ }
25
+ if (!label) {
26
+ const parent = input.closest("label");
27
+ if (parent) label = (parent.textContent ?? "").replace(input.value ?? "", "").trim().slice(0, 80);
28
+ }
29
+ const options = tag === "select" ? Array.from(input.options ?? []).map((o) => o.text?.trim()).filter(Boolean).slice(0, 20) : tag === "input" && type === "radio" ? [input.value] : void 0;
30
+ fields.push({
31
+ name: input.name || input.id || "",
32
+ type,
33
+ label: label || void 0,
34
+ required: !!input.required,
35
+ placeholder: input.placeholder || void 0,
36
+ minLength: input.minLength > 0 ? input.minLength : void 0,
37
+ maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : void 0,
38
+ min: input.min || void 0,
39
+ max: input.max || void 0,
40
+ pattern: input.pattern || void 0,
41
+ options,
42
+ autocomplete: input.autocomplete || void 0
43
+ });
44
+ }
45
+ const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]');
46
+ const hasFileUpload = !!form.querySelector('input[type="file"]');
47
+ const action = form.action || void 0;
48
+ const method = (form.method || "get").toUpperCase();
49
+ return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || void 0, hasFileUpload };
50
+ })).then((forms) => forms.map((f) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+ export {
56
+ detectForms
57
+ };
58
+ //# sourceMappingURL=form-detector.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/form-detector.ts"],
4
+ "sourcesContent": ["/**\n * Form detector \u2014 extract form metadata from crawled pages.\n *\n * For each form found on a page, captures: action URL, method, all input fields\n * with their types/names/required/validation constraints, and submit button text.\n *\n * This data feeds ZeTa's test generator to create form validation test cases\n * automatically (happy path + boundary/error cases per field type).\n */\n\nimport type { Page } from 'playwright';\n\nexport interface FormField {\n name: string;\n type: string; // text, email, password, number, select, textarea, checkbox, radio, file, date\n label?: string;\n required: boolean;\n placeholder?: string;\n minLength?: number;\n maxLength?: number;\n min?: string;\n max?: string;\n pattern?: string;\n options?: string[]; // for select/radio\n autocomplete?: string;\n}\n\nexport interface DetectedForm {\n index: number;\n action?: string;\n method: string;\n fields: FormField[];\n submitText?: string;\n hasFileUpload: boolean;\n purpose: 'login' | 'search' | 'registration' | 'contact' | 'checkout' | 'settings' | 'unknown';\n}\n\nfunction guessPurpose(fields: FormField[], action?: string): DetectedForm['purpose'] {\n const names = fields.map(f => f.name?.toLowerCase() + (f.label ?? '').toLowerCase()).join(' ');\n if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4) return 'login';\n if (/password/.test(names) && /confirm|repeat/.test(names)) return 'registration';\n if (/search|query|q\\b/.test(names) && fields.length <= 3) return 'search';\n if (/card|payment|cvv|expir/.test(names)) return 'checkout';\n if (/message|subject|contact/.test(names)) return 'contact';\n if (/setting|preference|profile/.test(names)) return 'settings';\n return 'unknown';\n}\n\nexport async function detectForms(page: Page): Promise<DetectedForm[]> {\n try {\n return await page.$$eval('form', (forms) => forms.map((form: any, index: number) => {\n const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type=\"submit\"]'));\n const fields: any[] = [];\n\n for (const input of inputs as any[]) {\n const tag = input.tagName.toLowerCase();\n const type = tag === 'button' ? 'submit' : (input.type || tag);\n if (['submit', 'reset', 'button', 'hidden'].includes(type)) continue;\n\n // Find associated label\n let label = '';\n if (input.id) {\n const lbl = form.querySelector(`label[for=\"${input.id}\"]`);\n if (lbl) label = (lbl.textContent ?? '').trim();\n }\n if (!label) {\n const parent = input.closest('label');\n if (parent) label = (parent.textContent ?? '').replace(input.value ?? '', '').trim().slice(0, 80);\n }\n\n const options = tag === 'select'\n ? Array.from(input.options ?? []).map((o: any) => o.text?.trim()).filter(Boolean).slice(0, 20)\n : tag === 'input' && type === 'radio'\n ? [input.value]\n : undefined;\n\n fields.push({\n name: input.name || input.id || '',\n type,\n label: label || undefined,\n required: !!input.required,\n placeholder: input.placeholder || undefined,\n minLength: input.minLength > 0 ? input.minLength : undefined,\n maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : undefined,\n min: input.min || undefined,\n max: input.max || undefined,\n pattern: input.pattern || undefined,\n options,\n autocomplete: input.autocomplete || undefined,\n });\n }\n\n const submitBtn = form.querySelector('button[type=\"submit\"],input[type=\"submit\"]') as any;\n const hasFileUpload = !!form.querySelector('input[type=\"file\"]');\n const action = form.action || undefined;\n const method = (form.method || 'get').toUpperCase();\n\n return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || undefined, hasFileUpload };\n })).then(forms => forms.map((f: any) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));\n } catch {\n return [];\n }\n}\n"],
5
+ "mappings": "AAqCA,SAAS,aAAa,QAAqB,QAA0C;AACnF,QAAM,QAAQ,OAAO,IAAI,OAAK,EAAE,MAAM,YAAY,KAAK,EAAE,SAAS,IAAI,YAAY,CAAC,EAAE,KAAK,GAAG;AAC7F,MAAI,WAAW,KAAK,KAAK,KAAK,sBAAsB,KAAK,KAAK,KAAK,OAAO,UAAU,EAAG,QAAO;AAC9F,MAAI,WAAW,KAAK,KAAK,KAAK,iBAAiB,KAAK,KAAK,EAAG,QAAO;AACnE,MAAI,mBAAmB,KAAK,KAAK,KAAK,OAAO,UAAU,EAAG,QAAO;AACjE,MAAI,yBAAyB,KAAK,KAAK,EAAG,QAAO;AACjD,MAAI,0BAA0B,KAAK,KAAK,EAAG,QAAO;AAClD,MAAI,6BAA6B,KAAK,KAAK,EAAG,QAAO;AACrD,SAAO;AACT;AAEA,eAAsB,YAAY,MAAqC;AACrE,MAAI;AACF,WAAO,MAAM,KAAK,OAAO,QAAQ,CAAC,UAAU,MAAM,IAAI,CAAC,MAAW,UAAkB;AAClF,YAAM,SAAS,MAAM,KAAK,KAAK,iBAAiB,6CAA6C,CAAC;AAC9F,YAAM,SAAgB,CAAC;AAEvB,iBAAW,SAAS,QAAiB;AACnC,cAAM,MAAM,MAAM,QAAQ,YAAY;AACtC,cAAM,OAAO,QAAQ,WAAW,WAAY,MAAM,QAAQ;AAC1D,YAAI,CAAC,UAAU,SAAS,UAAU,QAAQ,EAAE,SAAS,IAAI,EAAG;AAG5D,YAAI,QAAQ;AACZ,YAAI,MAAM,IAAI;AACZ,gBAAM,MAAM,KAAK,cAAc,cAAc,MAAM,EAAE,IAAI;AACzD,cAAI,IAAK,UAAS,IAAI,eAAe,IAAI,KAAK;AAAA,QAChD;AACA,YAAI,CAAC,OAAO;AACV,gBAAM,SAAS,MAAM,QAAQ,OAAO;AACpC,cAAI,OAAQ,UAAS,OAAO,eAAe,IAAI,QAAQ,MAAM,SAAS,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE;AAAA,QAClG;AAEA,cAAM,UAAU,QAAQ,WACpB,MAAM,KAAK,MAAM,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,MAAW,EAAE,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO,EAAE,MAAM,GAAG,EAAE,IAC3F,QAAQ,WAAW,SAAS,UAC5B,CAAC,MAAM,KAAK,IACZ;AAEJ,eAAO,KAAK;AAAA,UACV,MAAM,MAAM,QAAQ,MAAM,MAAM;AAAA,UAChC;AAAA,UACA,OAAO,SAAS;AAAA,UAChB,UAAU,CAAC,CAAC,MAAM;AAAA,UAClB,aAAa,MAAM,eAAe;AAAA,UAClC,WAAW,MAAM,YAAY,IAAI,MAAM,YAAY;AAAA,UACnD,WAAW,MAAM,YAAY,KAAK,MAAM,YAAY,SAAS,MAAM,YAAY;AAAA,UAC/E,KAAK,MAAM,OAAO;AAAA,UAClB,KAAK,MAAM,OAAO;AAAA,UAClB,SAAS,MAAM,WAAW;AAAA,UAC1B;AAAA,UACA,cAAc,MAAM,gBAAgB;AAAA,QACtC,CAAC;AAAA,MACH;AAEA,YAAM,YAAY,KAAK,cAAc,4CAA4C;AACjF,YAAM,gBAAgB,CAAC,CAAC,KAAK,cAAc,oBAAoB;AAC/D,YAAM,SAAS,KAAK,UAAU;AAC9B,YAAM,UAAU,KAAK,UAAU,OAAO,YAAY;AAElD,aAAO,EAAE,OAAO,QAAQ,QAAQ,QAAQ,YAAY,WAAW,aAAa,KAAK,KAAK,WAAW,SAAS,QAAW,cAAc;AAAA,IACrI,CAAC,CAAC,EAAE,KAAK,WAAS,MAAM,IAAI,CAAC,OAAY,EAAE,GAAG,GAAG,SAAS,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC;AAAA,EAChG,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Configurable crawl traversal strategy — matches crawl4AI's BFS/DFS/
3
+ * Best-First deep-crawl strategies (ZeTa previously had a single hardcoded
4
+ * FIFO queue, i.e. always BFS).
5
+ *
6
+ * Array-like on purpose (`length`, `shift()`, `push()`, `filter()`) so it
7
+ * drops into crawler.ts's existing queue call sites with a rename, not a
8
+ * rewrite — BFS's `shift()` behavior is byte-for-byte the same as the plain
9
+ * array it replaces, so BFS (the default) has zero behavior change.
10
+ */
11
+ export type TraversalStrategy = 'BFS' | 'DFS' | 'BEST_FIRST';
12
+ export declare class CrawlFrontier {
13
+ private items;
14
+ private readonly strategy;
15
+ private readonly keywords;
16
+ private readonly urlFilter;
17
+ constructor(strategy: TraversalStrategy, seedUrls: string[], keywords?: string[], urlFilter?: string);
18
+ get length(): number;
19
+ /**
20
+ * P3b: sparse-parent boost — if the parent page had fewer than 5 interactive
21
+ * elements (thin/canvas page), its child URLs score +1 in BEST_FIRST mode so
22
+ * the frontier revisits sparse branches before deep-linked content.
23
+ */
24
+ push(url: string, pushOpts?: {
25
+ parentElements?: number;
26
+ }): void;
27
+ /** Dequeues the next URL per this frontier's strategy. undefined if empty. */
28
+ shift(): string | undefined;
29
+ filter(predicate: (url: string) => boolean): string[];
30
+ /** Remove queued URLs not matching the regex pattern. Returns count removed. */
31
+ filterByPattern(pattern: string): number;
32
+ }
@@ -0,0 +1,83 @@
1
+ function scoreUrl(url, keywords) {
2
+ try {
3
+ const u = new URL(url);
4
+ const segments = u.pathname.split("/").filter(Boolean);
5
+ let score = -segments.length;
6
+ if (keywords.length > 0) {
7
+ const lower = url.toLowerCase();
8
+ for (const kw of keywords) {
9
+ if (kw && lower.includes(kw)) score += 10;
10
+ }
11
+ }
12
+ return score;
13
+ } catch {
14
+ return 0;
15
+ }
16
+ }
17
+ class CrawlFrontier {
18
+ items = [];
19
+ strategy;
20
+ keywords;
21
+ urlFilter;
22
+ constructor(strategy, seedUrls, keywords = [], urlFilter) {
23
+ this.strategy = strategy;
24
+ this.keywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
25
+ this.urlFilter = urlFilter ? (() => {
26
+ try {
27
+ return new RegExp(urlFilter, "i");
28
+ } catch {
29
+ return null;
30
+ }
31
+ })() : null;
32
+ for (const url of seedUrls) this.push(url);
33
+ }
34
+ get length() {
35
+ return this.items.length;
36
+ }
37
+ /**
38
+ * P3b: sparse-parent boost — if the parent page had fewer than 5 interactive
39
+ * elements (thin/canvas page), its child URLs score +1 in BEST_FIRST mode so
40
+ * the frontier revisits sparse branches before deep-linked content.
41
+ */
42
+ push(url, pushOpts) {
43
+ if (this.urlFilter && !this.urlFilter.test(url)) return;
44
+ const sparseBoost = this.strategy === "BEST_FIRST" && pushOpts?.parentElements !== void 0 && pushOpts.parentElements < 5 ? 1 : 0;
45
+ this.items.push({ url, score: this.strategy === "BEST_FIRST" ? scoreUrl(url, this.keywords) + sparseBoost : 0 });
46
+ }
47
+ /** Dequeues the next URL per this frontier's strategy. undefined if empty. */
48
+ shift() {
49
+ if (this.items.length === 0) return void 0;
50
+ switch (this.strategy) {
51
+ case "DFS":
52
+ return this.items.pop().url;
53
+ case "BEST_FIRST": {
54
+ let bestIdx = 0;
55
+ for (let i = 1; i < this.items.length; i++) {
56
+ if (this.items[i].score > this.items[bestIdx].score) bestIdx = i;
57
+ }
58
+ return this.items.splice(bestIdx, 1)[0].url;
59
+ }
60
+ case "BFS":
61
+ default:
62
+ return this.items.shift().url;
63
+ }
64
+ }
65
+ filter(predicate) {
66
+ return this.items.filter((e) => predicate(e.url)).map((e) => e.url);
67
+ }
68
+ /** Remove queued URLs not matching the regex pattern. Returns count removed. */
69
+ filterByPattern(pattern) {
70
+ try {
71
+ const re = new RegExp(pattern, "i");
72
+ const before = this.items.length;
73
+ this.items = this.items.filter((e) => re.test(e.url));
74
+ return before - this.items.length;
75
+ } catch {
76
+ return 0;
77
+ }
78
+ }
79
+ }
80
+ export {
81
+ CrawlFrontier
82
+ };
83
+ //# sourceMappingURL=frontier.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/frontier.ts"],
4
+ "sourcesContent": ["/**\n * Configurable crawl traversal strategy \u2014 matches crawl4AI's BFS/DFS/\n * Best-First deep-crawl strategies (ZeTa previously had a single hardcoded\n * FIFO queue, i.e. always BFS).\n *\n * Array-like on purpose (`length`, `shift()`, `push()`, `filter()`) so it\n * drops into crawler.ts's existing queue call sites with a rename, not a\n * rewrite \u2014 BFS's `shift()` behavior is byte-for-byte the same as the plain\n * array it replaces, so BFS (the default) has zero behavior change.\n */\n\nexport type TraversalStrategy = 'BFS' | 'DFS' | 'BEST_FIRST';\n\ninterface FrontierEntry {\n url: string;\n score: number;\n}\n\n/**\n * Best-First scoring heuristic: shallower paths score higher (breadth-like\n * bias toward top-level sections before deep sub-pages), boosted further by\n * any user-supplied keyword appearing in the URL. Simple and transparent by\n * design \u2014 crawl4AI's own default scorer is a similarly simple keyword-\n * relevance heuristic, not a learned model.\n */\nfunction scoreUrl(url: string, keywords: string[]): number {\n try {\n const u = new URL(url);\n const segments = u.pathname.split('/').filter(Boolean);\n let score = -segments.length;\n if (keywords.length > 0) {\n const lower = url.toLowerCase();\n for (const kw of keywords) {\n if (kw && lower.includes(kw)) score += 10;\n }\n }\n return score;\n } catch {\n return 0;\n }\n}\n\nexport class CrawlFrontier {\n private items: FrontierEntry[] = [];\n private readonly strategy: TraversalStrategy;\n private readonly keywords: string[];\n private readonly urlFilter: RegExp | null;\n\n constructor(strategy: TraversalStrategy, seedUrls: string[], keywords: string[] = [], urlFilter?: string) {\n this.strategy = strategy;\n this.keywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);\n this.urlFilter = urlFilter\n ? (() => { try { return new RegExp(urlFilter, 'i'); } catch { return null; } })()\n : null;\n for (const url of seedUrls) this.push(url);\n }\n\n get length(): number {\n return this.items.length;\n }\n\n /**\n * P3b: sparse-parent boost \u2014 if the parent page had fewer than 5 interactive\n * elements (thin/canvas page), its child URLs score +1 in BEST_FIRST mode so\n * the frontier revisits sparse branches before deep-linked content.\n */\n push(url: string, pushOpts?: { parentElements?: number }): void {\n if (this.urlFilter && !this.urlFilter.test(url)) return;\n const sparseBoost =\n this.strategy === 'BEST_FIRST' &&\n pushOpts?.parentElements !== undefined &&\n pushOpts.parentElements < 5\n ? 1\n : 0;\n this.items.push({ url, score: this.strategy === 'BEST_FIRST' ? scoreUrl(url, this.keywords) + sparseBoost : 0 });\n }\n\n /** Dequeues the next URL per this frontier's strategy. undefined if empty. */\n shift(): string | undefined {\n if (this.items.length === 0) return undefined;\n switch (this.strategy) {\n case 'DFS':\n return this.items.pop()!.url;\n case 'BEST_FIRST': {\n let bestIdx = 0;\n for (let i = 1; i < this.items.length; i++) {\n if (this.items[i].score > this.items[bestIdx].score) bestIdx = i;\n }\n return this.items.splice(bestIdx, 1)[0].url;\n }\n case 'BFS':\n default:\n return this.items.shift()!.url;\n }\n }\n\n filter(predicate: (url: string) => boolean): string[] {\n return this.items.filter((e) => predicate(e.url)).map((e) => e.url);\n }\n\n /** Remove queued URLs not matching the regex pattern. Returns count removed. */\n filterByPattern(pattern: string): number {\n try {\n const re = new RegExp(pattern, 'i');\n const before = this.items.length;\n this.items = this.items.filter((e) => re.test(e.url));\n return before - this.items.length;\n } catch {\n return 0;\n }\n }\n}\n"],
5
+ "mappings": "AAyBA,SAAS,SAAS,KAAa,UAA4B;AACzD,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,UAAM,WAAW,EAAE,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACrD,QAAI,QAAQ,CAAC,SAAS;AACtB,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,QAAQ,IAAI,YAAY;AAC9B,iBAAW,MAAM,UAAU;AACzB,YAAI,MAAM,MAAM,SAAS,EAAE,EAAG,UAAS;AAAA,MACzC;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,MAAM,cAAc;AAAA,EACjB,QAAyB,CAAC;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAA6B,UAAoB,WAAqB,CAAC,GAAG,WAAoB;AACxG,SAAK,WAAW;AAChB,SAAK,WAAW,SAAS,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,OAAO;AACnE,SAAK,YAAY,aACZ,MAAM;AAAE,UAAI;AAAE,eAAO,IAAI,OAAO,WAAW,GAAG;AAAA,MAAG,QAAQ;AAAE,eAAO;AAAA,MAAM;AAAA,IAAE,GAAG,IAC9E;AACJ,eAAW,OAAO,SAAU,MAAK,KAAK,GAAG;AAAA,EAC3C;AAAA,EAEA,IAAI,SAAiB;AACnB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAK,KAAa,UAA8C;AAC9D,QAAI,KAAK,aAAa,CAAC,KAAK,UAAU,KAAK,GAAG,EAAG;AACjD,UAAM,cACJ,KAAK,aAAa,gBAClB,UAAU,mBAAmB,UAC7B,SAAS,iBAAiB,IACtB,IACA;AACN,SAAK,MAAM,KAAK,EAAE,KAAK,OAAO,KAAK,aAAa,eAAe,SAAS,KAAK,KAAK,QAAQ,IAAI,cAAc,EAAE,CAAC;AAAA,EACjH;AAAA;AAAA,EAGA,QAA4B;AAC1B,QAAI,KAAK,MAAM,WAAW,EAAG,QAAO;AACpC,YAAQ,KAAK,UAAU;AAAA,MACrB,KAAK;AACH,eAAO,KAAK,MAAM,IAAI,EAAG;AAAA,MAC3B,KAAK,cAAc;AACjB,YAAI,UAAU;AACd,iBAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,cAAI,KAAK,MAAM,CAAC,EAAE,QAAQ,KAAK,MAAM,OAAO,EAAE,MAAO,WAAU;AAAA,QACjE;AACA,eAAO,KAAK,MAAM,OAAO,SAAS,CAAC,EAAE,CAAC,EAAE;AAAA,MAC1C;AAAA,MACA,KAAK;AAAA,MACL;AACE,eAAO,KAAK,MAAM,MAAM,EAAG;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,OAAO,WAA+C;AACpD,WAAO,KAAK,MAAM,OAAO,CAAC,MAAM,UAAU,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG;AAAA,EACpE;AAAA;AAAA,EAGA,gBAAgB,SAAyB;AACvC,QAAI;AACF,YAAM,KAAK,IAAI,OAAO,SAAS,GAAG;AAClC,YAAM,SAAS,KAAK,MAAM;AAC1B,WAAK,QAAQ,KAAK,MAAM,OAAO,CAAC,MAAM,GAAG,KAAK,EAAE,GAAG,CAAC;AACpD,aAAO,SAAS,KAAK,MAAM;AAAA,IAC7B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * P6: Gap Pass — LLM-guided second crawl pass
3
+ *
4
+ * After the primary BFS crawl completes, this module:
5
+ * 1. Sends the discovered screen list to an LLM for gap analysis
6
+ * 2. HEAD-validates each suggested URL
7
+ * 3. Returns confirmed gap URLs for targeted re-crawl
8
+ *
9
+ * The caller (crawler.ts) is responsible for actually visiting the confirmed
10
+ * URLs with the existing browser — this module only identifies them.
11
+ */
12
+ export interface GapCandidate {
13
+ category: string;
14
+ urlPatterns: string[];
15
+ confidence: number;
16
+ rationale: string;
17
+ confirmedUrl: string | null;
18
+ headStatus: number | null;
19
+ }
20
+ interface DiscoveredScreen {
21
+ url: string;
22
+ name?: string;
23
+ elements?: number;
24
+ }
25
+ interface LlmCfg {
26
+ provider: string;
27
+ model: string;
28
+ apiKey?: string;
29
+ }
30
+ /**
31
+ * Run LLM gap analysis + HEAD validation.
32
+ * Returns candidates with `confirmedUrl` set for those where HEAD returned 200.
33
+ */
34
+ export declare function runGapAnalysis(screens: DiscoveredScreen[], appUrl: string, llmCfg: LlmCfg | null | undefined): Promise<GapCandidate[]>;
35
+ export {};
@@ -0,0 +1,81 @@
1
+ import { logger as rootLogger } from "./logger.js";
2
+ const logger = rootLogger.child({ module: "gap-pass" });
3
+ async function runGapAnalysis(screens, appUrl, llmCfg) {
4
+ if (!llmCfg?.apiKey || screens.length < 2) return [];
5
+ const origin = (() => {
6
+ try {
7
+ return new URL(appUrl).origin;
8
+ } catch {
9
+ return "";
10
+ }
11
+ })();
12
+ if (!origin) return [];
13
+ const screenList = screens.slice(0, 80).map((s) => {
14
+ try {
15
+ return new URL(s.url).pathname + (s.name ? ` (${s.name})` : "");
16
+ } catch {
17
+ return s.url;
18
+ }
19
+ }).join("\n");
20
+ const prompt = `You are analyzing the screen map of a web application to find structural coverage gaps.
21
+
22
+ Discovered screens (${screens.length} total):
23
+ ${screenList}
24
+
25
+ App URL: ${appUrl}
26
+
27
+ Based on these screens and common patterns for this type of web application, identify screen categories that are likely present but missing. Focus on pages that would exist in a typical SaaS or web app but are not represented above.
28
+
29
+ Respond with ONLY valid JSON, no markdown:
30
+ {"gaps":[{"category":"User profile edit","urlPatterns":["/profile/edit","/account/profile"],"confidence":0.8,"rationale":"Profile view found but no edit page"},{"category":"Billing history","urlPatterns":["/settings/billing","/account/billing"],"confidence":0.7,"rationale":"Settings section found but no billing page"}]}`;
31
+ try {
32
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
33
+ const client = new Anthropic({ apiKey: llmCfg.apiKey });
34
+ const response = await client.messages.create({
35
+ model: llmCfg.model ?? "claude-haiku-4-5-20251001",
36
+ max_tokens: 1024,
37
+ // @ts-ignore
38
+ system: [{ type: "text", text: "You are a web application coverage analyst. Respond with valid JSON only.", cache_control: { type: "ephemeral" } }],
39
+ messages: [{ role: "user", content: prompt }]
40
+ });
41
+ const text = response.content[0]?.text ?? "";
42
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
43
+ if (!jsonMatch) return [];
44
+ const parsed = JSON.parse(jsonMatch[0]);
45
+ const rawGaps = parsed.gaps ?? [];
46
+ if (!Array.isArray(rawGaps) || rawGaps.length === 0) return [];
47
+ logger.info({ gaps: rawGaps.length }, "[P6] Gap analysis: LLM returned candidates");
48
+ const results = [];
49
+ for (const gap of rawGaps.slice(0, 15)) {
50
+ let confirmedUrl = null;
51
+ let headStatus = null;
52
+ for (const pattern of (gap.urlPatterns ?? []).slice(0, 4)) {
53
+ const fullUrl = pattern.startsWith("http") ? pattern : `${origin}${pattern.startsWith("/") ? "" : "/"}${pattern}`;
54
+ try {
55
+ const controller = new AbortController();
56
+ const timeout = setTimeout(() => controller.abort(), 6e3);
57
+ const res = await fetch(fullUrl, { method: "HEAD", signal: controller.signal, redirect: "follow" }).catch(() => null);
58
+ clearTimeout(timeout);
59
+ headStatus = res?.status ?? null;
60
+ if (res?.ok) {
61
+ confirmedUrl = fullUrl;
62
+ break;
63
+ }
64
+ } catch {
65
+ }
66
+ }
67
+ results.push({ category: gap.category, urlPatterns: gap.urlPatterns, confidence: gap.confidence, rationale: gap.rationale, confirmedUrl, headStatus });
68
+ logger.info({ category: gap.category, confirmedUrl, headStatus }, "[P6] Gap candidate checked");
69
+ }
70
+ const confirmed = results.filter((r) => r.confirmedUrl).length;
71
+ logger.info({ total: results.length, confirmed }, "[P6] Gap pass analysis complete");
72
+ return results;
73
+ } catch (err) {
74
+ logger.warn({ err: String(err) }, "[P6] Gap analysis failed \u2014 non-fatal");
75
+ return [];
76
+ }
77
+ }
78
+ export {
79
+ runGapAnalysis
80
+ };
81
+ //# sourceMappingURL=gap-pass.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/gap-pass.ts"],
4
+ "sourcesContent": ["/**\n * P6: Gap Pass \u2014 LLM-guided second crawl pass\n *\n * After the primary BFS crawl completes, this module:\n * 1. Sends the discovered screen list to an LLM for gap analysis\n * 2. HEAD-validates each suggested URL\n * 3. Returns confirmed gap URLs for targeted re-crawl\n *\n * The caller (crawler.ts) is responsible for actually visiting the confirmed\n * URLs with the existing browser \u2014 this module only identifies them.\n */\n\nimport { logger as rootLogger } from './logger.js';\n\nconst logger = rootLogger.child({ module: 'gap-pass' });\n\nexport interface GapCandidate {\n category: string;\n urlPatterns: string[];\n confidence: number;\n rationale: string;\n confirmedUrl: string | null;\n headStatus: number | null;\n}\n\ninterface DiscoveredScreen {\n url: string;\n name?: string;\n elements?: number;\n}\n\ninterface LlmCfg {\n provider: string;\n model: string;\n apiKey?: string;\n}\n\n/**\n * Run LLM gap analysis + HEAD validation.\n * Returns candidates with `confirmedUrl` set for those where HEAD returned 200.\n */\nexport async function runGapAnalysis(\n screens: DiscoveredScreen[],\n appUrl: string,\n llmCfg: LlmCfg | null | undefined,\n): Promise<GapCandidate[]> {\n if (!llmCfg?.apiKey || screens.length < 2) return [];\n\n const origin = (() => { try { return new URL(appUrl).origin; } catch { return ''; } })();\n if (!origin) return [];\n\n // Cap context \u2014 only send url + name, not full element data\n const screenList = screens\n .slice(0, 80)\n .map(s => {\n try { return new URL(s.url).pathname + (s.name ? ` (${s.name})` : ''); } catch { return s.url; }\n })\n .join('\\n');\n\n const prompt = `You are analyzing the screen map of a web application to find structural coverage gaps.\n\nDiscovered screens (${screens.length} total):\n${screenList}\n\nApp URL: ${appUrl}\n\nBased on these screens and common patterns for this type of web application, identify screen categories that are likely present but missing. Focus on pages that would exist in a typical SaaS or web app but are not represented above.\n\nRespond with ONLY valid JSON, no markdown:\n{\"gaps\":[{\"category\":\"User profile edit\",\"urlPatterns\":[\"/profile/edit\",\"/account/profile\"],\"confidence\":0.8,\"rationale\":\"Profile view found but no edit page\"},{\"category\":\"Billing history\",\"urlPatterns\":[\"/settings/billing\",\"/account/billing\"],\"confidence\":0.7,\"rationale\":\"Settings section found but no billing page\"}]}`;\n\n try {\n const Anthropic = (await import('@anthropic-ai/sdk')).default;\n const client = new Anthropic({ apiKey: llmCfg.apiKey });\n const response = await client.messages.create({\n model: llmCfg.model ?? 'claude-haiku-4-5-20251001',\n max_tokens: 1024,\n // @ts-ignore\n system: [{ type: 'text', text: 'You are a web application coverage analyst. Respond with valid JSON only.', cache_control: { type: 'ephemeral' } }],\n messages: [{ role: 'user', content: prompt }],\n });\n\n const text = (response.content[0] as any)?.text ?? '';\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) return [];\n const parsed = JSON.parse(jsonMatch[0]);\n const rawGaps: Array<{ category: string; urlPatterns: string[]; confidence: number; rationale: string }> =\n parsed.gaps ?? [];\n\n if (!Array.isArray(rawGaps) || rawGaps.length === 0) return [];\n logger.info({ gaps: rawGaps.length }, '[P6] Gap analysis: LLM returned candidates');\n\n // HEAD-validate each URL pattern, take the first 200\n const results: GapCandidate[] = [];\n for (const gap of rawGaps.slice(0, 15)) {\n let confirmedUrl: string | null = null;\n let headStatus: number | null = null;\n\n for (const pattern of (gap.urlPatterns ?? []).slice(0, 4)) {\n const fullUrl = pattern.startsWith('http') ? pattern : `${origin}${pattern.startsWith('/') ? '' : '/'}${pattern}`;\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 6_000);\n const res = await fetch(fullUrl, { method: 'HEAD', signal: controller.signal, redirect: 'follow' }).catch(() => null);\n clearTimeout(timeout);\n headStatus = res?.status ?? null;\n if (res?.ok) { confirmedUrl = fullUrl; break; }\n } catch { /* non-fatal */ }\n }\n\n results.push({ category: gap.category, urlPatterns: gap.urlPatterns, confidence: gap.confidence, rationale: gap.rationale, confirmedUrl, headStatus });\n logger.info({ category: gap.category, confirmedUrl, headStatus }, '[P6] Gap candidate checked');\n }\n\n const confirmed = results.filter(r => r.confirmedUrl).length;\n logger.info({ total: results.length, confirmed }, '[P6] Gap pass analysis complete');\n return results;\n } catch (err) {\n logger.warn({ err: String(err) }, '[P6] Gap analysis failed \u2014 non-fatal');\n return [];\n }\n}\n"],
5
+ "mappings": "AAYA,SAAS,UAAU,kBAAkB;AAErC,MAAM,SAAS,WAAW,MAAM,EAAE,QAAQ,WAAW,CAAC;AA2BtD,eAAsB,eACpB,SACA,QACA,QACyB;AACzB,MAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,EAAG,QAAO,CAAC;AAEnD,QAAM,UAAU,MAAM;AAAE,QAAI;AAAE,aAAO,IAAI,IAAI,MAAM,EAAE;AAAA,IAAQ,QAAQ;AAAE,aAAO;AAAA,IAAI;AAAA,EAAE,GAAG;AACvF,MAAI,CAAC,OAAQ,QAAO,CAAC;AAGrB,QAAM,aAAa,QAChB,MAAM,GAAG,EAAE,EACX,IAAI,OAAK;AACR,QAAI;AAAE,aAAO,IAAI,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,KAAK,EAAE,IAAI,MAAM;AAAA,IAAK,QAAQ;AAAE,aAAO,EAAE;AAAA,IAAK;AAAA,EACjG,CAAC,EACA,KAAK,IAAI;AAEZ,QAAM,SAAS;AAAA;AAAA,sBAEK,QAAQ,MAAM;AAAA,EAClC,UAAU;AAAA;AAAA,WAED,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAOf,MAAI;AACF,UAAM,aAAa,MAAM,OAAO,mBAAmB,GAAG;AACtD,UAAM,SAAS,IAAI,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC;AACtD,UAAM,WAAW,MAAM,OAAO,SAAS,OAAO;AAAA,MAC5C,OAAO,OAAO,SAAS;AAAA,MACvB,YAAY;AAAA;AAAA,MAEZ,QAAQ,CAAC,EAAE,MAAM,QAAQ,MAAM,6EAA6E,eAAe,EAAE,MAAM,YAAY,EAAE,CAAC;AAAA,MAClJ,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,OAAO,CAAC;AAAA,IAC9C,CAAC;AAED,UAAM,OAAQ,SAAS,QAAQ,CAAC,GAAW,QAAQ;AACnD,UAAM,YAAY,KAAK,MAAM,aAAa;AAC1C,QAAI,CAAC,UAAW,QAAO,CAAC;AACxB,UAAM,SAAS,KAAK,MAAM,UAAU,CAAC,CAAC;AACtC,UAAM,UACJ,OAAO,QAAQ,CAAC;AAElB,QAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO,CAAC;AAC7D,WAAO,KAAK,EAAE,MAAM,QAAQ,OAAO,GAAG,4CAA4C;AAGlF,UAAM,UAA0B,CAAC;AACjC,eAAW,OAAO,QAAQ,MAAM,GAAG,EAAE,GAAG;AACtC,UAAI,eAA8B;AAClC,UAAI,aAA4B;AAEhC,iBAAW,YAAY,IAAI,eAAe,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG;AACzD,cAAM,UAAU,QAAQ,WAAW,MAAM,IAAI,UAAU,GAAG,MAAM,GAAG,QAAQ,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,OAAO;AAC/G,YAAI;AACF,gBAAM,aAAa,IAAI,gBAAgB;AACvC,gBAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAK;AAC1D,gBAAM,MAAM,MAAM,MAAM,SAAS,EAAE,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,UAAU,SAAS,CAAC,EAAE,MAAM,MAAM,IAAI;AACpH,uBAAa,OAAO;AACpB,uBAAa,KAAK,UAAU;AAC5B,cAAI,KAAK,IAAI;AAAE,2BAAe;AAAS;AAAA,UAAO;AAAA,QAChD,QAAQ;AAAA,QAAkB;AAAA,MAC5B;AAEA,cAAQ,KAAK,EAAE,UAAU,IAAI,UAAU,aAAa,IAAI,aAAa,YAAY,IAAI,YAAY,WAAW,IAAI,WAAW,cAAc,WAAW,CAAC;AACrJ,aAAO,KAAK,EAAE,UAAU,IAAI,UAAU,cAAc,WAAW,GAAG,4BAA4B;AAAA,IAChG;AAEA,UAAM,YAAY,QAAQ,OAAO,OAAK,EAAE,YAAY,EAAE;AACtD,WAAO,KAAK,EAAE,OAAO,QAAQ,QAAQ,UAAU,GAAG,iCAAiC;AACnF,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO,KAAK,EAAE,KAAK,OAAO,GAAG,EAAE,GAAG,2CAAsC;AACxE,WAAO,CAAC;AAAA,EACV;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GraphQL detection + introspection.
3
+ *
4
+ * During crawl, if a GraphQL endpoint is discovered (via network observer or
5
+ * known-URL heuristics), fire a minimal introspection query to get the schema.
6
+ * The schema is stored in the crawl artifact for use during test generation.
7
+ */
8
+ export interface GqlType {
9
+ name: string;
10
+ kind: string;
11
+ fields?: Array<{
12
+ name: string;
13
+ type: string;
14
+ }>;
15
+ inputFields?: Array<{
16
+ name: string;
17
+ type: string;
18
+ }>;
19
+ enumValues?: string[];
20
+ }
21
+ export interface GqlSchema {
22
+ queryType?: string;
23
+ mutationType?: string;
24
+ subscriptionType?: string;
25
+ types: GqlType[];
26
+ }
27
+ export interface GqlIntrospectionResult {
28
+ endpoint: string;
29
+ schema: GqlSchema | null;
30
+ error?: string;
31
+ durationMs: number;
32
+ }
33
+ export declare function introspectGraphQL(endpoint: string, opts?: {
34
+ headers?: Record<string, string>;
35
+ timeoutMs?: number;
36
+ cookies?: string;
37
+ }): Promise<GqlIntrospectionResult>;
38
+ /** Guess GraphQL endpoint URLs from a known app URL. */
39
+ export declare function guessGraphQLEndpoints(appUrl: string): string[];
@@ -0,0 +1,86 @@
1
+ const INTROSPECTION_QUERY = `
2
+ query IntrospectionQuery {
3
+ __schema {
4
+ queryType { name }
5
+ mutationType { name }
6
+ subscriptionType { name }
7
+ types {
8
+ name
9
+ kind
10
+ fields(includeDeprecated: false) { name type { name kind ofType { name kind } } }
11
+ inputFields { name type { name kind ofType { name kind } } }
12
+ enumValues(includeDeprecated: false) { name }
13
+ }
14
+ }
15
+ }
16
+ `;
17
+ function flattenType(t) {
18
+ if (!t) return "unknown";
19
+ if (t.kind === "NON_NULL" || t.kind === "LIST") return flattenType(t.ofType);
20
+ return t.name ?? "unknown";
21
+ }
22
+ function parseSchema(raw) {
23
+ const s = raw?.__schema ?? raw?.data?.__schema ?? raw;
24
+ const types = (s?.types ?? []).filter((t) => !t.name?.startsWith("__")).map((t) => ({
25
+ name: t.name,
26
+ kind: t.kind,
27
+ fields: (t.fields ?? []).map((f) => ({ name: f.name, type: flattenType(f.type) })),
28
+ inputFields: (t.inputFields ?? []).map((f) => ({ name: f.name, type: flattenType(f.type) })),
29
+ enumValues: (t.enumValues ?? []).map((e) => e.name)
30
+ }));
31
+ return {
32
+ queryType: s?.queryType?.name,
33
+ mutationType: s?.mutationType?.name,
34
+ subscriptionType: s?.subscriptionType?.name,
35
+ types
36
+ };
37
+ }
38
+ async function introspectGraphQL(endpoint, opts = {}) {
39
+ const { headers = {}, timeoutMs = 1e4, cookies } = opts;
40
+ const t0 = Date.now();
41
+ try {
42
+ const reqHeaders = {
43
+ "Content-Type": "application/json",
44
+ "Accept": "application/json",
45
+ ...headers
46
+ };
47
+ if (cookies) reqHeaders["Cookie"] = cookies;
48
+ const res = await fetch(endpoint, {
49
+ method: "POST",
50
+ headers: reqHeaders,
51
+ body: JSON.stringify({ query: INTROSPECTION_QUERY }),
52
+ signal: AbortSignal.timeout(timeoutMs)
53
+ });
54
+ if (!res.ok) {
55
+ return { endpoint, schema: null, error: `HTTP ${res.status}`, durationMs: Date.now() - t0 };
56
+ }
57
+ const json = await res.json();
58
+ if (json?.errors?.length) {
59
+ const msg = json.errors[0]?.message ?? "introspection disabled";
60
+ return { endpoint, schema: null, error: msg, durationMs: Date.now() - t0 };
61
+ }
62
+ return { endpoint, schema: parseSchema(json?.data ?? json), durationMs: Date.now() - t0 };
63
+ } catch (e) {
64
+ return { endpoint, schema: null, error: e.message, durationMs: Date.now() - t0 };
65
+ }
66
+ }
67
+ function guessGraphQLEndpoints(appUrl) {
68
+ try {
69
+ const u = new URL(appUrl);
70
+ const base = `${u.protocol}//${u.host}`;
71
+ return [
72
+ `${base}/graphql`,
73
+ `${base}/api/graphql`,
74
+ `${base}/v1/graphql`,
75
+ `${base}/gql`,
76
+ `${base}/query`
77
+ ];
78
+ } catch {
79
+ return [];
80
+ }
81
+ }
82
+ export {
83
+ guessGraphQLEndpoints,
84
+ introspectGraphQL
85
+ };
86
+ //# sourceMappingURL=graphql-detector.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/graphql-detector.ts"],
4
+ "sourcesContent": ["/**\n * GraphQL detection + introspection.\n *\n * During crawl, if a GraphQL endpoint is discovered (via network observer or\n * known-URL heuristics), fire a minimal introspection query to get the schema.\n * The schema is stored in the crawl artifact for use during test generation.\n */\n\nexport interface GqlType {\n name: string;\n kind: string;\n fields?: Array<{ name: string; type: string }>;\n inputFields?: Array<{ name: string; type: string }>;\n enumValues?: string[];\n}\n\nexport interface GqlSchema {\n queryType?: string;\n mutationType?: string;\n subscriptionType?: string;\n types: GqlType[];\n}\n\nexport interface GqlIntrospectionResult {\n endpoint: string;\n schema: GqlSchema | null;\n error?: string;\n durationMs: number;\n}\n\nconst INTROSPECTION_QUERY = `\n query IntrospectionQuery {\n __schema {\n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n name\n kind\n fields(includeDeprecated: false) { name type { name kind ofType { name kind } } }\n inputFields { name type { name kind ofType { name kind } } }\n enumValues(includeDeprecated: false) { name }\n }\n }\n }\n`;\n\nfunction flattenType(t: any): string {\n if (!t) return 'unknown';\n if (t.kind === 'NON_NULL' || t.kind === 'LIST') return flattenType(t.ofType);\n return t.name ?? 'unknown';\n}\n\nfunction parseSchema(raw: any): GqlSchema {\n const s = raw?.__schema ?? raw?.data?.__schema ?? raw;\n const types: GqlType[] = (s?.types ?? [])\n .filter((t: any) => !t.name?.startsWith('__'))\n .map((t: any) => ({\n name: t.name,\n kind: t.kind,\n fields: (t.fields ?? []).map((f: any) => ({ name: f.name, type: flattenType(f.type) })),\n inputFields: (t.inputFields ?? []).map((f: any) => ({ name: f.name, type: flattenType(f.type) })),\n enumValues: (t.enumValues ?? []).map((e: any) => e.name),\n }));\n return {\n queryType: s?.queryType?.name,\n mutationType: s?.mutationType?.name,\n subscriptionType: s?.subscriptionType?.name,\n types,\n };\n}\n\nexport async function introspectGraphQL(\n endpoint: string,\n opts: {\n headers?: Record<string, string>;\n timeoutMs?: number;\n cookies?: string;\n } = {},\n): Promise<GqlIntrospectionResult> {\n const { headers = {}, timeoutMs = 10_000, cookies } = opts;\n const t0 = Date.now();\n try {\n const reqHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n ...headers,\n };\n if (cookies) reqHeaders['Cookie'] = cookies;\n\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: reqHeaders,\n body: JSON.stringify({ query: INTROSPECTION_QUERY }),\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n if (!res.ok) {\n return { endpoint, schema: null, error: `HTTP ${res.status}`, durationMs: Date.now() - t0 };\n }\n\n const json = await res.json();\n if (json?.errors?.length) {\n const msg = json.errors[0]?.message ?? 'introspection disabled';\n return { endpoint, schema: null, error: msg, durationMs: Date.now() - t0 };\n }\n\n return { endpoint, schema: parseSchema(json?.data ?? json), durationMs: Date.now() - t0 };\n } catch (e: any) {\n return { endpoint, schema: null, error: e.message, durationMs: Date.now() - t0 };\n }\n}\n\n/** Guess GraphQL endpoint URLs from a known app URL. */\nexport function guessGraphQLEndpoints(appUrl: string): string[] {\n try {\n const u = new URL(appUrl);\n const base = `${u.protocol}//${u.host}`;\n return [\n `${base}/graphql`,\n `${base}/api/graphql`,\n `${base}/v1/graphql`,\n `${base}/gql`,\n `${base}/query`,\n ];\n } catch {\n return [];\n }\n}\n"],
5
+ "mappings": "AA8BA,MAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiB5B,SAAS,YAAY,GAAgB;AACnC,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,EAAE,SAAS,cAAc,EAAE,SAAS,OAAQ,QAAO,YAAY,EAAE,MAAM;AAC3E,SAAO,EAAE,QAAQ;AACnB;AAEA,SAAS,YAAY,KAAqB;AACxC,QAAM,IAAI,KAAK,YAAY,KAAK,MAAM,YAAY;AAClD,QAAM,SAAoB,GAAG,SAAS,CAAC,GACpC,OAAO,CAAC,MAAW,CAAC,EAAE,MAAM,WAAW,IAAI,CAAC,EAC5C,IAAI,CAAC,OAAY;AAAA,IAChB,MAAM,EAAE;AAAA,IACR,MAAM,EAAE;AAAA,IACR,SAAS,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,OAAY,EAAE,MAAM,EAAE,MAAM,MAAM,YAAY,EAAE,IAAI,EAAE,EAAE;AAAA,IACtF,cAAc,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC,OAAY,EAAE,MAAM,EAAE,MAAM,MAAM,YAAY,EAAE,IAAI,EAAE,EAAE;AAAA,IAChG,aAAa,EAAE,cAAc,CAAC,GAAG,IAAI,CAAC,MAAW,EAAE,IAAI;AAAA,EACzD,EAAE;AACJ,SAAO;AAAA,IACL,WAAW,GAAG,WAAW;AAAA,IACzB,cAAc,GAAG,cAAc;AAAA,IAC/B,kBAAkB,GAAG,kBAAkB;AAAA,IACvC;AAAA,EACF;AACF;AAEA,eAAsB,kBACpB,UACA,OAII,CAAC,GAC4B;AACjC,QAAM,EAAE,UAAU,CAAC,GAAG,YAAY,KAAQ,QAAQ,IAAI;AACtD,QAAM,KAAK,KAAK,IAAI;AACpB,MAAI;AACF,UAAM,aAAqC;AAAA,MACzC,gBAAgB;AAAA,MAChB,UAAU;AAAA,MACV,GAAG;AAAA,IACL;AACA,QAAI,QAAS,YAAW,QAAQ,IAAI;AAEpC,UAAM,MAAM,MAAM,MAAM,UAAU;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,MAAM,KAAK,UAAU,EAAE,OAAO,oBAAoB,CAAC;AAAA,MACnD,QAAQ,YAAY,QAAQ,SAAS;AAAA,IACvC,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,aAAO,EAAE,UAAU,QAAQ,MAAM,OAAO,QAAQ,IAAI,MAAM,IAAI,YAAY,KAAK,IAAI,IAAI,GAAG;AAAA,IAC5F;AAEA,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,MAAM,QAAQ,QAAQ;AACxB,YAAM,MAAM,KAAK,OAAO,CAAC,GAAG,WAAW;AACvC,aAAO,EAAE,UAAU,QAAQ,MAAM,OAAO,KAAK,YAAY,KAAK,IAAI,IAAI,GAAG;AAAA,IAC3E;AAEA,WAAO,EAAE,UAAU,QAAQ,YAAY,MAAM,QAAQ,IAAI,GAAG,YAAY,KAAK,IAAI,IAAI,GAAG;AAAA,EAC1F,SAAS,GAAQ;AACf,WAAO,EAAE,UAAU,QAAQ,MAAM,OAAO,EAAE,SAAS,YAAY,KAAK,IAAI,IAAI,GAAG;AAAA,EACjF;AACF;AAGO,SAAS,sBAAsB,QAA0B;AAC9D,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,MAAM;AACxB,UAAM,OAAO,GAAG,EAAE,QAAQ,KAAK,EAAE,IAAI;AACrC,WAAO;AAAA,MACL,GAAG,IAAI;AAAA,MACP,GAAG,IAAI;AAAA,MACP,GAAG,IAAI;AAAA,MACP,GAAG,IAAI;AAAA,MACP,GAAG,IAAI;AAAA,IACT;AAAA,EACF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * HTTP-only crawl engine — 10-50x faster than browser for SSR/static sites.
3
+ *
4
+ * Uses native Node.js fetch (HTTP/2), linkedom, Readability, and Turndown.
5
+ * No browser overhead. Falls back to browser engine on JS-heavy sites.
6
+ *
7
+ * Features:
8
+ * - HTTP/2 native (Node 20 built-in fetch)
9
+ * - ETag / Last-Modified response caching
10
+ * - 429 / 503 exponential backoff with jitter
11
+ * - BFS / DFS traversal
12
+ * - URL regex filtering
13
+ * - Robots.txt compliance
14
+ * - Link discovery (href, action, src)
15
+ * - Markdown extraction (Readability + Turndown)
16
+ * - OG / schema.org / meta extraction
17
+ * - Table → JSON extraction
18
+ * - JSON Lines, CSV, raw HTML output formats
19
+ * - Streaming progress via onProgress callback
20
+ */
21
+ export type HttpOutputFormat = 'json' | 'jsonlines' | 'csv' | 'markdown' | 'html';
22
+ export interface HttpScreen {
23
+ url: string;
24
+ title: string | null;
25
+ markdown: string;
26
+ html?: string;
27
+ metadata: {
28
+ description: string | null;
29
+ ogTitle: string | null;
30
+ ogDescription: string | null;
31
+ ogImage: string | null;
32
+ canonical: string | null;
33
+ schemaTypes: string[];
34
+ lang: string | null;
35
+ };
36
+ tables: Array<Record<string, string>[]>;
37
+ links: string[];
38
+ images: Array<{
39
+ src: string;
40
+ alt: string;
41
+ }>;
42
+ statusCode: number;
43
+ fromCache: boolean;
44
+ requiresBrowser?: boolean;
45
+ durationMs: number;
46
+ }
47
+ export interface HttpCrawlOpts {
48
+ appUrl: string;
49
+ maxScreens?: number;
50
+ urlFilter?: string;
51
+ traversalStrategy?: 'BFS' | 'DFS';
52
+ outputFormats?: HttpOutputFormat[];
53
+ cacheDir?: string;
54
+ userAgent?: string;
55
+ headers?: Record<string, string>;
56
+ respectRobotsTxt?: boolean;
57
+ includeRawHtml?: boolean;
58
+ timeoutMs?: number;
59
+ concurrency?: number;
60
+ onProgress?: (update: {
61
+ url: string;
62
+ screensFound: number;
63
+ total: number;
64
+ cached: boolean;
65
+ }) => void;
66
+ }
67
+ export interface HttpCrawlResult {
68
+ screens: HttpScreen[];
69
+ stats: {
70
+ total: number;
71
+ cached: number;
72
+ errors: number;
73
+ rateLimited: number;
74
+ durationMs: number;
75
+ cacheHitRate: number;
76
+ };
77
+ jsonLines?: string;
78
+ csv?: string;
79
+ }
80
+ export declare function httpCrawl(opts: HttpCrawlOpts): Promise<HttpCrawlResult>;