@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,85 @@
1
+ /**
2
+ * Bulk export — export crawled screens to CSV / JSON / Markdown digest.
3
+ *
4
+ * Outputs:
5
+ * - CSV: url, name, wordCount, screenshotPath, markdownPath, createdAt — for spreadsheet analysis
6
+ * - JSON: full screen records array — for API integration
7
+ * - Markdown digest: concatenated markdown of all screens — for LLM context windows
8
+ */
9
+
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+
13
+ export interface ExportScreen {
14
+ id: string;
15
+ name: string;
16
+ url: string;
17
+ projectId: string;
18
+ wordCount?: number;
19
+ screenshotPath?: string | null;
20
+ markdownPath?: string | null;
21
+ markdown?: string | null;
22
+ createdAt?: Date;
23
+ updatedAt?: Date;
24
+ }
25
+
26
+ function escapeCsv(val: string): string {
27
+ if (val.includes(',') || val.includes('"') || val.includes('\n')) {
28
+ return `"${val.replace(/"/g, '""')}"`;
29
+ }
30
+ return val;
31
+ }
32
+
33
+ export async function exportToCsv(screens: ExportScreen[], outputPath: string): Promise<void> {
34
+ const headers = ['id', 'name', 'url', 'projectId', 'wordCount', 'screenshotPath', 'markdownPath', 'createdAt'];
35
+ const rows = screens.map(s => [
36
+ s.id, s.name, s.url, s.projectId,
37
+ String(s.wordCount ?? 0),
38
+ s.screenshotPath ?? '',
39
+ s.markdownPath ?? '',
40
+ s.createdAt?.toISOString() ?? '',
41
+ ].map(escapeCsv).join(','));
42
+
43
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
44
+ await fs.writeFile(outputPath, [headers.join(','), ...rows].join('\n'), 'utf8');
45
+ }
46
+
47
+ export async function exportToJson(screens: ExportScreen[], outputPath: string): Promise<void> {
48
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
49
+ const data = screens.map(s => ({
50
+ id: s.id,
51
+ name: s.name,
52
+ url: s.url,
53
+ projectId: s.projectId,
54
+ wordCount: s.wordCount,
55
+ screenshotPath: s.screenshotPath,
56
+ markdownPath: s.markdownPath,
57
+ createdAt: s.createdAt?.toISOString(),
58
+ updatedAt: s.updatedAt?.toISOString(),
59
+ }));
60
+ await fs.writeFile(outputPath, JSON.stringify(data, null, 2), 'utf8');
61
+ }
62
+
63
+ export async function exportToMarkdownDigest(
64
+ screens: ExportScreen[],
65
+ outputPath: string,
66
+ opts: { readMarkdownFromPath?: boolean; maxScreens?: number } = {},
67
+ ): Promise<void> {
68
+ const { readMarkdownFromPath = true, maxScreens = 200 } = opts;
69
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
70
+
71
+ const handle = await fs.open(outputPath, 'w');
72
+ try {
73
+ await handle.write(`# ZeTa Crawl Digest\n\nExported: ${new Date().toISOString()}\nScreens: ${Math.min(screens.length, maxScreens)}\n\n---\n\n`);
74
+
75
+ for (const screen of screens.slice(0, maxScreens)) {
76
+ let md = screen.markdown ?? '';
77
+ if (!md && readMarkdownFromPath && screen.markdownPath) {
78
+ md = await fs.readFile(screen.markdownPath, 'utf8').catch(() => '');
79
+ }
80
+ await handle.write(`## ${screen.name}\n\n**URL:** ${screen.url}\n\n${md.trim()}\n\n---\n\n`);
81
+ }
82
+ } finally {
83
+ await handle.close();
84
+ }
85
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Vector DB export — convert crawled screens to embedding-ready chunks.
3
+ *
4
+ * Produces a JSONL file where each line is one chunk ready for upsert into
5
+ * Pinecone, Qdrant, Weaviate, or any vector store. The embedding itself is
6
+ * NOT generated here (requires an embedding model at the user's end) — we
7
+ * produce the text chunks + metadata, and the caller embeds them.
8
+ *
9
+ * Format per line (JSONL):
10
+ * {
11
+ * "id": "<screenId>-<chunkIndex>",
12
+ * "text": "<chunk text>",
13
+ * "metadata": {
14
+ * "screenId": "...",
15
+ * "projectId": "...",
16
+ * "tenantId": "...",
17
+ * "url": "...",
18
+ * "screenName": "...",
19
+ * "chunkIndex": 0,
20
+ * "totalChunks": 3,
21
+ * "crawledAt": "2026-...",
22
+ * "wordCount": 142
23
+ * }
24
+ * }
25
+ */
26
+
27
+ import * as fs from 'fs/promises';
28
+ import * as path from 'path';
29
+
30
+ export interface VectorChunk {
31
+ id: string;
32
+ text: string;
33
+ metadata: {
34
+ screenId: string;
35
+ projectId: string;
36
+ tenantId: string;
37
+ url: string;
38
+ screenName: string;
39
+ chunkIndex: number;
40
+ totalChunks: number;
41
+ crawledAt: string;
42
+ wordCount: number;
43
+ };
44
+ }
45
+
46
+ export interface ScreenRecord {
47
+ id: string;
48
+ name: string;
49
+ url: string;
50
+ projectId: string;
51
+ tenantId: string;
52
+ markdown?: string | null;
53
+ markdownPath?: string | null;
54
+ updatedAt?: Date;
55
+ }
56
+
57
+ function splitIntoChunks(text: string, maxChunkWords = 300): string[] {
58
+ const paragraphs = text.split(/\n{2,}/).filter(p => p.trim().length > 0);
59
+ const chunks: string[] = [];
60
+ let current = '';
61
+ let wordCount = 0;
62
+
63
+ for (const para of paragraphs) {
64
+ const words = para.split(/\s+/).length;
65
+ if (wordCount + words > maxChunkWords && current.length > 0) {
66
+ chunks.push(current.trim());
67
+ current = '';
68
+ wordCount = 0;
69
+ }
70
+ current += (current ? '\n\n' : '') + para;
71
+ wordCount += words;
72
+ }
73
+ if (current.trim()) chunks.push(current.trim());
74
+ return chunks.length > 0 ? chunks : [text.slice(0, 2000)];
75
+ }
76
+
77
+ export async function exportScreensToVectorJsonl(
78
+ screens: ScreenRecord[],
79
+ outputPath: string,
80
+ opts: { maxChunkWords?: number; readMarkdownFromPath?: boolean } = {},
81
+ ): Promise<{ totalChunks: number; skipped: number }> {
82
+ const { maxChunkWords = 300, readMarkdownFromPath = true } = opts;
83
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
84
+
85
+ const handle = await fs.open(outputPath, 'w');
86
+ let totalChunks = 0;
87
+ let skipped = 0;
88
+
89
+ try {
90
+ for (const screen of screens) {
91
+ let markdown = screen.markdown ?? '';
92
+
93
+ if (!markdown && readMarkdownFromPath && screen.markdownPath) {
94
+ try {
95
+ markdown = await fs.readFile(screen.markdownPath, 'utf8');
96
+ } catch { /* file missing — skip */ }
97
+ }
98
+
99
+ if (!markdown || markdown.trim().length < 20) {
100
+ skipped++;
101
+ continue;
102
+ }
103
+
104
+ const textChunks = splitIntoChunks(markdown, maxChunkWords);
105
+ for (let i = 0; i < textChunks.length; i++) {
106
+ const chunk: VectorChunk = {
107
+ id: `${screen.id}-${i}`,
108
+ text: textChunks[i],
109
+ metadata: {
110
+ screenId: screen.id,
111
+ projectId: screen.projectId,
112
+ tenantId: screen.tenantId,
113
+ url: screen.url,
114
+ screenName: screen.name,
115
+ chunkIndex: i,
116
+ totalChunks: textChunks.length,
117
+ crawledAt: screen.updatedAt?.toISOString() ?? new Date().toISOString(),
118
+ wordCount: textChunks[i].split(/\s+/).length,
119
+ },
120
+ };
121
+ await handle.write(JSON.stringify(chunk) + '\n');
122
+ totalChunks++;
123
+ }
124
+ }
125
+ } finally {
126
+ await handle.close();
127
+ }
128
+
129
+ return { totalChunks, skipped };
130
+ }
131
+
132
+ /**
133
+ * Pinecone-compatible upsert batch format.
134
+ * Call this after generating embeddings to produce the final upsert payload.
135
+ */
136
+ export function toPineconeUpsert(
137
+ chunks: VectorChunk[],
138
+ embeddings: number[][],
139
+ namespace = 'zeta-crawl',
140
+ ): Array<{ id: string; values: number[]; metadata: VectorChunk['metadata'] }> {
141
+ return chunks.map((chunk, i) => ({
142
+ id: chunk.id,
143
+ values: embeddings[i],
144
+ metadata: chunk.metadata,
145
+ }));
146
+ }
147
+
148
+ /**
149
+ * Qdrant-compatible point format.
150
+ */
151
+ export function toQdrantPoints(
152
+ chunks: VectorChunk[],
153
+ embeddings: number[][],
154
+ ): Array<{ id: string; vector: number[]; payload: VectorChunk['metadata'] & { text: string } }> {
155
+ return chunks.map((chunk, i) => ({
156
+ id: chunk.id,
157
+ vector: embeddings[i],
158
+ payload: { ...chunk.metadata, text: chunk.text },
159
+ }));
160
+ }
@@ -0,0 +1,283 @@
1
+ import { parseHTML } from 'linkedom';
2
+
3
+ export interface ExtractionResult {
4
+ matches: string[];
5
+ count: number;
6
+ method: string;
7
+ }
8
+
9
+ export type TableRow = Record<string, string>;
10
+
11
+ export interface PageMetadata {
12
+ title: string | null;
13
+ description: string | null;
14
+ keywords: string | null;
15
+ ogTitle: string | null;
16
+ ogDescription: string | null;
17
+ ogImage: string | null;
18
+ ogUrl: string | null;
19
+ ogType: string | null;
20
+ twitterCard: string | null;
21
+ twitterTitle: string | null;
22
+ twitterDescription: string | null;
23
+ canonical: string | null;
24
+ robots: string | null;
25
+ jsonLd: object[];
26
+ schemaTypes: string[];
27
+ lang: string | null;
28
+ }
29
+
30
+ export function extractByCssSelector(html: string, selector: string, attr?: string): ExtractionResult {
31
+ try {
32
+ const { document } = parseHTML(html);
33
+ const nodes = Array.from(document.querySelectorAll(selector));
34
+ const matches = nodes.map((n: any) => {
35
+ if (attr) return (n as any).getAttribute(attr) ?? '';
36
+ return (n as any).textContent?.trim() ?? '';
37
+ }).filter(Boolean);
38
+ return { matches, count: matches.length, method: 'css-selector' };
39
+ } catch (e) {
40
+ return { matches: [], count: 0, method: 'css-selector-error' };
41
+ }
42
+ }
43
+
44
+ export function extractByXPath(html: string, xpath: string): ExtractionResult {
45
+ try {
46
+ const { document, XPathResult } = parseHTML(html) as any;
47
+ const result = document.evaluate(xpath, document, null, XPathResult?.ANY_TYPE ?? 0, null);
48
+ const matches: string[] = [];
49
+ let node = result.iterateNext?.();
50
+ while (node) {
51
+ const text = (node as any).textContent?.trim() ?? (node as any).nodeValue?.trim() ?? '';
52
+ if (text) matches.push(text);
53
+ node = result.iterateNext?.();
54
+ }
55
+ return { matches, count: matches.length, method: 'xpath' };
56
+ } catch (e) {
57
+ return { matches: [], count: 0, method: 'xpath-error' };
58
+ }
59
+ }
60
+
61
+ export function extractByRegex(text: string, pattern: string, flags?: string): ExtractionResult {
62
+ try {
63
+ const re = new RegExp(pattern, (flags ?? 'g').includes('g') ? flags : (flags ?? '') + 'g');
64
+ const matches: string[] = [];
65
+ let m: RegExpExecArray | null;
66
+ while ((m = re.exec(text)) !== null) {
67
+ matches.push(m[1] ?? m[0]);
68
+ if (!re.global) break;
69
+ }
70
+ return { matches, count: matches.length, method: 'regex' };
71
+ } catch {
72
+ return { matches: [], count: 0, method: 'regex-error' };
73
+ }
74
+ }
75
+
76
+ export function extractTables(html: string): TableRow[][] {
77
+ try {
78
+ const { document } = parseHTML(html);
79
+ const tables = Array.from(document.querySelectorAll('table'));
80
+ return tables.map((table: any) => {
81
+ const headers: string[] = [];
82
+ const rows: TableRow[] = [];
83
+ const ths = [...table.querySelectorAll('thead th, thead td, tr:first-child th')];
84
+ if (ths.length > 0) {
85
+ ths.forEach((th: any, i: number) => headers.push(th.textContent?.trim() || `col${i + 1}`));
86
+ }
87
+ const trs = [...table.querySelectorAll('tbody tr, tr')];
88
+ for (const tr of trs) {
89
+ const cells = [...(tr as any).querySelectorAll('td, th')];
90
+ if (cells.length === 0) continue;
91
+ if (headers.length === 0) {
92
+ cells.forEach((_: any, i: number) => headers.push(`col${i + 1}`));
93
+ }
94
+ const row: TableRow = {};
95
+ cells.forEach((cell: any, i: number) => {
96
+ const key = headers[i] || `col${i + 1}`;
97
+ row[key] = cell.textContent?.trim() ?? '';
98
+ });
99
+ if (Object.values(row).some(v => v)) rows.push(row);
100
+ }
101
+ return rows;
102
+ }).filter(t => t.length > 0);
103
+ } catch {
104
+ return [];
105
+ }
106
+ }
107
+
108
+ export function extractMetadata(html: string, url: string): PageMetadata {
109
+ const meta: PageMetadata = {
110
+ title: null, description: null, keywords: null,
111
+ ogTitle: null, ogDescription: null, ogImage: null, ogUrl: null, ogType: null,
112
+ twitterCard: null, twitterTitle: null, twitterDescription: null,
113
+ canonical: null, robots: null, jsonLd: [], schemaTypes: [], lang: null,
114
+ };
115
+ try {
116
+ const { document } = parseHTML(html);
117
+ meta.title = (document as any).title ?? (document as any).querySelector('title')?.textContent?.trim() ?? null;
118
+ meta.lang = (document as any).documentElement?.getAttribute('lang') ?? null;
119
+
120
+ const metas = [...(document as any).querySelectorAll('meta')];
121
+ for (const m of metas) {
122
+ const name = (m as any).getAttribute('name')?.toLowerCase() ?? '';
123
+ const prop = (m as any).getAttribute('property')?.toLowerCase() ?? '';
124
+ const content = (m as any).getAttribute('content') ?? '';
125
+ if (name === 'description') meta.description = content;
126
+ if (name === 'keywords') meta.keywords = content;
127
+ if (name === 'robots') meta.robots = content;
128
+ if (name === 'twitter:card') meta.twitterCard = content;
129
+ if (name === 'twitter:title') meta.twitterTitle = content;
130
+ if (name === 'twitter:description') meta.twitterDescription = content;
131
+ if (prop === 'og:title') meta.ogTitle = content;
132
+ if (prop === 'og:description') meta.ogDescription = content;
133
+ if (prop === 'og:image') meta.ogImage = content;
134
+ if (prop === 'og:url') meta.ogUrl = content;
135
+ if (prop === 'og:type') meta.ogType = content;
136
+ }
137
+
138
+ const canonical = (document as any).querySelector('link[rel="canonical"]');
139
+ if (canonical) meta.canonical = canonical.getAttribute('href') ?? null;
140
+
141
+ const jsonLdScripts = [...(document as any).querySelectorAll('script[type="application/ld+json"]')];
142
+ for (const s of jsonLdScripts) {
143
+ try {
144
+ const parsed = JSON.parse((s as any).textContent ?? '{}');
145
+ meta.jsonLd.push(parsed);
146
+ const types = Array.isArray(parsed) ? parsed.map((p: any) => p['@type']).flat() : [parsed['@type']];
147
+ meta.schemaTypes.push(...types.filter(Boolean));
148
+ } catch { /* skip invalid JSON-LD */ }
149
+ }
150
+ } catch { /* non-fatal */ }
151
+ return meta;
152
+ }
153
+
154
+ export interface ProductData {
155
+ name: string | null;
156
+ description: string | null;
157
+ brand: string | null;
158
+ sku: string | null;
159
+ price: number | null;
160
+ priceCurrency: string | null;
161
+ availability: string | null;
162
+ rating: number | null;
163
+ reviewCount: number | null;
164
+ image: string | null;
165
+ url: string | null;
166
+ }
167
+
168
+ export function extractStructuredProduct(jsonLd: object[]): ProductData | null {
169
+ for (const item of jsonLd) {
170
+ try {
171
+ const type = (item as any)['@type'];
172
+ const isProduct = Array.isArray(type)
173
+ ? type.some((t: any) => typeof t === 'string' && t.toLowerCase() === 'product')
174
+ : typeof type === 'string' && type.toLowerCase() === 'product';
175
+ if (!isProduct) continue;
176
+
177
+ const i = item as any;
178
+ const offers = Array.isArray(i.offers) ? i.offers[0] : i.offers;
179
+
180
+ const rawPrice = offers?.price;
181
+ const price = rawPrice != null ? parseFloat(rawPrice) : NaN;
182
+
183
+ const rawRating = i.aggregateRating?.ratingValue;
184
+ const rating = rawRating != null ? parseFloat(rawRating) : NaN;
185
+
186
+ const rawReviewCount = i.aggregateRating?.reviewCount;
187
+ const reviewCount = rawReviewCount != null ? parseInt(rawReviewCount, 10) : NaN;
188
+
189
+ const rawAvailability: string | undefined = offers?.availability;
190
+ const availability = rawAvailability
191
+ ? rawAvailability.replace(/^https?:\/\/schema\.org\//, '')
192
+ : null;
193
+
194
+ const rawImage = i.image;
195
+ const image = Array.isArray(rawImage) ? (rawImage[0] ?? null) : (rawImage ?? null);
196
+
197
+ const brand = i.brand?.name ?? i.brand ?? null;
198
+
199
+ return {
200
+ name: i.name ?? null,
201
+ description: i.description ?? null,
202
+ brand: typeof brand === 'string' ? brand : null,
203
+ sku: i.sku ?? null,
204
+ price: isNaN(price) ? null : price,
205
+ priceCurrency: offers?.priceCurrency ?? null,
206
+ availability,
207
+ rating: isNaN(rating) ? null : rating,
208
+ reviewCount: isNaN(reviewCount) ? null : reviewCount,
209
+ image,
210
+ url: i.url ?? null,
211
+ };
212
+ } catch { /* skip malformed item */ }
213
+ }
214
+ return null;
215
+ }
216
+
217
+ export function extractImages(html: string, baseUrl: string): Array<{ src: string; alt: string; width?: number; height?: number }> {
218
+ try {
219
+ const { document } = parseHTML(html);
220
+ const base = new URL(baseUrl);
221
+ return Array.from((document as any).querySelectorAll('img'))
222
+ .map((img: any) => {
223
+ const src = img.getAttribute('src') ?? img.getAttribute('data-src') ?? '';
224
+ if (!src) return null;
225
+ try {
226
+ return {
227
+ src: new URL(src, base).href,
228
+ alt: (img.getAttribute('alt') ?? '') as string,
229
+ width: img.getAttribute('width') ? parseInt(img.getAttribute('width')) : undefined,
230
+ height: img.getAttribute('height') ? parseInt(img.getAttribute('height')) : undefined,
231
+ } as { src: string; alt: string; width?: number; height?: number };
232
+ } catch { return null; }
233
+ })
234
+ .filter((x): x is { src: string; alt: string; width?: number; height?: number } => x !== null);
235
+ } catch {
236
+ return [];
237
+ }
238
+ }
239
+
240
+ export function extractMediaFiles(html: string, baseUrl: string): Array<{ url: string; type: 'image' | 'video' | 'audio' | 'document'; tag: string }> {
241
+ const IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg|ico|avif)(\?.*)?$/i;
242
+ const VIDEO_EXT = /\.(mp4|webm|ogv|mov|avi)(\?.*)?$/i;
243
+ const AUDIO_EXT = /\.(mp3|ogg|wav|flac|aac)(\?.*)?$/i;
244
+ const DOC_EXT = /\.(pdf|docx?|xlsx?|pptx?|csv|zip)(\?.*)?$/i;
245
+
246
+ function classify(url: string, tag: string): 'image' | 'video' | 'audio' | 'document' {
247
+ if (tag === 'img' || IMAGE_EXT.test(url)) return 'image';
248
+ if (tag === 'video' || VIDEO_EXT.test(url)) return 'video';
249
+ if (tag === 'audio' || AUDIO_EXT.test(url)) return 'audio';
250
+ if (DOC_EXT.test(url)) return 'document';
251
+ return 'document';
252
+ }
253
+
254
+ try {
255
+ const { document } = parseHTML(html);
256
+ const base = new URL(baseUrl);
257
+ const results: Array<{ url: string; type: 'image' | 'video' | 'audio' | 'document'; tag: string }> = [];
258
+ const seen = new Set<string>();
259
+
260
+ const selectors: [string, string] = ['img[src],video[src],audio[src],source[src]', 'src'];
261
+ const attrPairs: Array<[string, string]> = [
262
+ ['img', 'src'], ['video', 'src'], ['audio', 'src'], ['source', 'src'],
263
+ ['a', 'href'],
264
+ ];
265
+
266
+ for (const [sel, attr] of attrPairs) {
267
+ for (const el of [...(document as any).querySelectorAll(sel)]) {
268
+ const raw = (el as any).getAttribute(attr) ?? '';
269
+ if (!raw) continue;
270
+ try {
271
+ const resolved = new URL(raw, base).href;
272
+ if (seen.has(resolved)) continue;
273
+ if (sel === 'a' && !DOC_EXT.test(resolved) && !IMAGE_EXT.test(resolved) && !VIDEO_EXT.test(resolved) && !AUDIO_EXT.test(resolved)) continue;
274
+ seen.add(resolved);
275
+ results.push({ url: resolved, type: classify(resolved, sel), tag: sel });
276
+ } catch { /* skip */ }
277
+ }
278
+ }
279
+ return results;
280
+ } catch {
281
+ return [];
282
+ }
283
+ }
@@ -0,0 +1,111 @@
1
+ import crypto from 'node:crypto';
2
+ import type { RawElement } from '../screen-reader.js';
3
+
4
+ export interface CrawlEvidenceFact {
5
+ type: string;
6
+ payload: unknown;
7
+ }
8
+
9
+ export interface CrawlArtifactEnvelope {
10
+ tenantId: string;
11
+ projectId: string;
12
+ jobId?: string;
13
+ url: string;
14
+ canonicalUrl: string;
15
+ screenId?: string;
16
+ status: 'ok' | 'blocked' | 'auth_expired' | 'error' | 'skipped';
17
+ outputs: {
18
+ screenshot?: string;
19
+ html?: string;
20
+ markdown?: string;
21
+ fitMarkdown?: string;
22
+ accessibilityTree?: string;
23
+ structuredJson?: unknown;
24
+ networkTrace?: string;
25
+ consoleLog?: string;
26
+ };
27
+ fingerprints: {
28
+ textHash?: string;
29
+ domHash?: string;
30
+ accessibilityHash?: string;
31
+ urlCanonicalHash: string;
32
+ };
33
+ evidence: CrawlEvidenceFact[];
34
+ }
35
+
36
+ export function sha256(input: string): string {
37
+ return crypto.createHash('sha256').update(input).digest('hex');
38
+ }
39
+
40
+ export function buildFitMarkdown(markdown: string, keywords: string[] = []): string {
41
+ const lines = markdown
42
+ .split('\n')
43
+ .map((line) => line.trimEnd())
44
+ .filter((line) => line.trim().length > 0);
45
+ const lowerKeywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
46
+ const filtered = lines.filter((line) => {
47
+ const plain = line.replace(/[#*_`>\-[\]()]/g, '').trim();
48
+ if (plain.length < 3) return false;
49
+ if (/^(cookie|privacy|terms|copyright|all rights reserved|subscribe|newsletter)$/i.test(plain)) return false;
50
+ if (lowerKeywords.length === 0) return true;
51
+ const lower = plain.toLowerCase();
52
+ return lowerKeywords.some((kw) => lower.includes(kw)) || /^#{1,4}\s/.test(line);
53
+ });
54
+ const candidate = filtered.join('\n');
55
+ return candidate.length >= 200 ? candidate : markdown;
56
+ }
57
+
58
+ export function summarizeStructuredElements(elements: RawElement[]) {
59
+ const forms = elements.filter((e) => ['email', 'password', 'text', 'tel', 'date', 'number', 'select', 'textarea'].includes(e.role));
60
+ const actions = elements.filter((e) => e.role === 'button' || e.role === 'submit' || e.role === 'link' || e.role === 'a');
61
+ return {
62
+ elementCount: elements.length,
63
+ formFieldCount: forms.length,
64
+ actionCount: actions.length,
65
+ fields: forms.slice(0, 50).map((e) => ({ label: e.label, role: e.role, expectedData: e.expectedData })),
66
+ actions: actions.slice(0, 50).map((e) => ({ label: e.label, role: e.role })),
67
+ };
68
+ }
69
+
70
+ export function buildCrawlArtifactEnvelope(input: {
71
+ tenantId: string;
72
+ projectId: string;
73
+ jobId?: string;
74
+ url: string;
75
+ canonicalUrl: string;
76
+ screenId?: string;
77
+ screenshotPath?: string;
78
+ domPath?: string;
79
+ domContent?: string;
80
+ markdown?: string;
81
+ fitMarkdown?: string;
82
+ accessibilityElements?: RawElement[];
83
+ structuredJson?: unknown;
84
+ evidence?: CrawlEvidenceFact[];
85
+ }): CrawlArtifactEnvelope {
86
+ const accessibilityTree = input.accessibilityElements ? JSON.stringify(input.accessibilityElements) : undefined;
87
+ return {
88
+ tenantId: input.tenantId,
89
+ projectId: input.projectId,
90
+ jobId: input.jobId,
91
+ url: input.url,
92
+ canonicalUrl: input.canonicalUrl,
93
+ screenId: input.screenId,
94
+ status: 'ok',
95
+ outputs: {
96
+ screenshot: input.screenshotPath,
97
+ html: input.domPath,
98
+ markdown: input.markdown,
99
+ fitMarkdown: input.fitMarkdown,
100
+ accessibilityTree,
101
+ structuredJson: input.structuredJson,
102
+ },
103
+ fingerprints: {
104
+ textHash: input.markdown ? sha256(input.markdown) : undefined,
105
+ domHash: input.domContent ? sha256(input.domContent) : undefined,
106
+ accessibilityHash: accessibilityTree ? sha256(accessibilityTree) : undefined,
107
+ urlCanonicalHash: sha256(input.canonicalUrl),
108
+ },
109
+ evidence: input.evidence ?? [],
110
+ };
111
+ }
@@ -0,0 +1,49 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { askTrackedWithImages, askTracked } from '@detiq/agents';
3
+ import type { LLMConfig } from '@detiq/agents';
4
+ import { AppBrain } from '@detiq/app-brain';
5
+
6
+ export interface FailureContext {
7
+ tenantId?: string;
8
+ error: string;
9
+ stepDescription?: string;
10
+ consoleLogs?: Array<{ type: string; text: string }>;
11
+ networkErrors?: Array<{ url: string; status: number; method: string }>;
12
+ screenshotPath?: string;
13
+ }
14
+
15
+ export async function analyzeFailure(ctx: FailureContext): Promise<string | null> {
16
+ try {
17
+ let llmCfg: LLMConfig | undefined;
18
+ if (ctx.tenantId) {
19
+ llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, 'testRunner').catch(() => undefined) as LLMConfig | undefined ?? undefined;
20
+ }
21
+ if (!llmCfg) return null;
22
+
23
+ const consoleSection = ctx.consoleLogs?.length
24
+ ? `\n\nBrowser console (last ${ctx.consoleLogs.length} entries):\n${ctx.consoleLogs.map(l => `[${l.type}] ${l.text}`).join('\n')}`
25
+ : '';
26
+
27
+ const networkSection = ctx.networkErrors?.length
28
+ ? `\n\nFailed network requests:\n${ctx.networkErrors.map(n => `${n.method} ${n.url} → ${n.status}`).join('\n')}`
29
+ : '';
30
+
31
+ const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific — name the failing element, API endpoint, or error type. Do not suggest fixes.
32
+
33
+ Test step: ${ctx.stepDescription ?? 'unknown'}
34
+ Error: ${ctx.error}${consoleSection}${networkSection}`;
35
+
36
+ if (ctx.screenshotPath) {
37
+ try {
38
+ const img = readFileSync(ctx.screenshotPath);
39
+ const result = await askTrackedWithImages('', userText, [{ base64: img.toString('base64'), mediaType: 'image/png' }], llmCfg);
40
+ return result.text || null;
41
+ } catch { /* fall through to text-only */ }
42
+ }
43
+
44
+ const result = await askTracked('', userText, llmCfg, 256);
45
+ return result.text || null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }