@iflow-mcp/jakeliume-webpeel 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (547) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +313 -0
  3. package/dist/cache.d.ts +30 -0
  4. package/dist/cache.js +139 -0
  5. package/dist/cli/commands/auth.d.ts +5 -0
  6. package/dist/cli/commands/auth.js +411 -0
  7. package/dist/cli/commands/doctor.d.ts +37 -0
  8. package/dist/cli/commands/doctor.js +371 -0
  9. package/dist/cli/commands/fetch.d.ts +6 -0
  10. package/dist/cli/commands/fetch.js +1345 -0
  11. package/dist/cli/commands/guide.d.ts +2 -0
  12. package/dist/cli/commands/guide.js +183 -0
  13. package/dist/cli/commands/interact.d.ts +5 -0
  14. package/dist/cli/commands/interact.js +840 -0
  15. package/dist/cli/commands/jobs.d.ts +5 -0
  16. package/dist/cli/commands/jobs.js +997 -0
  17. package/dist/cli/commands/monitor.d.ts +12 -0
  18. package/dist/cli/commands/monitor.js +197 -0
  19. package/dist/cli/commands/observe.d.ts +12 -0
  20. package/dist/cli/commands/observe.js +158 -0
  21. package/dist/cli/commands/screenshot.d.ts +5 -0
  22. package/dist/cli/commands/screenshot.js +282 -0
  23. package/dist/cli/commands/search.d.ts +5 -0
  24. package/dist/cli/commands/search.js +1021 -0
  25. package/dist/cli/commands/setup.d.ts +13 -0
  26. package/dist/cli/commands/setup.js +244 -0
  27. package/dist/cli/commands/skill.d.ts +15 -0
  28. package/dist/cli/commands/skill.js +195 -0
  29. package/dist/cli/utils.d.ts +84 -0
  30. package/dist/cli/utils.js +806 -0
  31. package/dist/cli-auth.d.ts +75 -0
  32. package/dist/cli-auth.js +369 -0
  33. package/dist/cli.d.ts +17 -0
  34. package/dist/cli.js +99 -0
  35. package/dist/core/actions.d.ts +69 -0
  36. package/dist/core/actions.js +495 -0
  37. package/dist/core/agent.d.ts +98 -0
  38. package/dist/core/agent.js +558 -0
  39. package/dist/core/answer.d.ts +42 -0
  40. package/dist/core/answer.js +395 -0
  41. package/dist/core/application-tracker.d.ts +84 -0
  42. package/dist/core/application-tracker.js +184 -0
  43. package/dist/core/apply.d.ts +162 -0
  44. package/dist/core/apply.js +816 -0
  45. package/dist/core/auth-detection.d.ts +35 -0
  46. package/dist/core/auth-detection.js +358 -0
  47. package/dist/core/auto-extract.d.ts +82 -0
  48. package/dist/core/auto-extract.js +604 -0
  49. package/dist/core/auto-interact.d.ts +23 -0
  50. package/dist/core/auto-interact.js +246 -0
  51. package/dist/core/bm25-filter.d.ts +66 -0
  52. package/dist/core/bm25-filter.js +288 -0
  53. package/dist/core/branding.d.ts +54 -0
  54. package/dist/core/branding.js +234 -0
  55. package/dist/core/browser-fetch.d.ts +323 -0
  56. package/dist/core/browser-fetch.js +1600 -0
  57. package/dist/core/browser-pool.d.ts +91 -0
  58. package/dist/core/browser-pool.js +550 -0
  59. package/dist/core/budget.d.ts +42 -0
  60. package/dist/core/budget.js +324 -0
  61. package/dist/core/business-intel.d.ts +47 -0
  62. package/dist/core/business-intel.js +279 -0
  63. package/dist/core/cache.d.ts +13 -0
  64. package/dist/core/cache.js +121 -0
  65. package/dist/core/cf-worker-proxy.d.ts +32 -0
  66. package/dist/core/cf-worker-proxy.js +87 -0
  67. package/dist/core/challenge-detection.d.ts +26 -0
  68. package/dist/core/challenge-detection.js +468 -0
  69. package/dist/core/change-tracking.d.ts +75 -0
  70. package/dist/core/change-tracking.js +276 -0
  71. package/dist/core/chunker.d.ts +46 -0
  72. package/dist/core/chunker.js +249 -0
  73. package/dist/core/chunking.d.ts +42 -0
  74. package/dist/core/chunking.js +181 -0
  75. package/dist/core/circuit-breaker.d.ts +44 -0
  76. package/dist/core/circuit-breaker.js +85 -0
  77. package/dist/core/content-pruner.d.ts +47 -0
  78. package/dist/core/content-pruner.js +425 -0
  79. package/dist/core/cookie-cache.d.ts +60 -0
  80. package/dist/core/cookie-cache.js +163 -0
  81. package/dist/core/crawl-checkpoint.d.ts +54 -0
  82. package/dist/core/crawl-checkpoint.js +104 -0
  83. package/dist/core/crawler.d.ts +84 -0
  84. package/dist/core/crawler.js +349 -0
  85. package/dist/core/cross-verify.d.ts +27 -0
  86. package/dist/core/cross-verify.js +93 -0
  87. package/dist/core/deep-fetch.d.ts +74 -0
  88. package/dist/core/deep-fetch.js +405 -0
  89. package/dist/core/deep-research.d.ts +141 -0
  90. package/dist/core/deep-research.js +972 -0
  91. package/dist/core/design-analysis.d.ts +70 -0
  92. package/dist/core/design-analysis.js +490 -0
  93. package/dist/core/design-compare.d.ts +38 -0
  94. package/dist/core/design-compare.js +264 -0
  95. package/dist/core/diff.d.ts +61 -0
  96. package/dist/core/diff.js +289 -0
  97. package/dist/core/dns-cache.d.ts +20 -0
  98. package/dist/core/dns-cache.js +198 -0
  99. package/dist/core/documents.d.ts +23 -0
  100. package/dist/core/documents.js +123 -0
  101. package/dist/core/domain-memory.d.ts +66 -0
  102. package/dist/core/domain-memory.js +163 -0
  103. package/dist/core/domain-verify.d.ts +40 -0
  104. package/dist/core/domain-verify.js +379 -0
  105. package/dist/core/engine-ranker.d.ts +112 -0
  106. package/dist/core/engine-ranker.js +395 -0
  107. package/dist/core/extract-inline.d.ts +38 -0
  108. package/dist/core/extract-inline.js +215 -0
  109. package/dist/core/extract-listings.d.ts +38 -0
  110. package/dist/core/extract-listings.js +461 -0
  111. package/dist/core/extract.d.ts +9 -0
  112. package/dist/core/extract.js +139 -0
  113. package/dist/core/fetch-cache.d.ts +57 -0
  114. package/dist/core/fetch-cache.js +95 -0
  115. package/dist/core/fetcher.d.ts +13 -0
  116. package/dist/core/fetcher.js +12 -0
  117. package/dist/core/google-cache.d.ts +29 -0
  118. package/dist/core/google-cache.js +180 -0
  119. package/dist/core/google-serp-parser.d.ts +82 -0
  120. package/dist/core/google-serp-parser.js +287 -0
  121. package/dist/core/hotel-search.d.ts +122 -0
  122. package/dist/core/hotel-search.js +382 -0
  123. package/dist/core/http-fetch.d.ts +72 -0
  124. package/dist/core/http-fetch.js +820 -0
  125. package/dist/core/human.d.ts +175 -0
  126. package/dist/core/human.js +680 -0
  127. package/dist/core/image-caption.d.ts +44 -0
  128. package/dist/core/image-caption.js +271 -0
  129. package/dist/core/jobs.d.ts +75 -0
  130. package/dist/core/jobs.js +634 -0
  131. package/dist/core/json-ld.d.ts +15 -0
  132. package/dist/core/json-ld.js +617 -0
  133. package/dist/core/language-detect.d.ts +18 -0
  134. package/dist/core/language-detect.js +135 -0
  135. package/dist/core/links.d.ts +10 -0
  136. package/dist/core/links.js +44 -0
  137. package/dist/core/llm-extract.d.ts +71 -0
  138. package/dist/core/llm-extract.js +507 -0
  139. package/dist/core/llm-provider.d.ts +100 -0
  140. package/dist/core/llm-provider.js +702 -0
  141. package/dist/core/local-search.d.ts +60 -0
  142. package/dist/core/local-search.js +308 -0
  143. package/dist/core/logger.d.ts +28 -0
  144. package/dist/core/logger.js +104 -0
  145. package/dist/core/map.d.ts +33 -0
  146. package/dist/core/map.js +127 -0
  147. package/dist/core/markdown.d.ts +92 -0
  148. package/dist/core/markdown.js +809 -0
  149. package/dist/core/metadata.d.ts +34 -0
  150. package/dist/core/metadata.js +422 -0
  151. package/dist/core/observe.d.ts +113 -0
  152. package/dist/core/observe.js +395 -0
  153. package/dist/core/ocr.d.ts +12 -0
  154. package/dist/core/ocr.js +33 -0
  155. package/dist/core/paginate.d.ts +31 -0
  156. package/dist/core/paginate.js +106 -0
  157. package/dist/core/pdf.d.ts +8 -0
  158. package/dist/core/pdf.js +25 -0
  159. package/dist/core/peel-tls.d.ts +25 -0
  160. package/dist/core/peel-tls.js +220 -0
  161. package/dist/core/pipeline.d.ts +132 -0
  162. package/dist/core/pipeline.js +1666 -0
  163. package/dist/core/profiles.d.ts +61 -0
  164. package/dist/core/profiles.js +350 -0
  165. package/dist/core/prompt-guard.d.ts +30 -0
  166. package/dist/core/prompt-guard.js +119 -0
  167. package/dist/core/proxy-config.d.ts +90 -0
  168. package/dist/core/proxy-config.js +172 -0
  169. package/dist/core/quick-answer.d.ts +53 -0
  170. package/dist/core/quick-answer.js +833 -0
  171. package/dist/core/rate-governor.d.ts +80 -0
  172. package/dist/core/rate-governor.js +238 -0
  173. package/dist/core/readability.d.ts +57 -0
  174. package/dist/core/readability.js +533 -0
  175. package/dist/core/research.d.ts +66 -0
  176. package/dist/core/research.js +270 -0
  177. package/dist/core/retry.d.ts +60 -0
  178. package/dist/core/retry.js +119 -0
  179. package/dist/core/safe-browsing.d.ts +30 -0
  180. package/dist/core/safe-browsing.js +206 -0
  181. package/dist/core/schema-extraction.d.ts +66 -0
  182. package/dist/core/schema-extraction.js +352 -0
  183. package/dist/core/schema-postprocess.d.ts +32 -0
  184. package/dist/core/schema-postprocess.js +469 -0
  185. package/dist/core/schema-templates.d.ts +19 -0
  186. package/dist/core/schema-templates.js +143 -0
  187. package/dist/core/screenshot.d.ts +224 -0
  188. package/dist/core/screenshot.js +207 -0
  189. package/dist/core/search-engines.d.ts +25 -0
  190. package/dist/core/search-engines.js +182 -0
  191. package/dist/core/search-provider.d.ts +243 -0
  192. package/dist/core/search-provider.js +1629 -0
  193. package/dist/core/searxng-provider.d.ts +35 -0
  194. package/dist/core/searxng-provider.js +105 -0
  195. package/dist/core/selective-evidence.d.ts +151 -0
  196. package/dist/core/selective-evidence.js +389 -0
  197. package/dist/core/site-search.d.ts +44 -0
  198. package/dist/core/site-search.js +252 -0
  199. package/dist/core/sitemap.d.ts +23 -0
  200. package/dist/core/sitemap.js +105 -0
  201. package/dist/core/source-credibility.d.ts +29 -0
  202. package/dist/core/source-credibility.js +584 -0
  203. package/dist/core/source-scoring.d.ts +166 -0
  204. package/dist/core/source-scoring.js +396 -0
  205. package/dist/core/stemmer.d.ts +38 -0
  206. package/dist/core/stemmer.js +509 -0
  207. package/dist/core/strategies.d.ts +104 -0
  208. package/dist/core/strategies.js +1044 -0
  209. package/dist/core/strategy-hooks.d.ts +145 -0
  210. package/dist/core/strategy-hooks.js +74 -0
  211. package/dist/core/structured-extract.d.ts +43 -0
  212. package/dist/core/structured-extract.js +550 -0
  213. package/dist/core/summarize.d.ts +17 -0
  214. package/dist/core/summarize.js +78 -0
  215. package/dist/core/synonyms.d.ts +42 -0
  216. package/dist/core/synonyms.js +184 -0
  217. package/dist/core/system-monitor.d.ts +61 -0
  218. package/dist/core/system-monitor.js +133 -0
  219. package/dist/core/table-format.d.ts +30 -0
  220. package/dist/core/table-format.js +146 -0
  221. package/dist/core/threat-feeds.d.ts +23 -0
  222. package/dist/core/threat-feeds.js +104 -0
  223. package/dist/core/timing.d.ts +21 -0
  224. package/dist/core/timing.js +33 -0
  225. package/dist/core/transcript-export.d.ts +47 -0
  226. package/dist/core/transcript-export.js +107 -0
  227. package/dist/core/user-agents.d.ts +82 -0
  228. package/dist/core/user-agents.js +239 -0
  229. package/dist/core/vertical-search.d.ts +54 -0
  230. package/dist/core/vertical-search.js +158 -0
  231. package/dist/core/watch-manager.d.ts +175 -0
  232. package/dist/core/watch-manager.js +416 -0
  233. package/dist/core/watch.d.ts +101 -0
  234. package/dist/core/watch.js +389 -0
  235. package/dist/core/youtube.d.ts +130 -0
  236. package/dist/core/youtube.js +1175 -0
  237. package/dist/ee/challenge-re-export.d.ts +1 -0
  238. package/dist/ee/challenge-re-export.js +1 -0
  239. package/dist/ee/challenge-solver.d.ts +72 -0
  240. package/dist/ee/challenge-solver.js +720 -0
  241. package/dist/ee/domain-extractors.d.ts +8 -0
  242. package/dist/ee/domain-extractors.js +8 -0
  243. package/dist/ee/domain-intel.d.ts +16 -0
  244. package/dist/ee/domain-intel.js +133 -0
  245. package/dist/ee/extractors/allrecipes.d.ts +2 -0
  246. package/dist/ee/extractors/allrecipes.js +120 -0
  247. package/dist/ee/extractors/amazon.d.ts +2 -0
  248. package/dist/ee/extractors/amazon.js +78 -0
  249. package/dist/ee/extractors/arxiv.d.ts +2 -0
  250. package/dist/ee/extractors/arxiv.js +137 -0
  251. package/dist/ee/extractors/bestbuy.d.ts +2 -0
  252. package/dist/ee/extractors/bestbuy.js +78 -0
  253. package/dist/ee/extractors/carscom.d.ts +2 -0
  254. package/dist/ee/extractors/carscom.js +121 -0
  255. package/dist/ee/extractors/coingecko.d.ts +2 -0
  256. package/dist/ee/extractors/coingecko.js +134 -0
  257. package/dist/ee/extractors/craigslist.d.ts +2 -0
  258. package/dist/ee/extractors/craigslist.js +92 -0
  259. package/dist/ee/extractors/devto.d.ts +2 -0
  260. package/dist/ee/extractors/devto.js +135 -0
  261. package/dist/ee/extractors/ebay.d.ts +2 -0
  262. package/dist/ee/extractors/ebay.js +90 -0
  263. package/dist/ee/extractors/espn.d.ts +2 -0
  264. package/dist/ee/extractors/espn.js +260 -0
  265. package/dist/ee/extractors/etsy.d.ts +2 -0
  266. package/dist/ee/extractors/etsy.js +52 -0
  267. package/dist/ee/extractors/facebook.d.ts +2 -0
  268. package/dist/ee/extractors/facebook.js +46 -0
  269. package/dist/ee/extractors/github.d.ts +2 -0
  270. package/dist/ee/extractors/github.js +196 -0
  271. package/dist/ee/extractors/google-flights.d.ts +2 -0
  272. package/dist/ee/extractors/google-flights.js +176 -0
  273. package/dist/ee/extractors/hackernews.d.ts +2 -0
  274. package/dist/ee/extractors/hackernews.js +147 -0
  275. package/dist/ee/extractors/imdb.d.ts +2 -0
  276. package/dist/ee/extractors/imdb.js +172 -0
  277. package/dist/ee/extractors/index.d.ts +26 -0
  278. package/dist/ee/extractors/index.js +247 -0
  279. package/dist/ee/extractors/instagram.d.ts +2 -0
  280. package/dist/ee/extractors/instagram.js +102 -0
  281. package/dist/ee/extractors/kalshi.d.ts +2 -0
  282. package/dist/ee/extractors/kalshi.js +121 -0
  283. package/dist/ee/extractors/kayak-cars.d.ts +2 -0
  284. package/dist/ee/extractors/kayak-cars.js +270 -0
  285. package/dist/ee/extractors/linkedin.d.ts +2 -0
  286. package/dist/ee/extractors/linkedin.js +113 -0
  287. package/dist/ee/extractors/medium.d.ts +2 -0
  288. package/dist/ee/extractors/medium.js +130 -0
  289. package/dist/ee/extractors/news.d.ts +4 -0
  290. package/dist/ee/extractors/news.js +173 -0
  291. package/dist/ee/extractors/npm.d.ts +2 -0
  292. package/dist/ee/extractors/npm.js +86 -0
  293. package/dist/ee/extractors/pdf.d.ts +2 -0
  294. package/dist/ee/extractors/pdf.js +108 -0
  295. package/dist/ee/extractors/pinterest.d.ts +2 -0
  296. package/dist/ee/extractors/pinterest.js +34 -0
  297. package/dist/ee/extractors/polymarket.d.ts +2 -0
  298. package/dist/ee/extractors/polymarket.js +358 -0
  299. package/dist/ee/extractors/producthunt.d.ts +2 -0
  300. package/dist/ee/extractors/producthunt.js +88 -0
  301. package/dist/ee/extractors/pubmed.d.ts +2 -0
  302. package/dist/ee/extractors/pubmed.js +162 -0
  303. package/dist/ee/extractors/pypi.d.ts +2 -0
  304. package/dist/ee/extractors/pypi.js +80 -0
  305. package/dist/ee/extractors/reddit.d.ts +2 -0
  306. package/dist/ee/extractors/reddit.js +438 -0
  307. package/dist/ee/extractors/redfin.d.ts +2 -0
  308. package/dist/ee/extractors/redfin.js +156 -0
  309. package/dist/ee/extractors/semanticscholar.d.ts +2 -0
  310. package/dist/ee/extractors/semanticscholar.js +131 -0
  311. package/dist/ee/extractors/shared.d.ts +12 -0
  312. package/dist/ee/extractors/shared.js +76 -0
  313. package/dist/ee/extractors/soundcloud.d.ts +2 -0
  314. package/dist/ee/extractors/soundcloud.js +34 -0
  315. package/dist/ee/extractors/sportsbetting.d.ts +2 -0
  316. package/dist/ee/extractors/sportsbetting.js +37 -0
  317. package/dist/ee/extractors/spotify.d.ts +2 -0
  318. package/dist/ee/extractors/spotify.js +34 -0
  319. package/dist/ee/extractors/stackoverflow.d.ts +2 -0
  320. package/dist/ee/extractors/stackoverflow.js +61 -0
  321. package/dist/ee/extractors/substack.d.ts +2 -0
  322. package/dist/ee/extractors/substack.js +115 -0
  323. package/dist/ee/extractors/substackroot.d.ts +2 -0
  324. package/dist/ee/extractors/substackroot.js +46 -0
  325. package/dist/ee/extractors/tiktok.d.ts +2 -0
  326. package/dist/ee/extractors/tiktok.js +29 -0
  327. package/dist/ee/extractors/tradingview.d.ts +2 -0
  328. package/dist/ee/extractors/tradingview.js +182 -0
  329. package/dist/ee/extractors/twitch.d.ts +2 -0
  330. package/dist/ee/extractors/twitch.js +36 -0
  331. package/dist/ee/extractors/twitter.d.ts +2 -0
  332. package/dist/ee/extractors/twitter.js +327 -0
  333. package/dist/ee/extractors/types.d.ts +14 -0
  334. package/dist/ee/extractors/types.js +1 -0
  335. package/dist/ee/extractors/walmart.d.ts +2 -0
  336. package/dist/ee/extractors/walmart.js +50 -0
  337. package/dist/ee/extractors/weather.d.ts +2 -0
  338. package/dist/ee/extractors/weather.js +133 -0
  339. package/dist/ee/extractors/wikipedia.d.ts +4 -0
  340. package/dist/ee/extractors/wikipedia.js +235 -0
  341. package/dist/ee/extractors/yelp.d.ts +2 -0
  342. package/dist/ee/extractors/yelp.js +216 -0
  343. package/dist/ee/extractors/youtube.d.ts +2 -0
  344. package/dist/ee/extractors/youtube.js +189 -0
  345. package/dist/ee/extractors/zillow.d.ts +54 -0
  346. package/dist/ee/extractors/zillow.js +247 -0
  347. package/dist/ee/extractors-re-export.d.ts +1 -0
  348. package/dist/ee/extractors-re-export.js +1 -0
  349. package/dist/ee/premium-hooks.d.ts +20 -0
  350. package/dist/ee/premium-hooks.js +50 -0
  351. package/dist/ee/spa-detection.d.ts +2 -0
  352. package/dist/ee/spa-detection.js +2 -0
  353. package/dist/ee/stability.d.ts +4 -0
  354. package/dist/ee/stability.js +29 -0
  355. package/dist/ee/swr-cache.d.ts +14 -0
  356. package/dist/ee/swr-cache.js +34 -0
  357. package/dist/index.d.ts +143 -0
  358. package/dist/index.js +291 -0
  359. package/dist/integrations/index.d.ts +2 -0
  360. package/dist/integrations/index.js +2 -0
  361. package/dist/integrations/langchain.d.ts +64 -0
  362. package/dist/integrations/langchain.js +115 -0
  363. package/dist/integrations/llamaindex.d.ts +50 -0
  364. package/dist/integrations/llamaindex.js +91 -0
  365. package/dist/mcp/handlers/act.d.ts +5 -0
  366. package/dist/mcp/handlers/act.js +34 -0
  367. package/dist/mcp/handlers/definitions.d.ts +6 -0
  368. package/dist/mcp/handlers/definitions.js +395 -0
  369. package/dist/mcp/handlers/extract.d.ts +7 -0
  370. package/dist/mcp/handlers/extract.js +135 -0
  371. package/dist/mcp/handlers/fetch.d.ts +6 -0
  372. package/dist/mcp/handlers/fetch.js +98 -0
  373. package/dist/mcp/handlers/find.d.ts +5 -0
  374. package/dist/mcp/handlers/find.js +137 -0
  375. package/dist/mcp/handlers/index.d.ts +13 -0
  376. package/dist/mcp/handlers/index.js +63 -0
  377. package/dist/mcp/handlers/legacy.d.ts +25 -0
  378. package/dist/mcp/handlers/legacy.js +450 -0
  379. package/dist/mcp/handlers/meta.d.ts +6 -0
  380. package/dist/mcp/handlers/meta.js +40 -0
  381. package/dist/mcp/handlers/monitor.d.ts +5 -0
  382. package/dist/mcp/handlers/monitor.js +41 -0
  383. package/dist/mcp/handlers/observe.d.ts +8 -0
  384. package/dist/mcp/handlers/observe.js +37 -0
  385. package/dist/mcp/handlers/read.d.ts +6 -0
  386. package/dist/mcp/handlers/read.js +78 -0
  387. package/dist/mcp/handlers/see.d.ts +5 -0
  388. package/dist/mcp/handlers/see.js +75 -0
  389. package/dist/mcp/handlers/types.d.ts +29 -0
  390. package/dist/mcp/handlers/types.js +28 -0
  391. package/dist/mcp/server.d.ts +7 -0
  392. package/dist/mcp/server.js +108 -0
  393. package/dist/mcp/smart-router.d.ts +23 -0
  394. package/dist/mcp/smart-router.js +178 -0
  395. package/dist/server/app.d.ts +14 -0
  396. package/dist/server/app.js +632 -0
  397. package/dist/server/auth-store.d.ts +28 -0
  398. package/dist/server/auth-store.js +88 -0
  399. package/dist/server/bull-queues.d.ts +60 -0
  400. package/dist/server/bull-queues.js +90 -0
  401. package/dist/server/email-service.d.ts +55 -0
  402. package/dist/server/email-service.js +291 -0
  403. package/dist/server/job-queue.d.ts +100 -0
  404. package/dist/server/job-queue.js +145 -0
  405. package/dist/server/logger.d.ts +10 -0
  406. package/dist/server/logger.js +37 -0
  407. package/dist/server/middleware/audit-log.d.ts +14 -0
  408. package/dist/server/middleware/audit-log.js +73 -0
  409. package/dist/server/middleware/auth.d.ts +35 -0
  410. package/dist/server/middleware/auth.js +225 -0
  411. package/dist/server/middleware/rate-limit.d.ts +50 -0
  412. package/dist/server/middleware/rate-limit.js +270 -0
  413. package/dist/server/middleware/scope-guard.d.ts +25 -0
  414. package/dist/server/middleware/scope-guard.js +45 -0
  415. package/dist/server/middleware/url-validator.d.ts +15 -0
  416. package/dist/server/middleware/url-validator.js +201 -0
  417. package/dist/server/openapi.yaml +6418 -0
  418. package/dist/server/pg-auth-store.d.ts +146 -0
  419. package/dist/server/pg-auth-store.js +576 -0
  420. package/dist/server/pg-job-queue.d.ts +59 -0
  421. package/dist/server/pg-job-queue.js +375 -0
  422. package/dist/server/routes/activity.d.ts +6 -0
  423. package/dist/server/routes/activity.js +79 -0
  424. package/dist/server/routes/admin-active.d.ts +7 -0
  425. package/dist/server/routes/admin-active.js +120 -0
  426. package/dist/server/routes/admin-stats.d.ts +7 -0
  427. package/dist/server/routes/admin-stats.js +176 -0
  428. package/dist/server/routes/agent.d.ts +24 -0
  429. package/dist/server/routes/agent.js +480 -0
  430. package/dist/server/routes/answer.d.ts +5 -0
  431. package/dist/server/routes/answer.js +125 -0
  432. package/dist/server/routes/ask.d.ts +28 -0
  433. package/dist/server/routes/ask.js +295 -0
  434. package/dist/server/routes/batch.d.ts +6 -0
  435. package/dist/server/routes/batch.js +493 -0
  436. package/dist/server/routes/cache-warm.d.ts +25 -0
  437. package/dist/server/routes/cache-warm.js +212 -0
  438. package/dist/server/routes/cli-usage.d.ts +6 -0
  439. package/dist/server/routes/cli-usage.js +127 -0
  440. package/dist/server/routes/compat.d.ts +23 -0
  441. package/dist/server/routes/compat.js +652 -0
  442. package/dist/server/routes/crawl.d.ts +13 -0
  443. package/dist/server/routes/crawl.js +287 -0
  444. package/dist/server/routes/deep-fetch.d.ts +8 -0
  445. package/dist/server/routes/deep-fetch.js +57 -0
  446. package/dist/server/routes/deep-research.d.ts +11 -0
  447. package/dist/server/routes/deep-research.js +232 -0
  448. package/dist/server/routes/demo.d.ts +24 -0
  449. package/dist/server/routes/demo.js +517 -0
  450. package/dist/server/routes/do.d.ts +8 -0
  451. package/dist/server/routes/do.js +72 -0
  452. package/dist/server/routes/extract.d.ts +14 -0
  453. package/dist/server/routes/extract.js +325 -0
  454. package/dist/server/routes/feed.d.ts +15 -0
  455. package/dist/server/routes/feed.js +311 -0
  456. package/dist/server/routes/fetch-queue.d.ts +13 -0
  457. package/dist/server/routes/fetch-queue.js +357 -0
  458. package/dist/server/routes/fetch.d.ts +7 -0
  459. package/dist/server/routes/fetch.js +1274 -0
  460. package/dist/server/routes/go.d.ts +14 -0
  461. package/dist/server/routes/go.js +81 -0
  462. package/dist/server/routes/health.d.ts +11 -0
  463. package/dist/server/routes/health.js +141 -0
  464. package/dist/server/routes/jobs.d.ts +7 -0
  465. package/dist/server/routes/jobs.js +574 -0
  466. package/dist/server/routes/map.d.ts +11 -0
  467. package/dist/server/routes/map.js +116 -0
  468. package/dist/server/routes/mcp.d.ts +14 -0
  469. package/dist/server/routes/mcp.js +197 -0
  470. package/dist/server/routes/metrics.d.ts +37 -0
  471. package/dist/server/routes/metrics.js +149 -0
  472. package/dist/server/routes/oauth.d.ts +9 -0
  473. package/dist/server/routes/oauth.js +396 -0
  474. package/dist/server/routes/playground.d.ts +17 -0
  475. package/dist/server/routes/playground.js +283 -0
  476. package/dist/server/routes/reader.d.ts +18 -0
  477. package/dist/server/routes/reader.js +192 -0
  478. package/dist/server/routes/research.d.ts +14 -0
  479. package/dist/server/routes/research.js +482 -0
  480. package/dist/server/routes/screenshot.d.ts +22 -0
  481. package/dist/server/routes/screenshot.js +820 -0
  482. package/dist/server/routes/search.d.ts +6 -0
  483. package/dist/server/routes/search.js +874 -0
  484. package/dist/server/routes/session.d.ts +17 -0
  485. package/dist/server/routes/session.js +548 -0
  486. package/dist/server/routes/share.d.ts +18 -0
  487. package/dist/server/routes/share.js +462 -0
  488. package/dist/server/routes/smart-search/handlers/cars.d.ts +2 -0
  489. package/dist/server/routes/smart-search/handlers/cars.js +102 -0
  490. package/dist/server/routes/smart-search/handlers/flights.d.ts +2 -0
  491. package/dist/server/routes/smart-search/handlers/flights.js +72 -0
  492. package/dist/server/routes/smart-search/handlers/general.d.ts +13 -0
  493. package/dist/server/routes/smart-search/handlers/general.js +717 -0
  494. package/dist/server/routes/smart-search/handlers/hotels.d.ts +2 -0
  495. package/dist/server/routes/smart-search/handlers/hotels.js +88 -0
  496. package/dist/server/routes/smart-search/handlers/products.d.ts +2 -0
  497. package/dist/server/routes/smart-search/handlers/products.js +1309 -0
  498. package/dist/server/routes/smart-search/handlers/rental.d.ts +2 -0
  499. package/dist/server/routes/smart-search/handlers/rental.js +154 -0
  500. package/dist/server/routes/smart-search/handlers/restaurants.d.ts +2 -0
  501. package/dist/server/routes/smart-search/handlers/restaurants.js +225 -0
  502. package/dist/server/routes/smart-search/handlers/transit-verdict.d.ts +41 -0
  503. package/dist/server/routes/smart-search/handlers/transit-verdict.js +224 -0
  504. package/dist/server/routes/smart-search/index.d.ts +19 -0
  505. package/dist/server/routes/smart-search/index.js +546 -0
  506. package/dist/server/routes/smart-search/intent.d.ts +3 -0
  507. package/dist/server/routes/smart-search/intent.js +264 -0
  508. package/dist/server/routes/smart-search/llm.d.ts +16 -0
  509. package/dist/server/routes/smart-search/llm.js +70 -0
  510. package/dist/server/routes/smart-search/sources/reddit.d.ts +18 -0
  511. package/dist/server/routes/smart-search/sources/reddit.js +34 -0
  512. package/dist/server/routes/smart-search/sources/yelp.d.ts +25 -0
  513. package/dist/server/routes/smart-search/sources/yelp.js +171 -0
  514. package/dist/server/routes/smart-search/sources/youtube.d.ts +8 -0
  515. package/dist/server/routes/smart-search/sources/youtube.js +9 -0
  516. package/dist/server/routes/smart-search/types.d.ts +81 -0
  517. package/dist/server/routes/smart-search/types.js +1 -0
  518. package/dist/server/routes/smart-search/utils.d.ts +20 -0
  519. package/dist/server/routes/smart-search/utils.js +146 -0
  520. package/dist/server/routes/stats.d.ts +6 -0
  521. package/dist/server/routes/stats.js +71 -0
  522. package/dist/server/routes/stripe.d.ts +15 -0
  523. package/dist/server/routes/stripe.js +296 -0
  524. package/dist/server/routes/transcript-export.d.ts +10 -0
  525. package/dist/server/routes/transcript-export.js +178 -0
  526. package/dist/server/routes/usage.d.ts +9 -0
  527. package/dist/server/routes/usage.js +279 -0
  528. package/dist/server/routes/users.d.ts +8 -0
  529. package/dist/server/routes/users.js +1867 -0
  530. package/dist/server/routes/watch.d.ts +15 -0
  531. package/dist/server/routes/watch.js +309 -0
  532. package/dist/server/routes/webhooks.d.ts +26 -0
  533. package/dist/server/routes/webhooks.js +170 -0
  534. package/dist/server/routes/youtube.d.ts +6 -0
  535. package/dist/server/routes/youtube.js +130 -0
  536. package/dist/server/sentry.d.ts +14 -0
  537. package/dist/server/sentry.js +104 -0
  538. package/dist/server/types.d.ts +15 -0
  539. package/dist/server/types.js +7 -0
  540. package/dist/server/utils/response.d.ts +44 -0
  541. package/dist/server/utils/response.js +69 -0
  542. package/dist/server/utils/sse.d.ts +22 -0
  543. package/dist/server/utils/sse.js +38 -0
  544. package/dist/types.d.ts +552 -0
  545. package/dist/types.js +39 -0
  546. package/llms.txt +105 -0
  547. package/package.json +189 -0
