@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,2 @@
1
+ import type { SearchIntent, SmartSearchResult } from '../types.js';
2
+ export declare function handleRentalSearch(intent: SearchIntent): Promise<SmartSearchResult>;
@@ -0,0 +1,154 @@
1
+ import { getBestSearchProvider } from '../../../../core/search-provider.js';
2
+ import { addAffiliateTag, parsePrice, extractPriceValue } from '../utils.js';
3
+ import { callLLMQuick, PROMPT_INJECTION_DEFENSE } from '../llm.js';
4
+ export async function handleRentalSearch(intent) {
5
+ const t0 = Date.now();
6
+ // Extract location from query
7
+ const locMatch = intent.query.match(/\b(?:in|at|near|from|around)\s+(.+?)(?:\s+(?:for|under|from|to|between|\$|cheap|best).*)?$/i);
8
+ const location = locMatch ? locMatch[1].trim() : '';
9
+ // Extract dates if present
10
+ const dateMatch = intent.query.match(/(?:from|between)\s+(\w+\s+\d+)\s+(?:to|and|through|-)\s+(\w+\s+\d+)/i);
11
+ const dates = dateMatch ? { from: dateMatch[1], to: dateMatch[2] } : null;
12
+ // Extract budget if present
13
+ const budgetMatch = intent.query.match(/(?:under|\$|budget|max|cheaper than)\s*\$?(\d+)/i);
14
+ const budget = budgetMatch ? budgetMatch[1] : null;
15
+ const { provider: searchProvider } = getBestSearchProvider();
16
+ // Search for aggregator results that include prices + Reddit tips
17
+ const [aggregatorSettled, turoSettled, redditSettled] = await Promise.allSettled([
18
+ searchProvider.searchWeb(`car rental ${location || 'near me'} ${dates ? `${dates.from} to ${dates.to}` : ''} price cheapest site:kayak.com OR site:priceline.com OR site:expedia.com`, { count: 8 }),
19
+ searchProvider.searchWeb(`car rental ${location || ''} site:turo.com OR site:enterprise.com OR site:hertz.com`, { count: 3 }),
20
+ searchProvider.searchWeb(`car rental ${location || ''} reddit tips best deal cheapest`, { count: 2 }),
21
+ ]);
22
+ const rentalResults = [
23
+ ...(aggregatorSettled.status === 'fulfilled' ? aggregatorSettled.value : []),
24
+ ...(turoSettled.status === 'fulfilled' ? turoSettled.value : []),
25
+ ];
26
+ const redditResults = redditSettled.status === 'fulfilled' ? redditSettled.value : [];
27
+ // Known aggregators and direct providers
28
+ const RENTAL_SITES = {
29
+ 'kayak.com': { name: 'Kayak', type: 'aggregator' },
30
+ 'priceline.com': { name: 'Priceline', type: 'aggregator' },
31
+ 'cheapflights.com': { name: 'Cheapflights', type: 'aggregator' },
32
+ 'momondo.com': { name: 'Momondo', type: 'aggregator' },
33
+ 'skyscanner.com': { name: 'Skyscanner', type: 'aggregator' },
34
+ 'trip.com': { name: 'Trip.com', type: 'aggregator' },
35
+ 'carrentals.com': { name: 'CarRentals.com', type: 'aggregator' },
36
+ 'rentalcars.com': { name: 'RentalCars.com', type: 'aggregator' },
37
+ 'stressfreecarrental.com': { name: 'StressFree', type: 'aggregator' },
38
+ 'happycar.com': { name: 'HappyCar', type: 'aggregator' },
39
+ 'enterprise.com': { name: 'Enterprise', type: 'direct' },
40
+ 'hertz.com': { name: 'Hertz', type: 'direct' },
41
+ 'avis.com': { name: 'Avis', type: 'direct' },
42
+ 'budget.com': { name: 'Budget', type: 'direct' },
43
+ 'turo.com': { name: 'Turo', type: 'direct' },
44
+ 'sixt.com': { name: 'Sixt', type: 'direct' },
45
+ 'nationalcar.com': { name: 'National', type: 'direct' },
46
+ 'alamo.com': { name: 'Alamo', type: 'direct' },
47
+ 'costcotravel.com': { name: 'Costco Travel', type: 'direct' },
48
+ 'expedia.com': { name: 'Expedia', type: 'aggregator' },
49
+ };
50
+ const getSiteInfo = (url) => {
51
+ try {
52
+ const hostname = new URL(url).hostname.replace('www.', '');
53
+ for (const [domain, info] of Object.entries(RENTAL_SITES)) {
54
+ if (hostname === domain || hostname.endsWith('.' + domain)) {
55
+ return { company: info.name, siteType: info.type };
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ };
64
+ // Deduplicate by company — keep the most location-specific URL per company
65
+ const seen = new Map();
66
+ for (const r of rentalResults) {
67
+ const siteInfo = getSiteInfo(r.url);
68
+ if (!siteInfo)
69
+ continue;
70
+ const existing = seen.get(siteInfo.company);
71
+ // Prefer URLs that mention the location (more specific = better)
72
+ const locLower = (location || '').toLowerCase().replace(/\s+/g, '');
73
+ const urlLower = r.url.toLowerCase().replace(/[\s-]/g, '');
74
+ const isLocationSpecific = locLower && urlLower.includes(locLower.substring(0, 5));
75
+ if (!existing || isLocationSpecific) {
76
+ seen.set(siteInfo.company, r);
77
+ }
78
+ }
79
+ const listings = [...seen.entries()]
80
+ .map(([company, r]) => {
81
+ const siteInfo = getSiteInfo(r.url);
82
+ // Extract price from BOTH title and snippet; prefer title (more prominent = more accurate)
83
+ const titlePrice = parsePrice(r.title || '');
84
+ const snippetPrice = parsePrice(r.snippet || '');
85
+ const price = titlePrice || snippetPrice;
86
+ const priceValue = extractPriceValue(price);
87
+ return {
88
+ name: r.title?.replace(/\s*[-|–—].*$/, '').trim() || `${company} Car Rental`,
89
+ company,
90
+ siteType: siteInfo.siteType,
91
+ url: addAffiliateTag(r.url),
92
+ snippet: r.snippet || '',
93
+ price,
94
+ priceValue,
95
+ };
96
+ });
97
+ // Sort: aggregators with prices first (lowest price first), then aggregators without prices, then direct providers
98
+ listings.sort((a, b) => {
99
+ const aVal = a.priceValue ?? Infinity;
100
+ const bVal = b.priceValue ?? Infinity;
101
+ if (aVal !== bVal)
102
+ return aVal - bVal;
103
+ if (a.siteType !== b.siteType)
104
+ return a.siteType === 'aggregator' ? -1 : 1;
105
+ return 0;
106
+ });
107
+ const topListings = listings.slice(0, 6);
108
+ // Also add direct booking links for major providers if they didn't appear in search
109
+ const searchLocation = encodeURIComponent(location || 'New York');
110
+ const directLinks = [
111
+ { company: 'Kayak', siteType: 'aggregator', url: `https://www.kayak.com/cars/${searchLocation}`, name: 'Compare all rental companies' },
112
+ { company: 'Enterprise', siteType: 'direct', url: `https://www.enterprise.com/en/car-rental/locations/us.html`, name: 'Enterprise Rent-A-Car' },
113
+ { company: 'Hertz', siteType: 'direct', url: `https://www.hertz.com/rentacar/reservation/`, name: 'Hertz Car Rental' },
114
+ { company: 'Avis', siteType: 'direct', url: `https://www.avis.com/en/home`, name: 'Avis Car Rental' },
115
+ { company: 'Budget', siteType: 'direct', url: `https://www.budget.com/en/home`, name: 'Budget Car Rental' },
116
+ ].filter(d => !topListings.some(l => l.company === d.company));
117
+ const allListings = [
118
+ ...topListings,
119
+ ...directLinks.map(d => ({ ...d, snippet: '', price: undefined, priceValue: undefined })),
120
+ ];
121
+ // Build markdown content
122
+ const content = `# 🔑 Car Rentals${location ? ` — ${location}` : ''}${dates ? ` (${dates.from} to ${dates.to})` : ''}\n\n` +
123
+ allListings.map((l, i) => `${i + 1}. **${l.name}** — ${l.company}${l.price ? ` · ${l.price}/day` : ''}${l.siteType === 'aggregator' ? ' *(compares prices)*' : ''}\n ${l.snippet}`).join('\n\n');
124
+ // AI synthesis: use extracted prices + Reddit tips
125
+ let answer;
126
+ try {
127
+ const priceInfo = allListings.filter(l => l.price).map(l => `${l.company}: ${l.price}/day`).join(', ');
128
+ const redditContent = redditResults.slice(0, 3).map(r => `${r.title}: ${r.snippet || ''}`).join('\n');
129
+ const aiPrompt = `${PROMPT_INJECTION_DEFENSE}You are a car rental advisor. ONLY use information from the sources below. User wants to rent a car${location ? ' in ' + location : ''}.${dates ? ` Dates: ${dates.from} to ${dates.to}.` : ''}${budget ? ` Budget: $${budget}/day.` : ''} Prices found: ${priceInfo || 'no prices extracted yet — refer to sites below'}. Reddit tips: ${redditContent || 'none'}. Give a 2-3 sentence recommendation based ONLY on sources. Mention the cheapest option and actual price. Max 200 words. Cite sources inline as [1], [2], [3].`;
130
+ const aiText = await callLLMQuick(aiPrompt, { maxTokens: 250, timeoutMs: 8000, temperature: 0.3 });
131
+ if (aiText && aiText.length > 20)
132
+ answer = aiText;
133
+ }
134
+ catch (err) {
135
+ console.warn('[rental-search] LLM synthesis failed (graceful fallback):', err.message);
136
+ }
137
+ return {
138
+ type: 'rental',
139
+ source: 'Car Rentals + Reddit',
140
+ sourceUrl: `https://www.kayak.com/cars/${searchLocation}`,
141
+ content,
142
+ title: `Car Rentals${location ? ` in ${location}` : ''}`,
143
+ structured: { listings: allListings },
144
+ tokens: content.split(/\s+/).length,
145
+ fetchTimeMs: Date.now() - t0,
146
+ loadingMessage: 'Searching for rental cars...',
147
+ ...(answer !== undefined ? { answer } : {}),
148
+ sources: [
149
+ { type: 'rental', count: topListings.length },
150
+ { type: 'reddit', threads: redditResults.slice(0, 3).map(r => ({ title: r.title, url: r.url, snippet: r.snippet })) },
151
+ ],
152
+ };
153
+ }
154
+ // ─── Restaurant source fetchers ───────────────────────────────────────────
@@ -0,0 +1,2 @@
1
+ import type { SearchIntent, SmartSearchResult } from '../types.js';
2
+ export declare function handleRestaurantSearch(intent: SearchIntent, requestLanguage?: string): Promise<SmartSearchResult>;
@@ -0,0 +1,225 @@
1
+ import { localSearch } from '../../../../core/local-search.js';
2
+ import { callLLMQuick, sanitizeSearchQuery, PROMPT_INJECTION_DEFENSE } from '../llm.js';
3
+ import { fetchYelpResults } from '../sources/yelp.js';
4
+ import { fetchRedditResults } from '../sources/reddit.js';
5
+ import { fetchYouTubeResults } from '../sources/youtube.js';
6
+ export async function handleRestaurantSearch(intent, requestLanguage) {
7
+ const t0 = Date.now();
8
+ const location = intent.params.location || 'New York, NY';
9
+ const keyword = intent.query
10
+ .replace(/\b(best|top|good|cheap|affordable|near me|near|around|in|find|search|looking for)\b/gi, '')
11
+ .replace(/\s+/g, ' ')
12
+ .trim();
13
+ // ── Run ALL data sources in parallel for speed ──────────────────────────
14
+ // Previously sequential: localSearch → Yelp → Reddit+YouTube = 20-30s
15
+ // Now parallel: everything races at once = 8-10s max
16
+ const hasPlacesKey = !!process.env.GOOGLE_PLACES_API_KEY;
17
+ const [localSearchSettled, yelpSettled, redditSettled, youtubeSettled] = await Promise.allSettled([
18
+ // Google Places (primary when key available)
19
+ hasPlacesKey
20
+ ? Promise.race([
21
+ localSearch({ query: keyword || intent.query, location, language: requestLanguage, limit: 10 }),
22
+ new Promise((_, rej) => setTimeout(() => rej(new Error('local search timeout')), 8000)),
23
+ ])
24
+ : Promise.resolve(null),
25
+ // Yelp (secondary / fallback)
26
+ Promise.race([
27
+ fetchYelpResults(keyword, location).then(v => v),
28
+ new Promise((_, rej) => setTimeout(() => rej(new Error('yelp timeout')), 8000)),
29
+ ]),
30
+ // Reddit (best-effort supplementary)
31
+ Promise.race([
32
+ fetchRedditResults(keyword, location),
33
+ new Promise((_, rej) => setTimeout(() => rej(new Error('reddit timeout')), 6000)),
34
+ ]),
35
+ // YouTube (best-effort supplementary)
36
+ Promise.race([
37
+ fetchYouTubeResults(keyword, location),
38
+ new Promise((_, rej) => setTimeout(() => rej(new Error('youtube timeout')), 5000)),
39
+ ]),
40
+ ]);
41
+ const googlePlacesData = localSearchSettled.status === 'fulfilled' ? localSearchSettled.value : null;
42
+ if (googlePlacesData && googlePlacesData.results?.length > 0) {
43
+ console.log(`[smart-search] localSearch() returned ${googlePlacesData.results.length} results from ${googlePlacesData.source}`);
44
+ }
45
+ // Skip Yelp data if Google Places already has enough results
46
+ const skipYelp = googlePlacesData && googlePlacesData.results?.length >= 5;
47
+ const yelpData = (!skipYelp && yelpSettled.status === 'fulfilled') ? yelpSettled.value : null;
48
+ const redditData = redditSettled.status === 'fulfilled' ? redditSettled.value : null;
49
+ const youtubeData = youtubeSettled.status === 'fulfilled' ? youtubeSettled.value : null;
50
+ // Re-rank: composite score = rating * log2(reviewCount + 1)
51
+ // This naturally surfaces high-rated places with meaningful review volume
52
+ if (yelpData && yelpData.businesses.length > 0) {
53
+ yelpData.businesses.sort((a, b) => {
54
+ const scoreA = (a.rating || 0) * Math.log2((a.reviewCount || 0) + 1);
55
+ const scoreB = (b.rating || 0) * Math.log2((b.reviewCount || 0) + 1);
56
+ return scoreB - scoreA;
57
+ });
58
+ // For "best" queries, filter to minimum 50 reviews
59
+ const isBestQuery = /\b(best|top|highest rated)\b/i.test(intent.query);
60
+ if (isBestQuery) {
61
+ const filtered = yelpData.businesses.filter((b) => (b.reviewCount || 0) >= 50);
62
+ if (filtered.length >= 3) {
63
+ yelpData.businesses = filtered;
64
+ }
65
+ }
66
+ // Remove permanently closed businesses
67
+ yelpData.businesses = yelpData.businesses.filter((b) => !b.isClosed);
68
+ }
69
+ // ── Build markdown content from all sources ──────────────────────────
70
+ const contentParts = [];
71
+ // Google Places section (shown first when available — higher quality data)
72
+ if (googlePlacesData && googlePlacesData.results.length > 0) {
73
+ const priceLevelStr = (lvl) => lvl !== undefined ? '$'.repeat(Math.max(1, lvl)) : '';
74
+ contentParts.push(`## Google Places (${googlePlacesData.results.length} results)`);
75
+ googlePlacesData.results.slice(0, 10).forEach((b, i) => {
76
+ const name = b.name || 'Unknown';
77
+ const rating = b.rating ? `⭐${b.rating}` : '';
78
+ const reviews = b.reviewCount ? `(${b.reviewCount.toLocaleString()} reviews)` : '';
79
+ const price = b.priceLevel !== undefined ? ` · ${priceLevelStr(b.priceLevel)}` : '';
80
+ const openStatus = b.isOpen === true ? ' · 🟢 Open Now' : (b.isOpen === false ? ' · 🔴 Closed' : '');
81
+ const todayHours = b.hours?.length > 0 ? ` · 🕐 ${b.hours[0]}` : '';
82
+ const mapsLink = b.googleMapsUrl ? ` · [📍 Maps](${b.googleMapsUrl})` : '';
83
+ const addr = b.address || '';
84
+ contentParts.push(`${i + 1}. **${name}** ${rating} ${reviews}${price}${openStatus}${todayHours}${mapsLink}${addr ? ` — ${addr}` : ''}`);
85
+ });
86
+ contentParts.push('');
87
+ }
88
+ // Yelp section
89
+ if (yelpData) {
90
+ const businesses = yelpData.businesses;
91
+ if (businesses.length > 0) {
92
+ contentParts.push(`## Yelp (${businesses.length} restaurants)`);
93
+ businesses.slice(0, 10).forEach((b, i) => {
94
+ const name = b.name || b.title || 'Unknown';
95
+ const rating = b.rating ? `⭐${b.rating}` : '';
96
+ const reviews = b.reviewCount ? `(${b.reviewCount.toLocaleString()} reviews)` : '';
97
+ const address = b.address || b.location || '';
98
+ const price = b.price ? ` · ${b.price}` : '';
99
+ const openStatus = b.isClosed ? ' · ⛔ Permanently Closed' : (b.isOpenNow ? ' · 🟢 Open Now' : ' · 🔴 Closed');
100
+ const todayHours = b.todayHours && b.todayHours !== 'Closed today' ? ` · 🕐 ${b.todayHours}` : (b.todayHours === 'Closed today' ? ' · 🕐 Closed today' : '');
101
+ const txns = b.transactions?.length > 0 ? ` · ${b.transactions.map((t) => t === 'delivery' ? '🚗 Delivery' : t === 'pickup' ? '📦 Pickup' : t).join(' ')}` : '';
102
+ const mapsLink = b.googleMapsUrl ? ` · [📍 Google Maps](${b.googleMapsUrl})` : '';
103
+ contentParts.push(`${i + 1}. **${name}** ${rating} ${reviews}${price}${openStatus}${todayHours}${txns}${mapsLink}${address ? ` — ${address}` : ''}`);
104
+ });
105
+ }
106
+ else if (yelpData.content) {
107
+ contentParts.push(`## Yelp\n${yelpData.content.substring(0, 800)}`);
108
+ }
109
+ }
110
+ // Reddit section
111
+ if (redditData) {
112
+ contentParts.push('');
113
+ contentParts.push('## Reddit Recommendations');
114
+ if (redditData.thread) {
115
+ contentParts.push(`**${redditData.thread.title}**`);
116
+ if (redditData.thread.content) {
117
+ contentParts.push(redditData.thread.content.substring(0, 600));
118
+ }
119
+ }
120
+ if (redditData.otherThreads.length > 0) {
121
+ contentParts.push('');
122
+ redditData.otherThreads.slice(0, 3).forEach(t => {
123
+ contentParts.push(`- [${t.title}](${t.url}) — ${t.snippet || ''}`);
124
+ });
125
+ }
126
+ }
127
+ // YouTube section
128
+ if (youtubeData && youtubeData.videos.length > 0) {
129
+ contentParts.push('');
130
+ contentParts.push('## YouTube Reviews');
131
+ youtubeData.videos.forEach(v => {
132
+ contentParts.push(`🎬 [${v.title}](${v.url}) — ${v.snippet || ''}`);
133
+ });
134
+ }
135
+ const combinedContent = contentParts.join('\n');
136
+ // ── Build sources array for dashboard tabs ────────────────────────────
137
+ const sources = [];
138
+ if (googlePlacesData)
139
+ sources.push({ title: 'Google Places', url: `https://maps.google.com/?q=${encodeURIComponent(keyword + ' ' + location)}`, domain: 'google.com' });
140
+ if (yelpData)
141
+ sources.push({ title: 'Yelp', url: yelpData.url, domain: 'yelp.com' });
142
+ if (redditData?.thread)
143
+ sources.push({ title: redditData.thread.title, url: redditData.thread.url, domain: 'reddit.com' });
144
+ if (youtubeData?.videos[0])
145
+ sources.push({ title: youtubeData.videos[0].title, url: youtubeData.videos[0].url, domain: 'youtube.com' });
146
+ // ── AI Synthesis (uses Groq/OpenAI/Glama/Ollama — callLLMQuick picks best) ──
147
+ // Uses whichever data source returned results: Google Places OR Yelp
148
+ // NOTE: K8s OLLAMA_URL is port 11435 but Ollama runs on 11434 — fix in K8s secrets
149
+ let answer;
150
+ // Build restaurant lines from whichever source has data
151
+ const hasGoogleData = googlePlacesData && googlePlacesData.results?.length > 0;
152
+ const hasYelpData = yelpData && yelpData.businesses?.length > 0;
153
+ if (hasGoogleData || hasYelpData) {
154
+ try {
155
+ let restaurantLines;
156
+ let citations;
157
+ if (hasGoogleData) {
158
+ // Format Google Places results for LLM
159
+ const priceLevelStr = (lvl) => lvl !== undefined ? '$'.repeat(Math.max(1, lvl)) : '';
160
+ restaurantLines = googlePlacesData.results.slice(0, 3).map((b, i) => {
161
+ const openStatus = b.isOpen === true ? 'OPEN NOW' : (b.isOpen === false ? 'Closed right now' : 'hours unknown');
162
+ const hours = b.hours?.length > 0 ? b.hours[0] : 'not available';
163
+ const price = b.priceLevel !== undefined ? priceLevelStr(b.priceLevel) : '';
164
+ return `[${i + 1}] ${b.name} ⭐${b.rating || '?'} (${(b.reviewCount || 0).toLocaleString()} reviews) ${price} — ${b.address || ''}
165
+ ${openStatus} | Today: ${hours} | Categories: ${b.categories || b.types?.join(', ') || ''}
166
+ URL: ${b.googleMapsUrl || 'google.com/maps'}`;
167
+ }).join('\n');
168
+ citations = googlePlacesData.results.slice(0, 3).map((b, i) => `[${i + 1}] ${b.googleMapsUrl || 'google.com/maps'}`).join('\n');
169
+ }
170
+ else {
171
+ // Format Yelp results for LLM
172
+ restaurantLines = yelpData.businesses.slice(0, 3).map((b, i) => {
173
+ const openStatus = b.isClosed ? 'PERMANENTLY CLOSED' : (b.isOpenNow ? 'OPEN NOW' : 'Closed right now');
174
+ const txns = b.transactions?.length > 0 ? `Available: ${b.transactions.join(', ')}` : '';
175
+ const googleInfo = b.googleRating ? ` | Google: ⭐${b.googleRating} (${b.googleReviewCount} reviews)` : '';
176
+ return `[${i + 1}] ${b.name} ⭐${b.rating} (${b.reviewCount?.toLocaleString()} reviews) ${b.price || ''} — ${b.address}
177
+ ${openStatus} | Today: ${b.todayHours || 'hours not available'} | ${txns} | Categories: ${b.categories || ''}${googleInfo}
178
+ URL: ${b.url || ''}`;
179
+ }).join('\n');
180
+ citations = yelpData.businesses.slice(0, 3).map((b, i) => `[${i + 1}] ${b.url || 'yelp.com'}`).join('\n');
181
+ }
182
+ const redditHint = redditData?.otherThreads?.slice(0, 2).map((t) => t.title).join('; ') || '';
183
+ const systemPrompt = `${PROMPT_INJECTION_DEFENSE}Recommend top 3 restaurants. For each: name with inline citation [1][2][3], why it's good, open/closed status, hours.
184
+ Cite sources inline using [1], [2], [3] notation matching the numbered sources. At the end, list Sources with their URLs.
185
+ Be specific. Max 200 words.
186
+ `;
187
+ const userMessage = `Query: ${sanitizeSearchQuery(intent.query)}\n\nTop restaurants:\n${restaurantLines}${redditHint ? '\n\nReddit mentions: ' + redditHint : ''}\n\nSources:\n${citations}`;
188
+ const text = await callLLMQuick(`${systemPrompt}\n\n${userMessage}`, { maxTokens: 250, timeoutMs: 8000, temperature: 0.3 });
189
+ if (text)
190
+ answer = text;
191
+ }
192
+ catch (err) {
193
+ console.warn('[restaurant-search] LLM synthesis failed (graceful fallback):', err.message);
194
+ }
195
+ }
196
+ // If ALL sources completely failed, surface an error
197
+ if (!googlePlacesData && !yelpData && !redditData && !youtubeData) {
198
+ throw new Error('All restaurant sources failed');
199
+ }
200
+ const yelpUrl = yelpData?.url || `https://www.yelp.com/search?find_desc=${encodeURIComponent(keyword)}&find_loc=${encodeURIComponent(location)}`;
201
+ // Build source label based on what we actually used
202
+ const sourceLabel = [
203
+ googlePlacesData ? 'Google Places' : null,
204
+ yelpData ? 'Yelp' : null,
205
+ redditData ? 'Reddit' : null,
206
+ youtubeData ? 'YouTube' : null,
207
+ ].filter(Boolean).join(' + ') || 'Yelp + Reddit + YouTube';
208
+ // Merge structured data: prefer Google Places, fall back to Yelp
209
+ const structuredData = googlePlacesData
210
+ ? { businesses: googlePlacesData.results, googlePlaces: true }
211
+ : yelpData?.domainData?.structured;
212
+ return {
213
+ type: 'restaurants',
214
+ source: sourceLabel,
215
+ sourceUrl: yelpUrl,
216
+ content: combinedContent,
217
+ title: `${keyword} in ${location}`,
218
+ domainData: googlePlacesData ? { structured: structuredData } : yelpData?.domainData,
219
+ structured: structuredData,
220
+ tokens: combinedContent.split(/\s+/).length,
221
+ fetchTimeMs: Date.now() - t0,
222
+ ...(answer !== undefined ? { answer } : {}),
223
+ ...(sources.length > 0 ? { sources } : {}),
224
+ };
225
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Transit Verdict Builder
3
+ *
4
+ * Extracts structured pricing data from search results and peeled transit
5
+ * booking pages, then produces a TransactionalVerdict that the UI can render
6
+ * directly — no markdown parsing needed.
7
+ *
8
+ * Design principles:
9
+ * 1. Only use prices found in real source data (never fabricate).
10
+ * 2. Prefer booking/comparison sites (wanderu, flixbus, busbud, rome2rio,
11
+ * greyhound, amtrak) over generic snippets.
12
+ * 3. Deduplicate by provider+price so the UI gets clean data.
13
+ * 4. Return round-trip totals only when both legs have real data.
14
+ */
15
+ import type { TransactionalVerdict } from '../types.js';
16
+ export interface TransitSourceResult {
17
+ url: string;
18
+ domain: string;
19
+ title: string;
20
+ content: string;
21
+ snippet: string;
22
+ isTransitSource?: boolean;
23
+ legHint?: 'outbound' | 'return' | 'unknown';
24
+ }
25
+ export interface BuildTransitVerdictInput {
26
+ query: string;
27
+ transitSources: TransitSourceResult[];
28
+ parsedQuery: {
29
+ origin: string;
30
+ destination: string;
31
+ departDate: string;
32
+ returnDate: string;
33
+ isRoundTrip: boolean;
34
+ mode: string;
35
+ };
36
+ }
37
+ /**
38
+ * Build a TransactionalVerdict from transit search results.
39
+ * Returns null if no usable prices were found.
40
+ */
41
+ export declare function buildTransitVerdict(input: BuildTransitVerdictInput): TransactionalVerdict | null;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Transit Verdict Builder
3
+ *
4
+ * Extracts structured pricing data from search results and peeled transit
5
+ * booking pages, then produces a TransactionalVerdict that the UI can render
6
+ * directly — no markdown parsing needed.
7
+ *
8
+ * Design principles:
9
+ * 1. Only use prices found in real source data (never fabricate).
10
+ * 2. Prefer booking/comparison sites (wanderu, flixbus, busbud, rome2rio,
11
+ * greyhound, amtrak) over generic snippets.
12
+ * 3. Deduplicate by provider+price so the UI gets clean data.
13
+ * 4. Return round-trip totals only when both legs have real data.
14
+ */
15
+ /** Known booking domains — results from these are higher-trust for pricing */
16
+ const BOOKING_DOMAINS = new Set([
17
+ 'wanderu.com', 'flixbus.com', 'greyhound.com', 'busbud.com',
18
+ 'amtrak.com', 'rome2rio.com', 'megabus.com', 'ourbus.com',
19
+ 'gotobus.com', 'trailways.com', 'peterpanbus.com', 'coachrun.com',
20
+ ]);
21
+ /** Provider name normalization map */
22
+ const PROVIDER_NAMES = {
23
+ flixbus: 'FlixBus',
24
+ greyhound: 'Greyhound',
25
+ megabus: 'Megabus',
26
+ amtrak: 'Amtrak',
27
+ ourbus: 'OurBus',
28
+ wanderu: 'Wanderu',
29
+ busbud: 'Busbud',
30
+ rome2rio: 'Rome2Rio',
31
+ peterpanbus: 'Peter Pan Bus',
32
+ peterpan: 'Peter Pan Bus',
33
+ gotobus: 'GotoBus',
34
+ coachrun: 'CoachRun',
35
+ trailways: 'Trailways',
36
+ boltbus: 'BoltBus',
37
+ vamoose: 'Vamoose',
38
+ };
39
+ /**
40
+ * Extract price-provider pairs from a transit source page.
41
+ *
42
+ * Tries multiple heuristics:
43
+ * 1. "$XX on Provider" / "Provider from $XX" / "Provider: $XX"
44
+ * 2. Lines with a known provider name + a dollar amount
45
+ * 3. Plain dollar amounts with domain-based provider attribution
46
+ */
47
+ function extractPricesFromSource(source, leg) {
48
+ const prices = [];
49
+ const text = `${source.title} ${source.snippet} ${source.content}`;
50
+ const isBooking = BOOKING_DOMAINS.has(source.domain);
51
+ // Guess provider from domain
52
+ const domainProvider = source.domain.replace(/\.com$|\.net$|\.org$/, '');
53
+ const fallbackProvider = PROVIDER_NAMES[domainProvider] || titleCase(domainProvider);
54
+ // ── Strategy 1: "$XX … provider" or "provider … $XX" patterns ──
55
+ // Match lines like: "FlixBus from $19.99" or "$19 on Greyhound"
56
+ const providerKeys = Object.keys(PROVIDER_NAMES).join('|');
57
+ const providerPriceRe = new RegExp(`(?:(${providerKeys})[^$]{0,30}\\$(\\d+(?:\\.\\d{1,2})?))|(?:\\$(\\d+(?:\\.\\d{1,2})?)[^\\n]{0,30}(${providerKeys}))`, 'gi');
58
+ let m;
59
+ while ((m = providerPriceRe.exec(text)) !== null) {
60
+ const providerKey = (m[1] || m[4] || '').toLowerCase();
61
+ const priceStr = m[2] || m[3];
62
+ const price = parseFloat(priceStr);
63
+ if (price > 0 && price < 1000 && PROVIDER_NAMES[providerKey]) {
64
+ prices.push({
65
+ provider: PROVIDER_NAMES[providerKey],
66
+ price,
67
+ currency: 'USD',
68
+ url: source.url,
69
+ isBookingSite: isBooking,
70
+ leg,
71
+ });
72
+ }
73
+ }
74
+ // ── Strategy 2: Plain dollar amounts — attribute to domain provider ──
75
+ if (prices.length === 0) {
76
+ const plainPrices = text.match(/\$(\d+(?:\.\d{1,2})?)/g);
77
+ if (plainPrices) {
78
+ for (const raw of plainPrices) {
79
+ const price = parseFloat(raw.replace('$', ''));
80
+ if (price > 0 && price < 1000) {
81
+ prices.push({
82
+ provider: fallbackProvider,
83
+ price,
84
+ currency: 'USD',
85
+ url: source.url,
86
+ isBookingSite: isBooking,
87
+ leg,
88
+ });
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return prices;
94
+ }
95
+ function titleCase(s) {
96
+ return s
97
+ .split(/\s+/)
98
+ .filter(Boolean)
99
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
100
+ .join(' ');
101
+ }
102
+ /**
103
+ * Deduplicate prices: keep lowest price per provider.
104
+ */
105
+ function dedup(prices) {
106
+ const map = new Map();
107
+ for (const p of prices) {
108
+ const key = `${p.provider.toLowerCase()}|${p.leg}`;
109
+ const existing = map.get(key);
110
+ if (!existing || p.price < existing.price) {
111
+ map.set(key, p);
112
+ }
113
+ }
114
+ return [...map.values()].sort((a, b) => a.price - b.price);
115
+ }
116
+ /**
117
+ * Build a TransactionalVerdict from transit search results.
118
+ * Returns null if no usable prices were found.
119
+ */
120
+ export function buildTransitVerdict(input) {
121
+ const { transitSources, parsedQuery } = input;
122
+ const { origin, destination, isRoundTrip, mode, departDate, returnDate } = parsedQuery;
123
+ if (transitSources.length === 0)
124
+ return null;
125
+ // ── Extract prices from all sources ──
126
+ const allPrices = [];
127
+ for (const src of transitSources) {
128
+ // Determine leg: if the source URL/content suggests a return route, tag it
129
+ const isReturnLeg = src.legHint === 'return' || (origin && destination &&
130
+ (src.content.toLowerCase().includes(`${destination.toLowerCase()} to ${origin.toLowerCase()}`) ||
131
+ src.title.toLowerCase().includes(`${destination.toLowerCase()} to ${origin.toLowerCase()}`)));
132
+ const leg = src.legHint || (isReturnLeg ? 'return' : (origin && destination ? 'outbound' : 'unknown'));
133
+ allPrices.push(...extractPricesFromSource(src, leg));
134
+ }
135
+ if (allPrices.length === 0)
136
+ return null;
137
+ // ── Deduplicate ──
138
+ const uniquePrices = dedup(allPrices);
139
+ // ── Separate by leg ──
140
+ const outbound = uniquePrices.filter(p => p.leg === 'outbound' || p.leg === 'unknown');
141
+ const returnLeg = uniquePrices.filter(p => p.leg === 'return');
142
+ // ── Build options ──
143
+ const toOption = (p) => ({
144
+ provider: p.provider,
145
+ price: p.price,
146
+ currency: p.currency,
147
+ route: origin && destination
148
+ ? `${titleCase(origin)} → ${titleCase(destination)}`
149
+ : undefined,
150
+ url: p.url,
151
+ notes: p.isBookingSite ? 'Booking site' : 'Price from search results',
152
+ });
153
+ // Best = cheapest from booking sites, else cheapest overall
154
+ const bookingPrices = outbound.filter(p => p.isBookingSite);
155
+ const best = bookingPrices.length > 0 ? bookingPrices[0] : outbound[0];
156
+ if (!best)
157
+ return null;
158
+ const bestOption = toOption(best);
159
+ const maxReasonableAltPrice = Math.max(best.price * 3, best.price + 75);
160
+ const alternatives = outbound
161
+ .filter(p => !(p.provider === best.provider && p.price === best.price))
162
+ .filter(p => p.price <= maxReasonableAltPrice)
163
+ .slice(0, 5)
164
+ .map(toOption);
165
+ // ── Route label ──
166
+ const routeLabel = origin && destination
167
+ ? `${titleCase(origin)} → ${titleCase(destination)}`
168
+ : 'this route';
169
+ // ── Headline ──
170
+ const headline = `Cheapest I found is $${best.price.toFixed(2)} on ${best.provider} for ${routeLabel}`;
171
+ // ── Totals ──
172
+ let totals;
173
+ if (isRoundTrip) {
174
+ const returnBest = returnLeg.length > 0
175
+ ? returnLeg[0]
176
+ : null;
177
+ totals = {
178
+ oneWayLowest: best.price,
179
+ currency: 'USD',
180
+ };
181
+ if (returnBest) {
182
+ totals.returnLowest = returnBest.price;
183
+ totals.roundTripLowest = best.price + returnBest.price;
184
+ }
185
+ }
186
+ // ── Confidence ──
187
+ const bookingSourceCount = new Set(uniquePrices.filter(p => p.isBookingSite).map(p => p.provider.toLowerCase())).size;
188
+ let confidence = 'LOW';
189
+ if (bookingSourceCount >= 2 && uniquePrices.length >= 3) {
190
+ confidence = 'HIGH';
191
+ }
192
+ else if (bookingSourceCount >= 1 || uniquePrices.length >= 2) {
193
+ confidence = 'MEDIUM';
194
+ }
195
+ // ── Caveats ──
196
+ const caveats = [];
197
+ caveats.push('Prices may vary by date and availability. Book directly for confirmed pricing.');
198
+ if (!departDate) {
199
+ caveats.push('No specific date detected — prices shown are general/representative.');
200
+ }
201
+ if (isRoundTrip && returnLeg.length === 0) {
202
+ caveats.push('Could not find separate return leg pricing. Round-trip total unavailable.');
203
+ }
204
+ if (bookingSourceCount === 0) {
205
+ caveats.push('No booking site prices found — prices extracted from search snippets only.');
206
+ }
207
+ return {
208
+ vertical: 'transit',
209
+ headline,
210
+ confidence,
211
+ bestOption,
212
+ alternatives,
213
+ ...(totals ? { totals } : {}),
214
+ caveats,
215
+ query: {
216
+ origin: origin || undefined,
217
+ destination: destination || undefined,
218
+ departDate: departDate || undefined,
219
+ returnDate: returnDate || undefined,
220
+ isRoundTrip,
221
+ mode,
222
+ },
223
+ };
224
+ }