@@ -0,0 +1,717 @@
1
+ import { peel } from '../../../../index.js';
2
+ import { getBestSearchProvider, } from '../../../../core/search-provider.js';
3
+ import { getSourceCredibility } from '../../../../core/source-credibility.js';
4
+ import { isFactualQuery } from '../../../../core/source-scoring.js';
5
+ import { selectEvidence, formatEvidenceForLLM, isUnusableEvidenceContent, } from '../../../../core/selective-evidence.js';
6
+ import { callLLMQuick, sanitizeSearchQuery, PROMPT_INJECTION_DEFENSE } from '../llm.js';
7
+ import { buildTransitVerdict } from './transit-verdict.js';
8
+ /**
9
+ * Parse a transit/travel booking query to extract origin, destination, dates, and trip type.
10
+ */
11
+ export function parseTransitQuery(query) {
12
+ const q = query.toLowerCase();
13
+ // Detect transport mode
14
+ let mode = 'bus';
15
+ if (/\b(train|amtrak|acela|metro.?north|lirr|brightline)\b/.test(q))
16
+ mode = 'train';
17
+ else if (/\b(ferry|ferries|water taxi)\b/.test(q))
18
+ mode = 'ferry';
19
+ // Detect round trip
20
+ const isRoundTrip = /\b(round\s*trip|return|back|both\s*ways|come\s*back)\b/.test(q);
21
+ // Extract origin and destination
22
+ let origin = '';
23
+ let destination = '';
24
+ // Clean up noise words helper
25
+ const stripNoise = (s) => {
26
+ let cleaned = s.trim();
27
+ // Repeatedly strip trailing noise words
28
+ const noisePattern = /\b(i|want|to|the|a|an|take|cheap|cheapest|find|help|me|please|it|my|is|and|but|or)\s*$/i;
29
+ for (let i = 0; i < 5; i++) {
30
+ const before = cleaned;
31
+ cleaned = cleaned.replace(noisePattern, '').trim();
32
+ if (cleaned === before)
33
+ break;
34
+ }
35
+ // Also strip leading noise words
36
+ const leadingNoise = /^\s*(i|want|to|the|a|an|take|cheap|cheapest|find|help|me|please|it|my|is|and|but|or)\b\s*/i;
37
+ for (let i = 0; i < 5; i++) {
38
+ const before = cleaned;
39
+ cleaned = cleaned.replace(leadingNoise, '').trim();
40
+ if (cleaned === before)
41
+ break;
42
+ }
43
+ return cleaned;
44
+ };
45
+ // City name pattern: letters and spaces, not starting with common noise words
46
+ // We use a terminator approach: capture until we hit a known stop word or end of string
47
+ const STOP = '(?=\\s+(?:i\\b|on\\b|for\\b|cheap|bus\\b|train\\b|ferry|ticket|price|depart|return|round|one\\b|april|may|jun|jul|aug|sep|oct|nov|dec|jan|feb|mar|\\d)|\\s*[.,;!?]|\\s*$)';
48
+ // Pattern 1: "from <origin> to <destination>" โ€” most common transit pattern
49
+ const fromToRe = new RegExp(`\\bfrom\\s+([a-z][a-z\\s.]{1,30}?)\\s+(?:to|โ†’|->)\\s+([a-z][a-z\\s.]{1,30}?)${STOP}`, 'i');
50
+ const fromToMatch = q.match(fromToRe);
51
+ if (fromToMatch) {
52
+ const potentialOrigin = stripNoise(fromToMatch[1]);
53
+ const potentialDest = stripNoise(fromToMatch[2]);
54
+ if (potentialOrigin.length >= 2 && potentialDest.length >= 2) {
55
+ origin = potentialOrigin;
56
+ destination = potentialDest;
57
+ }
58
+ }
59
+ // Pattern 2: "<city> ticket from <city>" (e.g., "boston ticket from new york")
60
+ if (!origin || !destination) {
61
+ const ticketFromRe = new RegExp(`\\b([a-z][a-z\\s.]{1,30}?)\\s+ticket(?:s)?\\s+from\\s+([a-z][a-z\\s.]{1,30}?)${STOP}`, 'i');
62
+ const ticketFromMatch = q.match(ticketFromRe);
63
+ if (ticketFromMatch) {
64
+ const potentialDest = stripNoise(ticketFromMatch[1]);
65
+ const potentialOrigin = stripNoise(ticketFromMatch[2]);
66
+ if (potentialOrigin.length >= 2 && potentialDest.length >= 2) {
67
+ destination = potentialDest;
68
+ origin = potentialOrigin;
69
+ }
70
+ }
71
+ }
72
+ // Pattern 3: "to <destination> from <origin>"
73
+ if (!origin || !destination) {
74
+ const toFromRe = new RegExp(`\\b(?:to|โ†’|->)\\s+([a-z][a-z\\s.]{1,30}?)\\s+from\\s+([a-z][a-z\\s.]{1,30}?)${STOP}`, 'i');
75
+ const toFromMatch = q.match(toFromRe);
76
+ if (toFromMatch) {
77
+ destination = stripNoise(toFromMatch[1]);
78
+ origin = stripNoise(toFromMatch[2]);
79
+ }
80
+ }
81
+ // Extract dates (basic: "april 2", "apr 5th", "4/2", etc.)
82
+ let departDate = '';
83
+ let returnDate = '';
84
+ const datePatterns = q.matchAll(/\b(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+(\d{1,2})(?:st|nd|rd|th)?|\b(\d{1,2})\/(\d{1,2})\b/gi);
85
+ const dates = [...datePatterns].map(m => m[0]);
86
+ if (dates.length >= 1)
87
+ departDate = dates[0];
88
+ if (dates.length >= 2)
89
+ returnDate = dates[1];
90
+ return { origin, destination, departDate, returnDate, isRoundTrip, mode };
91
+ }
92
+ export async function handleGeneralSearch(query) {
93
+ const t0 = Date.now();
94
+ // Equipment rental / service business enhancement via Google Places
95
+ const GOOGLE_PLACES_KEY = process.env.GOOGLE_PLACES_API_KEY;
96
+ const isEquipmentRental = /\b(rent|rental|renting|hire|lease)\b/.test(query) && /\b(forklift|dumpster|pressure washer|generator|excavator|bobcat|crane|scaffolding|tent|truck|van|trailer|equipment|tool|power tool)\b/.test(query);
97
+ const isServiceBusiness = /\b(plumber|electrician|mechanic|dentist|doctor|lawyer|locksmith|handyman|contractor|vet|salon|barber|spa|gym|daycare|moving|storage|cleaning|pest control|roofing|hvac|landscaping)\b/.test(query) && /\b(near|in|around|open|best|cheap|emergency|24.hour)\b/.test(query);
98
+ const isGasStation = /\b(gas|gasoline|fuel|gas station|petrol|diesel)\b/.test(query) && /\b(cheap|cheapest|price|near|closest|best)\b/.test(query);
99
+ const isTravelBooking = /\b(cruise|vacation|resort|all.inclusive|trip|package|tour|excursion|safari|honeymoon|disneyland|disney world|disney cruise|universal|theme park|spring break)\b/.test(query) && /\b(cheap|cheapest|price|ticket|book|deal|cost|per person)\b/.test(query);
100
+ const isTransitBooking = /\b(bus|buses|coach|greyhound|flixbus|megabus|busbud|wanderu|peter pan|ourbus|boltbus|train|trains|amtrak|acela|metro.?north|lirr|nj\s*transit|brightline|ferry|ferries|water taxi)\b/i.test(query) && /\b(ticket|tickets|book|booking|cheap|cheapest|price|schedule|ride|fare|fares|route|take|travel|trip|round\s*trip|one\s*way|depart|return|from|to)\b/i.test(query);
101
+ let localBusinesses = [];
102
+ let transitVerdict = null;
103
+ // โ”€โ”€ Try Places API (New) for gas stations (has fuel prices) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
104
+ if (isGasStation && GOOGLE_PLACES_KEY) {
105
+ try {
106
+ const newApiRes = await fetch('https://places.googleapis.com/v1/places:searchText', {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'X-Goog-Api-Key': GOOGLE_PLACES_KEY,
111
+ 'X-Goog-FieldMask': 'places.displayName,places.formattedAddress,places.fuelOptions,places.rating,places.userRatingCount,places.currentOpeningHours,places.googleMapsUri,places.location',
112
+ },
113
+ body: JSON.stringify({ textQuery: query, maxResultCount: 10 }),
114
+ signal: AbortSignal.timeout(5000),
115
+ });
116
+ if (newApiRes.ok) {
117
+ const data = await newApiRes.json();
118
+ if (data.places?.length > 0) {
119
+ const shortDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
120
+ const dayMap = { Monday: 'Mon', Tuesday: 'Tue', Wednesday: 'Wed', Thursday: 'Thu', Friday: 'Fri', Saturday: 'Sat', Sunday: 'Sun' };
121
+ const today = shortDays[new Date().getDay()];
122
+ localBusinesses = data.places.map((p) => {
123
+ // Parse fuel prices
124
+ const fuelPrices = {};
125
+ if (p.fuelOptions?.fuelPrices) {
126
+ for (const fp of p.fuelOptions.fuelPrices) {
127
+ const price = fp.price ? `$${fp.price.units || 0}.${String(fp.price.nanos || 0).padStart(9, '0').substring(0, 2)}` : null;
128
+ if (price) {
129
+ const typeMap = {
130
+ 'REGULAR_UNLEADED': 'Regular',
131
+ 'MIDGRADE': 'Midgrade',
132
+ 'PREMIUM': 'Premium',
133
+ 'DIESEL': 'Diesel',
134
+ 'E85': 'E85',
135
+ };
136
+ fuelPrices[typeMap[fp.type] || fp.type] = price;
137
+ }
138
+ }
139
+ }
140
+ // Parse hours
141
+ const hours = {};
142
+ if (p.currentOpeningHours?.weekdayDescriptions) {
143
+ for (const desc of p.currentOpeningHours.weekdayDescriptions) {
144
+ const colonIdx = desc.indexOf(':');
145
+ if (colonIdx > 0) {
146
+ const dayFull = desc.substring(0, colonIdx).trim();
147
+ const timeStr = desc.substring(colonIdx + 1).trim();
148
+ if (dayMap[dayFull])
149
+ hours[dayMap[dayFull]] = timeStr;
150
+ }
151
+ }
152
+ }
153
+ return {
154
+ name: p.displayName?.text || 'Gas Station',
155
+ address: p.formattedAddress || '',
156
+ rating: p.rating,
157
+ reviewCount: p.userRatingCount || 0,
158
+ isOpenNow: p.currentOpeningHours?.openNow,
159
+ todayHours: hours[today] || '',
160
+ googleMapsUrl: p.googleMapsUri || '',
161
+ fuelPrices,
162
+ latitude: p.location?.latitude,
163
+ longitude: p.location?.longitude,
164
+ businessStatus: 'OPERATIONAL',
165
+ };
166
+ });
167
+ console.log(`[smart-search] Places API (New) returned ${localBusinesses.length} gas stations`);
168
+ }
169
+ }
170
+ }
171
+ catch { /* New API failed โ€” fall through to legacy */ }
172
+ }
173
+ // โ”€โ”€ Legacy Google Places search (used when Places API New is unavailable or non-gas queries) โ”€โ”€
174
+ if (localBusinesses.length === 0 && (isEquipmentRental || isServiceBusiness || isGasStation) && GOOGLE_PLACES_KEY) {
175
+ try {
176
+ // Use Google Places Text Search
177
+ const findRes = await fetch(`https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(query)}&key=${GOOGLE_PLACES_KEY}`, { signal: AbortSignal.timeout(5000) });
178
+ if (findRes.ok) {
179
+ const findData = await findRes.json();
180
+ if (findData.status === 'OK' && findData.results?.length > 0) {
181
+ // Get details for top 3 (hours, phone, etc.)
182
+ const top5 = findData.results.slice(0, 5);
183
+ const details = await Promise.allSettled(top5.slice(0, 3).map(async (place) => {
184
+ const detailRes = await fetch(`https://maps.googleapis.com/maps/api/place/details/json?place_id=${place.place_id}&fields=name,formatted_phone_number,opening_hours,rating,user_ratings_total,url,formatted_address,website,business_status&key=${GOOGLE_PLACES_KEY}`, { signal: AbortSignal.timeout(3000) });
185
+ if (!detailRes.ok)
186
+ return null;
187
+ const detailData = await detailRes.json();
188
+ return detailData.result || null;
189
+ }));
190
+ localBusinesses = top5.map((place, i) => {
191
+ const detail = details[i]?.status === 'fulfilled' ? details[i].value : null;
192
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
193
+ const today = dayNames[new Date().getDay()];
194
+ const todayHours = detail?.opening_hours?.weekday_text?.find((h) => {
195
+ const dayMap = { Monday: 'Mon', Tuesday: 'Tue', Wednesday: 'Wed', Thursday: 'Thu', Friday: 'Fri', Saturday: 'Sat', Sunday: 'Sun' };
196
+ return Object.entries(dayMap).some(([full, short]) => h.startsWith(full) && short === today);
197
+ })?.split(': ').slice(1).join(': ') || '';
198
+ return {
199
+ name: detail?.name || place.name,
200
+ address: detail?.formatted_address || place.formatted_address || '',
201
+ phone: detail?.formatted_phone_number || '',
202
+ rating: detail?.rating || place.rating,
203
+ reviewCount: detail?.user_ratings_total || place.user_ratings_total || 0,
204
+ isOpenNow: detail?.opening_hours?.open_now ?? place.opening_hours?.open_now,
205
+ todayHours,
206
+ website: detail?.website || '',
207
+ googleMapsUrl: detail?.url || '',
208
+ mapEmbedUrl: `https://www.google.com/maps/embed/v1/place?q=place_id:${place.place_id}&key=${GOOGLE_PLACES_KEY}`,
209
+ latitude: place.geometry?.location?.lat,
210
+ longitude: place.geometry?.location?.lng,
211
+ businessStatus: detail?.business_status || place.business_status || 'OPERATIONAL',
212
+ };
213
+ }).filter((b) => b.businessStatus === 'OPERATIONAL');
214
+ }
215
+ }
216
+ }
217
+ catch { /* Google Places failed โ€” continue with web search */ }
218
+ }
219
+ const { provider: searchProvider } = getBestSearchProvider();
220
+ // Transit queries already do focused booking-site searches below.
221
+ // Skip the broad generic web search here so we don't burn 20s+ before the useful path starts.
222
+ const rawResults = isTransitBooking
223
+ ? []
224
+ : await searchProvider.searchWeb(query, { count: 10 });
225
+ const searchMs = Date.now() - t0;
226
+ const getDomain = (url) => {
227
+ try {
228
+ return new URL(url).hostname.replace(/^www\./, '');
229
+ }
230
+ catch {
231
+ return '';
232
+ }
233
+ };
234
+ const tierOrder = { official: 0, established: 1, community: 2, new: 3, suspicious: 4 };
235
+ let results = rawResults
236
+ .map((r) => {
237
+ const cred = getSourceCredibility(r.url);
238
+ return {
239
+ title: r.title,
240
+ url: r.url,
241
+ snippet: r.snippet,
242
+ domain: getDomain(r.url),
243
+ credibility: cred,
244
+ ...(r.imageUrl ? { imageUrl: r.imageUrl } : {}),
245
+ };
246
+ })
247
+ .sort((a, b) => {
248
+ const aTier = tierOrder[a.credibility?.tier || 'new'] ?? 3;
249
+ const bTier = tierOrder[b.credibility?.tier || 'new'] ?? 3;
250
+ return aTier - bTier;
251
+ })
252
+ .map((r, i) => ({ ...r, rank: i + 1 }));
253
+ // Enrich only a bounded number of results, with bounded concurrency.
254
+ // Smart-search runs inside API pods, so parallel peel fanout directly affects memory pressure.
255
+ const tPeel = Date.now();
256
+ const enrichLimit = parseInt(process.env.SMART_SEARCH_ENRICH_LIMIT || '6', 10);
257
+ const enrichConcurrency = parseInt(process.env.SMART_SEARCH_ENRICH_CONCURRENCY || '3', 10);
258
+ const topResults = isTransitBooking ? [] : results.slice(0, enrichLimit);
259
+ console.log(`[smart-search] handleGeneralSearch: enriching ${topResults.length} pages via peel (concurrency ${enrichConcurrency})`);
260
+ async function pLimited(tasks, n) {
261
+ const out = new Array(tasks.length);
262
+ let cursor = 0;
263
+ async function worker() {
264
+ while (cursor < tasks.length) {
265
+ const idx = cursor++;
266
+ out[idx] = await tasks[idx]();
267
+ }
268
+ }
269
+ await Promise.all(Array.from({ length: Math.min(n, tasks.length) }, () => worker()));
270
+ return out;
271
+ }
272
+ const enriched = await Promise.allSettled(await pLimited(topResults.map((r) => async () => {
273
+ try {
274
+ // Use lite mode + noEscalate for speed: skip pruning, quality scoring,
275
+ // metadata extraction, and browser escalation. We only need a rough
276
+ // markdown body for BM25 snippet extraction + LLM synthesis.
277
+ const peeled = await peel(r.url, { timeout: 4000, maxTokens: 2000, lite: true, noEscalate: true });
278
+ return {
279
+ url: r.url,
280
+ content: peeled.content?.substring(0, 2000),
281
+ title: peeled.title || r.title,
282
+ fetchTimeMs: peeled.elapsed,
283
+ metadata: peeled.metadata,
284
+ structured: peeled.domainData?.structured,
285
+ };
286
+ }
287
+ catch {
288
+ return { url: r.url, content: null, title: r.title, fetchTimeMs: 0, metadata: undefined, structured: undefined };
289
+ }
290
+ }), enrichConcurrency));
291
+ const peelMs = Date.now() - tPeel;
292
+ // Check if any peel succeeded; if none did, skip LLM and return raw results
293
+ const anyPeelSucceeded = enriched.some((s) => s.status === 'fulfilled' && s.value.content !== null);
294
+ for (const settled of enriched) {
295
+ if (settled.status === 'fulfilled' && settled.value.content) {
296
+ const match = results.find((r) => r.url === settled.value.url);
297
+ if (match) {
298
+ match.content = settled.value.content;
299
+ match.fetchTimeMs = settled.value.fetchTimeMs;
300
+ // Preserve provider thumbnail when present; otherwise use peeled OG image
301
+ const ogImage = settled.value.metadata?.image;
302
+ if (ogImage && !match.imageUrl) {
303
+ match.imageUrl = ogImage;
304
+ }
305
+ }
306
+ }
307
+ }
308
+ let content = results
309
+ .map((r, i) => `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`)
310
+ .join('\n\n');
311
+ // For equipment rentals and gas stations, also search for pricing data
312
+ let pricingInfo = '';
313
+ if (isGasStation) {
314
+ try {
315
+ const locMatch = query.match(/\b(?:in|near|around)\s+([a-z\s]+?)(?:\s+(?:under|below|cheap|\$).*)?$/i);
316
+ const gasLocation = locMatch ? locMatch[1].trim() : 'New York';
317
+ const gasPriceResults = await searchProvider.searchWeb(`gas prices ${gasLocation} per gallon today cheapest gasbuddy`, { count: 3 });
318
+ const gasPrices = [];
319
+ for (const r of gasPriceResults) {
320
+ const text = `${r.title || ''} ${r.snippet || ''}`;
321
+ const priceMatches = text.match(/\$\d+\.\d{2}/g);
322
+ if (priceMatches)
323
+ gasPrices.push(...priceMatches);
324
+ }
325
+ if (gasPrices.length > 0) {
326
+ const uniquePrices = [...new Set(gasPrices)].sort((a, b) => parseFloat(a.slice(1)) - parseFloat(b.slice(1)));
327
+ pricingInfo = `\n\n## โ›ฝ Gas Prices (${gasLocation})\n${uniquePrices.slice(0, 8).map(p => `- ${p}/gal`).join('\n')}`;
328
+ // Add pricing snippets for AI
329
+ for (const r of gasPriceResults.slice(0, 2)) {
330
+ if (r.snippet?.match(/\$/)) {
331
+ results.push({
332
+ title: r.title,
333
+ url: r.url,
334
+ snippet: r.snippet,
335
+ domain: getDomain(r.url),
336
+ content: r.snippet,
337
+ isPricing: true,
338
+ });
339
+ }
340
+ }
341
+ }
342
+ }
343
+ catch { /* gas price search failed โ€” non-fatal */ }
344
+ }
345
+ else if (isTravelBooking) {
346
+ try {
347
+ // Search specifically for prices + comparison across providers
348
+ const travelPriceResults = await searchProvider.searchWeb(`${query} price per person comparison cheapest 2026 site:cruisefever.net OR site:cruisecritic.com OR site:vacationstogo.com OR site:costcotravel.com OR site:kayak.com`, { count: 3 });
349
+ const travelPrices = [];
350
+ for (const r of travelPriceResults) {
351
+ const text = `${r.title || ''} ${r.snippet || ''}`;
352
+ const priceMatches = text.match(/\$[\d,]+(?:\s*(?:per person|pp|\/person))?/gi);
353
+ if (priceMatches)
354
+ travelPrices.push(...priceMatches.slice(0, 4));
355
+ }
356
+ if (travelPrices.length > 0) {
357
+ pricingInfo = `\n\n## ๐Ÿ’ฐ Pricing Found\n${[...new Set(travelPrices)].slice(0, 8).map(p => `- ${p}`).join('\n')}`;
358
+ }
359
+ // Peel the top comparison page for detailed data
360
+ const comparisonPage = travelPriceResults[0];
361
+ if (comparisonPage?.url) {
362
+ try {
363
+ const peeled = await peel(comparisonPage.url, { timeout: 6000, maxTokens: 3000 });
364
+ if (peeled.content && peeled.content.length > 200) {
365
+ results.push({
366
+ title: comparisonPage.title,
367
+ url: comparisonPage.url,
368
+ snippet: comparisonPage.snippet,
369
+ domain: getDomain(comparisonPage.url),
370
+ content: peeled.content.substring(0, 3000),
371
+ isPricing: true,
372
+ });
373
+ }
374
+ }
375
+ catch { /* peel failed โ€” use snippet */ }
376
+ }
377
+ // Add remaining results for sources
378
+ for (const r of travelPriceResults.slice(1, 3)) {
379
+ if (r.snippet) {
380
+ results.push({
381
+ title: r.title,
382
+ url: r.url,
383
+ snippet: r.snippet,
384
+ domain: getDomain(r.url),
385
+ content: r.snippet,
386
+ isPricing: true,
387
+ });
388
+ }
389
+ }
390
+ }
391
+ catch { /* travel price search failed */ }
392
+ }
393
+ else if (isTransitBooking) {
394
+ // โ”€โ”€ Transit / ground-travel booking (bus, train, ferry) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
395
+ // Parse origin, destination, and dates from query for targeted route searches
396
+ try {
397
+ const transitInfo = parseTransitQuery(query);
398
+ const { origin, destination, isRoundTrip } = transitInfo;
399
+ const TRANSIT_DOMAINS = [
400
+ 'wanderu.com', 'flixbus.com', 'greyhound.com', 'busbud.com', 'amtrak.com', 'rome2rio.com',
401
+ 'coachrun.com', 'checkmybus.com', 'peterpanbus.com', 'ourbus.com', 'omio.com', 'gotobus.com', 'megabus.com', 'trailways.com'
402
+ ];
403
+ const siteFilter = TRANSIT_DOMAINS.map(d => `site:${d}`).join(' OR ');
404
+ // Search outbound (focused booking-site query first)
405
+ const outboundQuery = origin && destination
406
+ ? `${origin} to ${destination} bus train ticket price ${siteFilter}`
407
+ : `${query} ${siteFilter}`;
408
+ let outboundResults = await searchProvider.searchWeb(outboundQuery, { count: 6 });
409
+ // If the site-filtered query underperforms in production, fall back to a broader transit search
410
+ if (outboundResults.length === 0 || !outboundResults.some(r => TRANSIT_DOMAINS.some(d => r.url.includes(d)))) {
411
+ const broadOutboundQuery = origin && destination
412
+ ? `${origin} to ${destination} cheapest bus train ticket`
413
+ : `${query} cheapest bus train ticket`;
414
+ const broadResults = await searchProvider.searchWeb(broadOutboundQuery, { count: 10 });
415
+ outboundResults = broadResults.filter(r => TRANSIT_DOMAINS.some(d => r.url.includes(d))).slice(0, 8);
416
+ console.log(`[smart-search] Transit outbound fallback search used: ${outboundResults.length} filtered results`);
417
+ }
418
+ // Search return leg if round trip
419
+ let returnResults = [];
420
+ if (isRoundTrip && origin && destination) {
421
+ const returnQuery = `${destination} to ${origin} bus train ticket price ${siteFilter}`;
422
+ returnResults = await searchProvider.searchWeb(returnQuery, { count: 4 });
423
+ if (returnResults.length === 0 || !returnResults.some(r => TRANSIT_DOMAINS.some(d => r.url.includes(d)))) {
424
+ const broadReturnQuery = `${destination} to ${origin} cheapest bus train ticket`;
425
+ const broadReturnResults = await searchProvider.searchWeb(broadReturnQuery, { count: 8 });
426
+ returnResults = broadReturnResults.filter(r => TRANSIT_DOMAINS.some(d => r.url.includes(d))).slice(0, 5);
427
+ console.log(`[smart-search] Transit return fallback search used: ${returnResults.length} filtered results`);
428
+ }
429
+ }
430
+ // Tag return results so we can propagate leg info downstream
431
+ const allTransitResults = [
432
+ ...outboundResults.map(r => ({ ...r, _leg: 'outbound' })),
433
+ ...returnResults.map(r => ({ ...r, _leg: 'return' })),
434
+ ];
435
+ const transitPrices = [];
436
+ // Peel top route pages from booking sites (up to 6 โ€” reserve 2 for return)
437
+ const outboundPeelTargets = allTransitResults
438
+ .filter(r => r._leg === 'outbound' && TRANSIT_DOMAINS.some(d => r.url.includes(d)))
439
+ .slice(0, 4);
440
+ const returnPeelTargets = allTransitResults
441
+ .filter(r => r._leg === 'return' && TRANSIT_DOMAINS.some(d => r.url.includes(d)))
442
+ .slice(0, 2);
443
+ const peelTargets = [...outboundPeelTargets, ...returnPeelTargets];
444
+ const transitPeeled = await Promise.allSettled(peelTargets.map(async (r) => {
445
+ const peeled = await peel(r.url, { timeout: 6000, maxTokens: 3000 });
446
+ return { url: r.url, title: r.title || peeled.title || '', content: peeled.content || '', snippet: r.snippet || '', legHint: r._leg };
447
+ }));
448
+ for (const settled of transitPeeled) {
449
+ if (settled.status === 'fulfilled' && settled.value.content) {
450
+ const v = settled.value;
451
+ const text = `${v.content} ${v.snippet}`;
452
+ const priceMatches = text.match(/\$\d+(?:\.\d{2})?/g);
453
+ if (priceMatches)
454
+ transitPrices.push(...priceMatches);
455
+ results.push({
456
+ title: v.title,
457
+ url: v.url,
458
+ snippet: v.snippet,
459
+ domain: getDomain(v.url),
460
+ content: v.content.substring(0, 3000),
461
+ isPricing: true,
462
+ isTransitSource: true,
463
+ legHint: v.legHint,
464
+ });
465
+ }
466
+ }
467
+ // Also add non-peeled transit results with snippets
468
+ for (const r of allTransitResults) {
469
+ if (!peelTargets.some(pt => pt.url === r.url) && r.snippet) {
470
+ const text = `${r.title || ''} ${r.snippet}`;
471
+ const priceMatches = text.match(/\$\d+(?:\.\d{2})?/g);
472
+ if (priceMatches)
473
+ transitPrices.push(...priceMatches);
474
+ results.push({
475
+ title: r.title,
476
+ url: r.url,
477
+ snippet: r.snippet,
478
+ domain: getDomain(r.url),
479
+ content: r.snippet,
480
+ isPricing: true,
481
+ isTransitSource: true,
482
+ legHint: r._leg,
483
+ });
484
+ }
485
+ }
486
+ // โ”€โ”€ Build structured transit verdict โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
487
+ const transitSourcesForVerdict = results
488
+ .filter((r) => r.isTransitSource)
489
+ .map((r) => ({
490
+ url: r.url,
491
+ domain: r.domain || getDomain(r.url),
492
+ title: r.title || '',
493
+ content: r.content || '',
494
+ snippet: r.snippet || '',
495
+ isTransitSource: true,
496
+ legHint: r.legHint,
497
+ }));
498
+ transitVerdict = buildTransitVerdict({
499
+ query,
500
+ transitSources: transitSourcesForVerdict,
501
+ parsedQuery: transitInfo,
502
+ });
503
+ // Build pricingInfo from the verdict (single source of truth) or fall back to raw prices
504
+ if (transitVerdict) {
505
+ const routeLabel = origin && destination
506
+ ? `${origin.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')} โ†’ ${destination.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}`
507
+ : 'this route';
508
+ const allAltPrices = transitVerdict.alternatives.map(a => `$${a.price.toFixed(2)} (${a.provider})`);
509
+ pricingInfo = `\n\n## ๐ŸšŒ Transit Prices Found\nCheapest: **$${transitVerdict.bestOption.price.toFixed(2)}** on ${transitVerdict.bestOption.provider} for ${routeLabel}`;
510
+ if (allAltPrices.length > 0) {
511
+ pricingInfo += `\nAlternatives: ${allAltPrices.join(', ')}`;
512
+ }
513
+ if (transitVerdict.totals?.roundTripLowest) {
514
+ pricingInfo += `\n๐Ÿ”„ Round trip from **$${transitVerdict.totals.roundTripLowest.toFixed(2)}**`;
515
+ }
516
+ console.log(`[smart-search] Transit verdict built: ${transitVerdict.headline} (${transitVerdict.confidence})`);
517
+ }
518
+ else if (transitPrices.length > 0) {
519
+ const uniquePrices = [...new Set(transitPrices)]
520
+ .map(p => parseFloat(p.replace('$', '')))
521
+ .filter(p => p > 0 && p < 1000)
522
+ .sort((a, b) => a - b);
523
+ if (uniquePrices.length > 0) {
524
+ const cheapest = uniquePrices[0];
525
+ const routeLabel = origin && destination ? `${origin} โ†’ ${destination}` : 'this route';
526
+ pricingInfo = `\n\n## ๐ŸšŒ Transit Prices Found\nCheapest: **$${cheapest.toFixed(2)}** for ${routeLabel}\nAll prices found: ${uniquePrices.slice(0, 10).map(p => `$${p.toFixed(2)}`).join(', ')}`;
527
+ }
528
+ console.log(`[smart-search] Transit pricing fallback used: ${transitPrices.length} raw price hits, no structured verdict`);
529
+ }
530
+ else {
531
+ console.warn(`[smart-search] Transit search returned no usable prices or verdict for query: ${query}`);
532
+ }
533
+ }
534
+ catch (err) {
535
+ console.warn('[smart-search] Transit price search failed (non-fatal):', err.message);
536
+ }
537
+ }
538
+ else if (isEquipmentRental) {
539
+ try {
540
+ const pricingResults = await searchProvider.searchWeb(`${query} cost price per day rate 2025`, { count: 3 });
541
+ const prices = [];
542
+ for (const r of pricingResults) {
543
+ const text = `${r.title || ''} ${r.snippet || ''}`;
544
+ // Extract price ranges like "$140-$160 per day" or "$210 to $1,200"
545
+ const priceMatches = text.match(/\$[\d,]+(?:\s*[-โ€“to]+\s*\$[\d,]+)?(?:\s*(?:per|\/)\s*(?:day|week|month|hour))?/gi);
546
+ if (priceMatches) {
547
+ prices.push(...priceMatches.slice(0, 3));
548
+ }
549
+ }
550
+ if (prices.length > 0) {
551
+ pricingInfo = `\n\n## ๐Ÿ’ฐ Typical Pricing\n${[...new Set(prices)].slice(0, 6).map(p => `- ${p}`).join('\n')}`;
552
+ // Also add pricing snippets to the sources for AI to reference
553
+ for (const r of pricingResults.slice(0, 2)) {
554
+ if (r.snippet?.match(/\$/)) {
555
+ results.push({
556
+ title: r.title,
557
+ url: r.url,
558
+ snippet: r.snippet,
559
+ domain: getDomain(r.url),
560
+ content: r.snippet,
561
+ isPricing: true,
562
+ });
563
+ }
564
+ }
565
+ }
566
+ }
567
+ catch { /* pricing search failed โ€” non-fatal */ }
568
+ }
569
+ // If we found local businesses via Google Places, prepend them
570
+ if (localBusinesses.length > 0) {
571
+ const localContent = localBusinesses.map((b, i) => {
572
+ const status = b.isOpenNow ? '๐ŸŸข Open Now' : '๐Ÿ”ด Closed';
573
+ return `${i + 1}. **${b.name}** โญ${b.rating || '?'} (${b.reviewCount} reviews) โ€” ${status}${b.todayHours ? ` ยท ๐Ÿ• ${b.todayHours}` : ''}
574
+ ๐Ÿ“ ${b.address}${b.phone ? ` ยท ๐Ÿ“ž ${b.phone}` : ''}${b.website ? ` ยท [Website](${b.website})` : ''}${b.googleMapsUrl ? ` ยท [๐Ÿ“ Map](${b.googleMapsUrl})` : ''}`;
575
+ }).join('\n\n');
576
+ content = `## ๐Ÿ“ Nearby Businesses\n\n${localContent}${pricingInfo}\n\n---\n\n## ๐Ÿ” Web Results\n\n${content}`;
577
+ // Also add to results array for structured rendering
578
+ results.unshift(...localBusinesses.map((b, i) => ({
579
+ title: b.name,
580
+ url: b.googleMapsUrl || b.website || '#',
581
+ snippet: `โญ${b.rating || '?'} (${b.reviewCount} reviews) ยท ${b.isOpenNow ? '๐ŸŸข Open' : '๐Ÿ”ด Closed'}${b.todayHours ? ' ยท ' + b.todayHours : ''} ยท ${b.address}${b.phone ? ' ยท ๐Ÿ“ž ' + b.phone : ''}${b.fuelPrices && Object.keys(b.fuelPrices).length > 0 ? ' ยท โ›ฝ ' + Object.entries(b.fuelPrices).map(([type, price]) => type + ': ' + price + '/gal').join(' | ') : ''}`,
582
+ domain: 'google.com/maps',
583
+ rank: i + 1,
584
+ isLocalBusiness: true,
585
+ isOpenNow: b.isOpenNow,
586
+ ...(b.fuelPrices && Object.keys(b.fuelPrices).length > 0 ? { fuelPrices: b.fuelPrices } : {}),
587
+ })));
588
+ }
589
+ else if (pricingInfo) {
590
+ content = `${pricingInfo.trim()}\n\n---\n\n## ๐Ÿ” Web Results\n\n${content}`;
591
+ }
592
+ const extraPricingSources = results
593
+ .filter((r) => r.isPricing && r.url && (r.content || r.snippet))
594
+ .slice(0, 4)
595
+ .map((r, i) => ({
596
+ index: i + 1,
597
+ title: r.title,
598
+ url: r.url,
599
+ domain: r.domain || getDomain(r.url),
600
+ content: (r.content || r.snippet || '').slice(0, 800),
601
+ }));
602
+ // Build sources array from successfully peeled results plus extra pricing sources
603
+ const sources = [
604
+ ...enriched
605
+ .filter((s) => s.status === 'fulfilled' && s.value.content !== null)
606
+ .map((s) => {
607
+ const v = s.value;
608
+ return {
609
+ title: v.title,
610
+ url: v.url,
611
+ domain: getDomain(v.url),
612
+ };
613
+ }),
614
+ ...extraPricingSources.map((s) => ({
615
+ title: s.title,
616
+ url: s.url,
617
+ domain: s.domain,
618
+ })),
619
+ ].filter((source, index, arr) => arr.findIndex((s) => s.url === source.url) === index).slice(0, 8);
620
+ // โ”€โ”€ AI Synthesis (uses Groq/OpenAI/Glama/Ollama โ€” callLLMQuick picks best) โ”€โ”€
621
+ let answer;
622
+ let confidence;
623
+ let llmMs = 0;
624
+ // Only call LLM if at least one page was successfully peeled
625
+ if (anyPeelSucceeded) {
626
+ try {
627
+ // Build evidence context for the LLM using selective evidence aggregation
628
+ // (AttnRes-inspired: query-aware scoring, credibility weighting, domain diversity)
629
+ const evidenceSources = enriched
630
+ .filter(s => s.status === 'fulfilled' && s.value.content)
631
+ .map(s => {
632
+ const v = s.value;
633
+ const matchingResult = results.find((r) => r.url === v.url);
634
+ return {
635
+ url: v.url,
636
+ title: v.title || '',
637
+ content: v.content || '',
638
+ snippet: matchingResult?.snippet,
639
+ structured: v.structured,
640
+ metadata: v.metadata,
641
+ };
642
+ });
643
+ const evidenceResult = selectEvidence({
644
+ query,
645
+ sources: evidenceSources,
646
+ maxBlocks: 10,
647
+ maxChars: 4000,
648
+ });
649
+ const sourceContent = formatEvidenceForLLM(evidenceResult);
650
+ const systemPrompt = `${PROMPT_INJECTION_DEFENSE}Answer the query using these sources. Be specific with names, numbers, dates, and prices. Bold key facts. Cite sources inline as [1], [2], [3] etc. At the end, list Sources with their URLs. If sources disagree, note the difference.${isFactualQuery(query) ? ' IMPORTANT: For pricing/spec/limit questions, prefer official sources. If an official source appears only as a [snippet] fallback, treat it as weaker than a full successful fetch and avoid overclaiming confidence.' : ''}${isEquipmentRental ? ' IMPORTANT: Include specific rental prices/rates per day or week if available in the sources. Mention the cheapest option.' : ''}${isServiceBusiness ? ' IMPORTANT: Include business hours, phone numbers, and whether they are open now.' : ''}${isGasStation ? ' IMPORTANT: Include gas prices per gallon if available. Mention the cheapest station, its address, and current price. Sort by price.' : ''}${isTravelBooking ? ' IMPORTANT: List specific prices per person for different cruise lines/options. Format as a comparison: cruise line, ship name, duration, departure port, price. Sort cheapest first. Include dates if available.' : ''}${isTransitBooking ? ' IMPORTANT: This is a bus/train/ferry ticket query. Lead with the cheapest price found, the provider (e.g. FlixBus, Greyhound), route, and a direct link. If a round trip is implied, list cheapest outbound AND cheapest return separately, then total. Use ONLY prices that appear in the source data โ€” do NOT invent prices. If multiple providers have prices, compare them. Never say "check with bus companies directly" if you have concrete prices from the sources.' : ''} Max 200 words.`;
651
+ const truncatedSources = sourceContent.substring(0, 4000);
652
+ const userMessage = `Query: ${sanitizeSearchQuery(query)}\n\nSources:\n${truncatedSources}`;
653
+ const tLlm = Date.now();
654
+ const text = await callLLMQuick(`${systemPrompt}\n\n${userMessage}`, { maxTokens: 250, timeoutMs: 8000, temperature: 0.3 });
655
+ console.log(`[smart-search] LLM answered: ${text.length} chars`);
656
+ if (text) {
657
+ answer = text;
658
+ }
659
+ llmMs = Date.now() - tLlm;
660
+ // โ”€โ”€ Confidence scoring โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
661
+ // Compute confidence from USABLE evidence, not placeholder block pages.
662
+ const usablePeeledSources = enriched.filter((s) => {
663
+ if (s.status !== 'fulfilled')
664
+ return false;
665
+ const content = s.value.content;
666
+ return !isUnusableEvidenceContent(content);
667
+ });
668
+ const usableDomains = new Set(usablePeeledSources.map((s) => getDomain(s.value.url)));
669
+ const usableUrls = new Set(usablePeeledSources.map((s) => s.value.url));
670
+ const topOfficialCandidates = results.slice(0, 5).filter((r) => r.credibility?.tier === 'official' || r.credibility?.tier === 'established');
671
+ const hasUsableOfficialFetch = topOfficialCandidates.some((r) => usableUrls.has(r.url));
672
+ const hasOfficialSnippetFallback = topOfficialCandidates.some((r) => !usableUrls.has(r.url) && typeof r.snippet === 'string' && r.snippet.trim().length > 20);
673
+ if (isFactualQuery(query)) {
674
+ if (hasUsableOfficialFetch && usableDomains.size >= 2) {
675
+ confidence = 'HIGH';
676
+ }
677
+ else if ((hasUsableOfficialFetch || hasOfficialSnippetFallback) && usableDomains.size >= 1) {
678
+ confidence = 'MEDIUM';
679
+ }
680
+ else {
681
+ confidence = 'LOW';
682
+ }
683
+ }
684
+ else if (usableDomains.size >= 3 && hasUsableOfficialFetch) {
685
+ confidence = 'HIGH';
686
+ }
687
+ else if (usableDomains.size >= 2) {
688
+ confidence = 'MEDIUM';
689
+ }
690
+ else {
691
+ confidence = 'LOW';
692
+ }
693
+ }
694
+ catch (err) {
695
+ // Graceful degradation: LLM failure โ†’ return raw results without answer
696
+ console.warn('General search LLM synthesis failed (graceful fallback):', err.message);
697
+ }
698
+ }
699
+ const mapUrl = localBusinesses.length > 0 && GOOGLE_PLACES_KEY
700
+ ? `https://www.google.com/maps/embed/v1/search?q=${encodeURIComponent(query)}&key=${GOOGLE_PLACES_KEY}`
701
+ : undefined;
702
+ return {
703
+ type: 'general',
704
+ source: 'Web Search',
705
+ sourceUrl: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
706
+ content,
707
+ results,
708
+ tokens: content.split(/\s+/).length,
709
+ fetchTimeMs: Date.now() - t0,
710
+ ...(answer !== undefined ? { answer } : {}),
711
+ ...(confidence !== undefined ? { confidence } : {}),
712
+ ...(sources.length > 0 ? { sources } : {}),
713
+ timing: { searchMs, peelMs, llmMs },
714
+ ...(mapUrl ? { mapUrl } : {}),
715
+ ...(transitVerdict ? { verdict: transitVerdict } : {}),
716
+ };
717
+ }