@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,1309 @@
1
+ import { peel } from '../../../../index.js';
2
+ import { getBestSearchProvider } from '../../../../core/search-provider.js';
3
+ import { buildSiteSearchUrl } from '../../../../core/site-search.js';
4
+ import { extractDomainData } from '../../../../ee/extractors/index.js';
5
+ import { getProfilePath, loadStorageState, touchProfile } from '../../../../core/profiles.js';
6
+ import { localSearch } from '../../../../core/local-search.js';
7
+ import { addAffiliateTag, getStoreInfo, parsePrice, cleanProductTitle, extractPriceValue, detectRequestedStore, stripRequestedStoreFromQuery, isRequestedStoreUrl } from '../utils.js';
8
+ import { callLLMQuick, sanitizeSearchQuery, PROMPT_INJECTION_DEFENSE } from '../llm.js';
9
+ const QUERY_STOPWORDS = new Set([
10
+ 'buy', 'shop', 'shopping', 'purchase', 'order', 'deal', 'deals', 'discount', 'sale', 'price', 'prices',
11
+ 'cheap', 'cheapest', 'best', 'under', 'for', 'with', 'the', 'a', 'an', 'and', 'or', 'to', 'of', 'in',
12
+ 'near', 'me', 'on', 'from', 'at', 'wireless', 'new', 'latest', 'review', 'reviews', 'worth', 'it',
13
+ ]);
14
+ const BUNDLE_PENALTY_RE = /\b(bundle|charger|stand|case|guide|accessor(?:y|ies)|kit|pack|renewed|refurbished|open box|used)\b/i;
15
+ const CONDITION_RE = /\b(Near Mint|NM|Lightly Played|LP|Moderately Played|MP|Heavily Played|HP|Damaged|DMG|Brand New|New|Used|Like New|Good|Very Good|Excellent|Open Box|Refurbished|Pre-Owned)\b/i;
16
+ const DEFAULT_RETAILER_STRATEGY = {
17
+ id: 'generic',
18
+ store: 'Retailer',
19
+ domain: 'generic',
20
+ difficulty: 'medium',
21
+ adapterFirst: false,
22
+ preferPersistentProfile: false,
23
+ profileHints: [],
24
+ };
25
+ const RETAILER_STRATEGIES = {
26
+ 'amazon.com': {
27
+ id: 'amazon',
28
+ store: 'Amazon',
29
+ domain: 'amazon.com',
30
+ difficulty: 'hard',
31
+ adapterFirst: false,
32
+ preferPersistentProfile: true,
33
+ profileHints: ['amazon.com', 'amazon'],
34
+ },
35
+ 'walmart.com': {
36
+ id: 'walmart',
37
+ store: 'Walmart',
38
+ domain: 'walmart.com',
39
+ difficulty: 'hard',
40
+ adapterFirst: true,
41
+ preferPersistentProfile: true,
42
+ profileHints: ['walmart.com', 'walmart'],
43
+ },
44
+ 'bestbuy.com': {
45
+ id: 'bestbuy',
46
+ store: 'Best Buy',
47
+ domain: 'bestbuy.com',
48
+ difficulty: 'hard',
49
+ adapterFirst: true,
50
+ preferPersistentProfile: true,
51
+ profileHints: ['bestbuy.com', 'bestbuy', 'best-buy'],
52
+ },
53
+ 'ebay.com': {
54
+ id: 'ebay',
55
+ store: 'eBay',
56
+ domain: 'ebay.com',
57
+ difficulty: 'medium',
58
+ adapterFirst: false,
59
+ preferPersistentProfile: false,
60
+ profileHints: ['ebay.com', 'ebay'],
61
+ },
62
+ 'target.com': {
63
+ id: 'target',
64
+ store: 'Target',
65
+ domain: 'target.com',
66
+ difficulty: 'hard',
67
+ adapterFirst: false,
68
+ preferPersistentProfile: true,
69
+ profileHints: ['target.com', 'target'],
70
+ },
71
+ 'etsy.com': {
72
+ id: 'etsy',
73
+ store: 'Etsy',
74
+ domain: 'etsy.com',
75
+ difficulty: 'medium',
76
+ adapterFirst: false,
77
+ preferPersistentProfile: false,
78
+ profileHints: ['etsy.com', 'etsy'],
79
+ },
80
+ };
81
+ function getRequestedStorePreference(intent) {
82
+ const { requestedStoreId, requestedStore, requestedStoreDomain } = intent.params || {};
83
+ if (requestedStoreId && requestedStore && requestedStoreDomain) {
84
+ return {
85
+ id: requestedStoreId,
86
+ store: requestedStore,
87
+ domain: requestedStoreDomain,
88
+ };
89
+ }
90
+ return detectRequestedStore(intent.query);
91
+ }
92
+ function escapeRegExp(value) {
93
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94
+ }
95
+ function buildProductKeyword(query, requestedStore, params = {}) {
96
+ const baseQuery = requestedStore ? stripRequestedStoreFromQuery(query, requestedStore) : query;
97
+ const locationText = params.location || params.localLocation || '';
98
+ const locationPattern = locationText
99
+ ? new RegExp(`\\b(?:near|around|in|at)?\\s*${escapeRegExp(locationText)}\\b`, 'gi')
100
+ : null;
101
+ let keyword = baseQuery
102
+ .replace(/\bbest price\b/gi, '')
103
+ .replace(/\b(?<!best\s)(buy|shop|shopping|purchase|order|deal|discount|sale|price|cheap|cheapest|under)\b/gi, '')
104
+ .replace(/\$\d[\d,]*/g, '');
105
+ if (params.localIntent === 'true') {
106
+ keyword = keyword
107
+ .replace(/\b(near me|nearby|closest|nearest|my local|local)\b/gi, '')
108
+ .replace(/\b(?:inventory|availability|available|in stock|stock|pickup|store pickup)\b/gi, '');
109
+ if (locationPattern)
110
+ keyword = keyword.replace(locationPattern, '');
111
+ }
112
+ return keyword.replace(/\s+/g, ' ').trim() || baseQuery.trim() || query;
113
+ }
114
+ function hasLocalRetailIntent(intent, requestedStore) {
115
+ return !!requestedStore && intent.params.localIntent === 'true';
116
+ }
117
+ function buildRequestedStoreSourceUrl(requestedStore, keyword) {
118
+ try {
119
+ return addAffiliateTag(buildSiteSearchUrl(requestedStore.id, keyword).url);
120
+ }
121
+ catch {
122
+ switch (requestedStore.id) {
123
+ case 'costco':
124
+ return `https://www.costco.com/CatalogSearch?dept=All&keyword=${encodeURIComponent(keyword)}`;
125
+ case 'bjs':
126
+ return `https://www.bjs.com/search/${encodeURIComponent(keyword)}`;
127
+ case 'traderjoes':
128
+ return `https://www.traderjoes.com/home/search?q=${encodeURIComponent(keyword)}`;
129
+ case 'dyson':
130
+ return `https://www.dyson.com/search-results?query=${encodeURIComponent(keyword)}`;
131
+ default:
132
+ return `https://www.${requestedStore.domain}`;
133
+ }
134
+ }
135
+ }
136
+ function getRequestedStoreLocalSearchQuery(requestedStore) {
137
+ switch (requestedStore.id) {
138
+ case 'bjs':
139
+ return "BJ's Wholesale Club";
140
+ case 'dyson':
141
+ return 'Dyson store';
142
+ default:
143
+ return requestedStore.store;
144
+ }
145
+ }
146
+ function matchesRequestedStoreName(name, requestedStore) {
147
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
148
+ const normalizedRequestedStore = requestedStore.store.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
149
+ if (normalizedName !== normalizedRequestedStore && /\b(gas|gasoline|pharmacy|optical|tire|hearing|food court)\b/.test(normalizedName)) {
150
+ return false;
151
+ }
152
+ const detected = detectRequestedStore(name);
153
+ if (detected?.id === requestedStore.id)
154
+ return true;
155
+ const requiredTokens = requestedStore.store
156
+ .toLowerCase()
157
+ .replace(/[^a-z0-9]+/g, ' ')
158
+ .split(' ')
159
+ .filter(token => token.length >= 2 && !['wholesale', 'club', 'store'].includes(token));
160
+ return requiredTokens.length > 0 && requiredTokens.every(token => normalizedName.includes(token));
161
+ }
162
+ async function resolveNearbyRetailStores(intent, requestedStore) {
163
+ const location = (intent.params.location || intent.params.localLocation || intent.params.zip || '').trim();
164
+ if (!location) {
165
+ return {
166
+ status: 'needs-location',
167
+ message: `I can see the local/nearby intent for ${requestedStore.store}, but this smart-search route still needs a city, ZIP, or coordinates to resolve "near me" honestly.`,
168
+ stores: [],
169
+ };
170
+ }
171
+ try {
172
+ const response = await localSearch({
173
+ query: getRequestedStoreLocalSearchQuery(requestedStore),
174
+ location,
175
+ limit: 5,
176
+ });
177
+ const stores = response.results
178
+ .filter(store => matchesRequestedStoreName(store.name, requestedStore))
179
+ .slice(0, 5);
180
+ if (stores.length === 0) {
181
+ return {
182
+ status: 'not-found',
183
+ location,
184
+ source: response.source,
185
+ message: `I did not find any nearby ${requestedStore.store} locations for ${location}.`,
186
+ stores: [],
187
+ };
188
+ }
189
+ return {
190
+ status: 'resolved',
191
+ location,
192
+ source: response.source,
193
+ message: `Found ${stores.length} nearby ${requestedStore.store} location${stores.length === 1 ? '' : 's'} for ${location}.`,
194
+ stores,
195
+ };
196
+ }
197
+ catch (err) {
198
+ return {
199
+ status: 'error',
200
+ location,
201
+ message: `Nearby ${requestedStore.store} lookup failed: ${err.message}`,
202
+ stores: [],
203
+ };
204
+ }
205
+ }
206
+ function buildRequestedStoreSearchQuery(requestedStore, keyword, isBulk, isGrocery, isCollectible) {
207
+ if (isGrocery && requestedStore.id === 'walmart') {
208
+ return `${keyword} price site:walmart.com/grocery OR site:walmart.com`;
209
+ }
210
+ if (isCollectible && requestedStore.id === 'ebay') {
211
+ return `${keyword} price site:ebay.com sold`;
212
+ }
213
+ if (isCollectible && requestedStore.id === 'etsy') {
214
+ return `${keyword} price site:etsy.com`;
215
+ }
216
+ if (isBulk && ['amazon', 'walmart', 'bestbuy', 'target'].includes(requestedStore.id)) {
217
+ return `${keyword} site:${requestedStore.domain}`;
218
+ }
219
+ return `${keyword} price site:${requestedStore.domain}`;
220
+ }
221
+ function filterToRequestedStore(items, requestedStore) {
222
+ if (!requestedStore)
223
+ return items;
224
+ return items.filter(item => isRequestedStoreUrl(item.rawUrl || item.url || '', requestedStore));
225
+ }
226
+ function normalizeMatchText(value) {
227
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim();
228
+ }
229
+ function collapseMatchText(value) {
230
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
231
+ }
232
+ function getQueryTokens(query) {
233
+ return Array.from(new Set(normalizeMatchText(query)
234
+ .split(' ')
235
+ .filter(token => token.length >= 2 && !QUERY_STOPWORDS.has(token))));
236
+ }
237
+ function scoreKeywordMatch(text, query) {
238
+ const normalizedText = normalizeMatchText(text);
239
+ const collapsedText = collapseMatchText(text);
240
+ const normalizedQuery = normalizeMatchText(query);
241
+ const collapsedQuery = collapseMatchText(query);
242
+ const tokens = getQueryTokens(query);
243
+ if (!normalizedText && !collapsedText)
244
+ return 0;
245
+ let score = 0;
246
+ if (collapsedQuery && collapsedText.includes(collapsedQuery))
247
+ score += 1.1;
248
+ if (normalizedQuery && normalizedText.includes(normalizedQuery))
249
+ score += 0.45;
250
+ if (tokens.length > 0) {
251
+ const tokenHits = tokens.filter(token => normalizedText.includes(token) || collapsedText.includes(token)).length;
252
+ score += tokenHits / tokens.length;
253
+ const modelTokens = tokens.filter(token => /\d/.test(token));
254
+ const modelHits = modelTokens.filter(token => collapsedText.includes(token.replace(/[^a-z0-9]+/g, ''))).length;
255
+ if (modelTokens.length > 0) {
256
+ score += (modelHits / modelTokens.length) * 0.35;
257
+ }
258
+ }
259
+ if (BUNDLE_PENALTY_RE.test(text))
260
+ score -= 0.2;
261
+ return score;
262
+ }
263
+ function formatUsd(value) {
264
+ return `$${value.toLocaleString('en-US', { minimumFractionDigits: value % 1 === 0 ? 0 : 2, maximumFractionDigits: 2 })}`;
265
+ }
266
+ function normalizePriceString(raw, content = '') {
267
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0)
268
+ return formatUsd(raw);
269
+ if (typeof raw === 'string' && raw.trim()) {
270
+ const directMatch = raw.match(/(?:US\s*)?\$\s*([\d,]+(?:\.\d{2})?)/i);
271
+ if (directMatch) {
272
+ const numeric = parseFloat(directMatch[1].replace(/,/g, ''));
273
+ if (Number.isFinite(numeric) && numeric > 0) {
274
+ return `$${numeric.toLocaleString('en-US', {
275
+ minimumFractionDigits: directMatch[1].includes('.') ? 2 : 0,
276
+ maximumFractionDigits: 2,
277
+ })}`;
278
+ }
279
+ }
280
+ const parsed = parsePrice(raw);
281
+ if (parsed)
282
+ return parsed;
283
+ }
284
+ const labeledLine = content.split('\n').find(line => /\bprice\b/i.test(line) && /\$/i.test(line));
285
+ if (labeledLine) {
286
+ const parsed = parsePrice(labeledLine);
287
+ if (parsed)
288
+ return parsed;
289
+ }
290
+ const firstDollar = content.match(/(?:US\s*)?\$\s*[\d,]+(?:\.\d{2})?/i)?.[0];
291
+ if (firstDollar) {
292
+ const parsed = parsePrice(firstDollar);
293
+ if (parsed)
294
+ return parsed;
295
+ }
296
+ return undefined;
297
+ }
298
+ function normalizeAvailability(rawAvailability, rawInStock, content = '') {
299
+ if (typeof rawAvailability === 'string' && rawAvailability.trim()) {
300
+ return rawAvailability.replace(/^https?:\/\/schema\.org\//i, '').replace(/\s+/g, ' ').trim();
301
+ }
302
+ if (typeof rawInStock === 'boolean')
303
+ return rawInStock ? 'In Stock' : 'Out of Stock';
304
+ const availabilityLine = content.split('\n').find(line => /\b(in stock|out of stock|available|unavailable|sold out)\b/i.test(line));
305
+ if (!availabilityLine)
306
+ return undefined;
307
+ if (/out of stock|sold out|unavailable/i.test(availabilityLine))
308
+ return 'Out of Stock';
309
+ if (/in stock|available/i.test(availabilityLine))
310
+ return 'In Stock';
311
+ return undefined;
312
+ }
313
+ function normalizeCondition(rawCondition, content = '') {
314
+ const coerceCondition = (value) => {
315
+ const cleaned = value.replace(/\s+/g, ' ').replace(/^\**\s*condition\s*:\s*/i, '').replace(/\*+/g, '').trim();
316
+ if (!cleaned)
317
+ return undefined;
318
+ const explicit = cleaned.match(/(Brand New|New|Used|Like New|Good(?:\s*-\s*Refurbished)?|Very Good|Excellent|Open Box|Refurbished|Pre-Owned)/i)?.[1];
319
+ if (explicit)
320
+ return explicit;
321
+ return cleaned.length > 120 ? `${cleaned.slice(0, 117)}…` : cleaned;
322
+ };
323
+ if (typeof rawCondition === 'string' && rawCondition.trim()) {
324
+ return coerceCondition(rawCondition);
325
+ }
326
+ const conditionLine = content.split('\n').find(line => /\bcondition\b/i.test(line));
327
+ if (conditionLine) {
328
+ return coerceCondition(conditionLine);
329
+ }
330
+ return undefined;
331
+ }
332
+ function looksBlockedPage(result) {
333
+ if (!result)
334
+ return true;
335
+ if (result.blocked)
336
+ return true;
337
+ const title = (result.title || '').toLowerCase();
338
+ const content = (result.content || '').toLowerCase();
339
+ const method = String(result.method || '').toLowerCase();
340
+ return (title === 'robot or human?' ||
341
+ /access denied|captcha|robot or human|verify you are human|temporarily unavailable/i.test(`${title}\n${content}`) ||
342
+ method === 'search-fallback' ||
343
+ /limited content .* blocked direct access/i.test(content));
344
+ }
345
+ function extractLocalInventorySignal(structured, content = '') {
346
+ const structuredCandidates = [
347
+ structured.pickupAvailability,
348
+ structured.pickupStatus,
349
+ structured.storeAvailability,
350
+ structured.localAvailability,
351
+ structured.availabilityMessage,
352
+ structured.fulfillmentMessage,
353
+ ].filter((value) => typeof value === 'string' && value.trim().length > 0);
354
+ const contentCandidates = content
355
+ .split('\n')
356
+ .map(line => line.replace(/^[#>*\-\s]+/, '').replace(/\*+/g, '').trim())
357
+ .filter(line => /\b(pickup|pick up|same day|curbside|local store|nearby store|store pickup)\b/i.test(line));
358
+ for (const candidate of [...structuredCandidates, ...contentCandidates]) {
359
+ const cleaned = candidate.replace(/\s+/g, ' ').trim();
360
+ if (!cleaned)
361
+ continue;
362
+ if (!/\b(pickup|pick up|same day|curbside|local store|nearby store|store pickup)\b/i.test(cleaned))
363
+ continue;
364
+ if (!/\b(available|in stock|ready|unavailable|not available|out of stock|sold out)\b/i.test(cleaned))
365
+ continue;
366
+ return {
367
+ verified: true,
368
+ status: cleaned.length > 160 ? `${cleaned.slice(0, 157)}…` : cleaned,
369
+ };
370
+ }
371
+ return null;
372
+ }
373
+ function isLikelyProductDetailUrl(url) {
374
+ try {
375
+ const parsed = new URL(url);
376
+ const hostname = parsed.hostname.replace(/^www\./, '');
377
+ const path = parsed.pathname;
378
+ if (/amazon\./.test(hostname))
379
+ return /\/dp\/[A-Z0-9]{10}/i.test(path) || /\/gp\/product\//i.test(path);
380
+ if (/walmart\./.test(hostname))
381
+ return /\/ip\//i.test(path) && !/\/browse\//i.test(path);
382
+ if (/bestbuy\./.test(hostname))
383
+ return /\/site\//i.test(path) && /\/\d+\.p$/i.test(path) || /\/product\//i.test(path);
384
+ if (/target\./.test(hostname))
385
+ return /\/p\//i.test(path);
386
+ if (/costco\./.test(hostname))
387
+ return /\/Product\./i.test(path) || /\.html$/i.test(path);
388
+ if (/bjs\./.test(hostname))
389
+ return /\/product\//i.test(path) || /\/sku\//i.test(path);
390
+ if (/traderjoes\./.test(hostname))
391
+ return /\/products\/pdp\//i.test(path);
392
+ if (/dyson\./.test(hostname))
393
+ return /\/[^/]+\/[^/]+$/i.test(path) && !/\/search/i.test(path);
394
+ if (/ebay\./.test(hostname))
395
+ return /\/itm\//i.test(path);
396
+ if (/etsy\./.test(hostname))
397
+ return /\/listing\//i.test(path);
398
+ if (/tcgplayer\./.test(hostname))
399
+ return /\/product\//i.test(path);
400
+ if (/mercari\./.test(hostname))
401
+ return /\/item\//i.test(path);
402
+ return /\/product\//i.test(path) || /\/p\//i.test(path) || /\/itm\//i.test(path) || /\.html$/i.test(path);
403
+ }
404
+ catch {
405
+ return false;
406
+ }
407
+ }
408
+ function isLikelyCategoryOrSearchUrl(url, title = '', snippet = '') {
409
+ try {
410
+ const parsed = new URL(url);
411
+ const text = `${parsed.pathname} ${parsed.search} ${title} ${snippet}`.toLowerCase();
412
+ return /\/s\b|search|searchpage|\/browse\/|category|department|results|shop for|all products|collections?/.test(text);
413
+ }
414
+ catch {
415
+ return false;
416
+ }
417
+ }
418
+ function getRetailerStrategy(url) {
419
+ const storeInfo = getStoreInfo(url);
420
+ if (!storeInfo)
421
+ return DEFAULT_RETAILER_STRATEGY;
422
+ return RETAILER_STRATEGIES[storeInfo.domain] || {
423
+ id: storeInfo.domain.replace(/\..*$/, ''),
424
+ store: storeInfo.store,
425
+ domain: storeInfo.domain,
426
+ difficulty: 'medium',
427
+ adapterFirst: false,
428
+ preferPersistentProfile: false,
429
+ profileHints: [storeInfo.domain, storeInfo.store.toLowerCase().replace(/[^a-z0-9]+/g, '-')],
430
+ };
431
+ }
432
+ function getRetailerProfile(url, strategy) {
433
+ if (!strategy.preferPersistentProfile)
434
+ return null;
435
+ const candidates = new Set(strategy.profileHints);
436
+ try {
437
+ const hostname = new URL(url).hostname.replace(/^www\./, '');
438
+ candidates.add(hostname);
439
+ const root = hostname.split('.').slice(-2).join('.');
440
+ if (root)
441
+ candidates.add(root);
442
+ }
443
+ catch {
444
+ // Ignore invalid URLs
445
+ }
446
+ const normalizedStore = strategy.store.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
447
+ if (normalizedStore) {
448
+ candidates.add(normalizedStore);
449
+ candidates.add(normalizedStore.replace(/-/g, ''));
450
+ }
451
+ for (const profileName of candidates) {
452
+ if (!profileName)
453
+ continue;
454
+ const profileDir = getProfilePath(profileName);
455
+ if (!profileDir)
456
+ continue;
457
+ return {
458
+ profileName,
459
+ profileDir,
460
+ storageState: loadStorageState(profileName) ?? undefined,
461
+ };
462
+ }
463
+ return null;
464
+ }
465
+ function getStructuredPriceCandidate(structured) {
466
+ return structured.price
467
+ ?? structured.salePrice
468
+ ?? structured.regularPrice
469
+ ?? structured.currentPrice
470
+ ?? structured.currentPrice?.price
471
+ ?? structured.priceInfo?.currentPrice?.price
472
+ ?? structured.offerPrice;
473
+ }
474
+ function buildRetailerPeelAttempts(strategy, profile) {
475
+ const attempts = [];
476
+ const profileAttempt = profile
477
+ ? {
478
+ render: true,
479
+ stealth: true,
480
+ timeout: strategy.difficulty === 'hard' ? 14000 : 11000,
481
+ wait: strategy.difficulty === 'hard' ? 1000 : 750,
482
+ profileDir: profile.profileDir,
483
+ storageState: profile.storageState,
484
+ }
485
+ : null;
486
+ if (profileAttempt)
487
+ attempts.push(profileAttempt);
488
+ switch (strategy.domain) {
489
+ case 'amazon.com':
490
+ case 'walmart.com':
491
+ case 'bestbuy.com':
492
+ case 'target.com':
493
+ attempts.push({ render: true, stealth: true, timeout: 12000, wait: 1000 }, { render: true, timeout: 9000, wait: 750 }, { render: false, timeout: 7000 });
494
+ break;
495
+ case 'ebay.com':
496
+ attempts.push({ render: false, timeout: 6000 }, { render: true, timeout: 8500, wait: 500 }, { render: true, stealth: true, timeout: 10000, wait: 800 });
497
+ break;
498
+ default:
499
+ attempts.push({ render: false, timeout: 7000 }, { render: true, timeout: 9000, wait: 750 }, { render: true, stealth: true, timeout: 12000, wait: 1000 });
500
+ break;
501
+ }
502
+ const seen = new Set();
503
+ return attempts.filter((attempt) => {
504
+ const key = JSON.stringify(attempt);
505
+ if (seen.has(key))
506
+ return false;
507
+ seen.add(key);
508
+ return true;
509
+ });
510
+ }
511
+ function extractEvidenceFromStructuredSource(input) {
512
+ const structured = input.structured || {};
513
+ const content = input.content || '';
514
+ const rawTitle = String(structured.title || structured.name || input.title || input.fallbackTitle || '').trim();
515
+ const title = cleanProductTitle(rawTitle);
516
+ if (!title || /^learn more$/i.test(title))
517
+ return null;
518
+ const price = normalizePriceString(getStructuredPriceCandidate(structured), content);
519
+ const priceValue = extractPriceValue(price);
520
+ if (!price || priceValue === undefined)
521
+ return null;
522
+ const matchScore = scoreKeywordMatch(`${title} ${input.url}`, input.keyword);
523
+ if (matchScore < 0.8)
524
+ return null;
525
+ const localInventory = extractLocalInventorySignal(structured, content);
526
+ const storeInfo = getStoreInfo(input.url);
527
+ return {
528
+ title,
529
+ price,
530
+ priceValue,
531
+ url: addAffiliateTag(input.url),
532
+ rawUrl: input.url,
533
+ store: storeInfo?.store || input.strategy.store || new URL(input.url).hostname.replace(/^www\./, ''),
534
+ availability: normalizeAvailability(structured.availability ?? structured.availabilityStatus, structured.inStock ?? structured.onlineAvailability, content),
535
+ condition: normalizeCondition(structured.condition, content),
536
+ brand: typeof (structured.brand ?? structured.manufacturer) === 'string' ? String(structured.brand ?? structured.manufacturer) : undefined,
537
+ model: typeof (structured.model ?? structured.modelNumber) === 'string' ? String(structured.model ?? structured.modelNumber) : undefined,
538
+ image: typeof structured.image === 'string' ? structured.image : undefined,
539
+ matchScore,
540
+ difficulty: input.strategy.difficulty,
541
+ source: input.source,
542
+ fetchMethod: input.fetchMethod,
543
+ profileUsed: input.profileUsed,
544
+ localInventoryVerified: localInventory?.verified,
545
+ localInventoryStatus: localInventory?.status,
546
+ };
547
+ }
548
+ function extractEvidenceFromPage(result, url, fallbackTitle, keyword, strategy, profileUsed) {
549
+ if (looksBlockedPage(result))
550
+ return null;
551
+ const structured = (result.domainData?.structured || {});
552
+ return extractEvidenceFromStructuredSource({
553
+ structured,
554
+ content: result.content || '',
555
+ title: result.title || '',
556
+ url,
557
+ fallbackTitle,
558
+ keyword,
559
+ strategy,
560
+ source: Object.keys(structured).length > 0 ? 'page-domain-data' : 'page-content',
561
+ fetchMethod: result.method,
562
+ profileUsed,
563
+ });
564
+ }
565
+ async function fetchPageWithRetailerRouting(url) {
566
+ const strategy = getRetailerStrategy(url);
567
+ const profile = getRetailerProfile(url, strategy);
568
+ if (strategy.adapterFirst && isLikelyProductDetailUrl(url)) {
569
+ try {
570
+ const domainResult = await extractDomainData('', url);
571
+ if (domainResult?.structured) {
572
+ return {
573
+ strategy,
574
+ page: null,
575
+ domainResult,
576
+ source: 'retailer-extractor',
577
+ fetchMethod: 'retailer-extractor',
578
+ };
579
+ }
580
+ }
581
+ catch {
582
+ // Fall through to peel attempts
583
+ }
584
+ }
585
+ let lastResult = null;
586
+ let lastProfileUsed;
587
+ for (const attempt of buildRetailerPeelAttempts(strategy, profile)) {
588
+ try {
589
+ const result = await peel(url, attempt);
590
+ lastResult = result;
591
+ const usedProfile = typeof attempt.profileDir === 'string' ? profile?.profileName : undefined;
592
+ if (usedProfile) {
593
+ lastProfileUsed = usedProfile;
594
+ }
595
+ if (!looksBlockedPage(result) && ((result.content?.length || 0) > 120 || (result.links?.length || 0) > 0 || !!result.domainData)) {
596
+ if (usedProfile)
597
+ touchProfile(usedProfile);
598
+ return {
599
+ strategy,
600
+ page: result,
601
+ domainResult: null,
602
+ source: result.domainData ? 'page-domain-data' : 'page-content',
603
+ fetchMethod: result.method,
604
+ profileUsed: usedProfile,
605
+ };
606
+ }
607
+ }
608
+ catch {
609
+ // Ignore and escalate
610
+ }
611
+ }
612
+ return {
613
+ strategy,
614
+ page: lastResult,
615
+ domainResult: null,
616
+ source: lastResult?.domainData ? 'page-domain-data' : (lastResult ? 'page-content' : undefined),
617
+ fetchMethod: lastResult?.method,
618
+ profileUsed: lastProfileUsed,
619
+ };
620
+ }
621
+ function selectMatchingProductLinks(links, keyword, parentUrl) {
622
+ let parentHost = '';
623
+ try {
624
+ parentHost = new URL(parentUrl).hostname.replace(/^www\./, '');
625
+ }
626
+ catch {
627
+ parentHost = '';
628
+ }
629
+ const scored = links
630
+ .filter(link => /^https?:\/\//i.test(link))
631
+ .filter(link => getStoreInfo(link) !== null)
632
+ .filter(link => isLikelyProductDetailUrl(link))
633
+ .map(link => {
634
+ let sameHostBonus = 0;
635
+ try {
636
+ const host = new URL(link).hostname.replace(/^www\./, '');
637
+ if (host === parentHost)
638
+ sameHostBonus = 0.15;
639
+ }
640
+ catch {
641
+ sameHostBonus = 0;
642
+ }
643
+ return {
644
+ link,
645
+ score: scoreKeywordMatch(decodeURIComponent(link), keyword) + sameHostBonus,
646
+ };
647
+ })
648
+ .filter(item => item.score >= 0.9)
649
+ .sort((a, b) => b.score - a.score);
650
+ const seen = new Set();
651
+ const picked = [];
652
+ for (const item of scored) {
653
+ if (seen.has(item.link))
654
+ continue;
655
+ seen.add(item.link);
656
+ picked.push(item.link);
657
+ if (picked.length >= 2)
658
+ break;
659
+ }
660
+ return picked;
661
+ }
662
+ async function drillDownSearchResult(result, keyword) {
663
+ if (!result.url || !getStoreInfo(result.url))
664
+ return { evidence: null, checkedProductPages: 0, routing: [] };
665
+ if (isLikelyProductDetailUrl(result.url)) {
666
+ const routed = await fetchPageWithRetailerRouting(result.url);
667
+ const directEvidence = routed.domainResult
668
+ ? extractEvidenceFromStructuredSource({
669
+ structured: routed.domainResult.structured,
670
+ content: routed.domainResult.cleanContent,
671
+ title: routed.domainResult.structured?.title || routed.domainResult.structured?.name,
672
+ url: result.url,
673
+ fallbackTitle: result.title || '',
674
+ keyword,
675
+ strategy: routed.strategy,
676
+ source: 'retailer-extractor',
677
+ fetchMethod: routed.fetchMethod,
678
+ profileUsed: routed.profileUsed,
679
+ })
680
+ : (routed.page ? extractEvidenceFromPage(routed.page, result.url, result.title || '', keyword, routed.strategy, routed.profileUsed) : null);
681
+ return {
682
+ evidence: directEvidence,
683
+ checkedProductPages: routed.page || routed.domainResult ? 1 : 0,
684
+ routing: [{
685
+ url: result.url,
686
+ store: routed.strategy.store,
687
+ domain: routed.strategy.domain,
688
+ difficulty: routed.strategy.difficulty,
689
+ route: routed.domainResult ? 'adapter-first' : 'peel',
690
+ source: routed.source,
691
+ fetchMethod: routed.fetchMethod,
692
+ profileUsed: routed.profileUsed,
693
+ evidenceFound: !!directEvidence,
694
+ }],
695
+ };
696
+ }
697
+ const routedParent = await fetchPageWithRetailerRouting(result.url);
698
+ const page = routedParent.page;
699
+ if (!page) {
700
+ return {
701
+ evidence: null,
702
+ checkedProductPages: 0,
703
+ routing: [{
704
+ url: result.url,
705
+ store: routedParent.strategy.store,
706
+ domain: routedParent.strategy.domain,
707
+ difficulty: routedParent.strategy.difficulty,
708
+ route: 'peel',
709
+ source: routedParent.source,
710
+ fetchMethod: routedParent.fetchMethod,
711
+ profileUsed: routedParent.profileUsed,
712
+ evidenceFound: false,
713
+ }],
714
+ };
715
+ }
716
+ const pageEvidence = extractEvidenceFromPage(page, result.url, result.title || '', keyword, routedParent.strategy, routedParent.profileUsed);
717
+ const isCategoryOrSearchPage = isLikelyCategoryOrSearchUrl(result.url, result.title, result.snippet);
718
+ if (pageEvidence && !isCategoryOrSearchPage) {
719
+ return {
720
+ evidence: pageEvidence,
721
+ checkedProductPages: 1,
722
+ routing: [{
723
+ url: result.url,
724
+ store: routedParent.strategy.store,
725
+ domain: routedParent.strategy.domain,
726
+ difficulty: routedParent.strategy.difficulty,
727
+ route: 'peel',
728
+ source: routedParent.source,
729
+ fetchMethod: routedParent.fetchMethod,
730
+ profileUsed: routedParent.profileUsed,
731
+ evidenceFound: true,
732
+ }],
733
+ };
734
+ }
735
+ if (!isCategoryOrSearchPage) {
736
+ return {
737
+ evidence: null,
738
+ checkedProductPages: 0,
739
+ routing: [{
740
+ url: result.url,
741
+ store: routedParent.strategy.store,
742
+ domain: routedParent.strategy.domain,
743
+ difficulty: routedParent.strategy.difficulty,
744
+ route: 'peel',
745
+ source: routedParent.source,
746
+ fetchMethod: routedParent.fetchMethod,
747
+ profileUsed: routedParent.profileUsed,
748
+ evidenceFound: false,
749
+ }],
750
+ };
751
+ }
752
+ const candidateLinks = selectMatchingProductLinks(page.links || [], keyword, result.url);
753
+ let bestEvidence = null;
754
+ let checkedProductPages = 0;
755
+ const routing = [{
756
+ url: result.url,
757
+ store: routedParent.strategy.store,
758
+ domain: routedParent.strategy.domain,
759
+ difficulty: routedParent.strategy.difficulty,
760
+ route: 'peel',
761
+ source: routedParent.source,
762
+ fetchMethod: routedParent.fetchMethod,
763
+ profileUsed: routedParent.profileUsed,
764
+ evidenceFound: false,
765
+ }];
766
+ for (const link of candidateLinks) {
767
+ const routedLink = await fetchPageWithRetailerRouting(link);
768
+ const evidence = routedLink.domainResult
769
+ ? extractEvidenceFromStructuredSource({
770
+ structured: routedLink.domainResult.structured,
771
+ content: routedLink.domainResult.cleanContent,
772
+ title: routedLink.domainResult.structured?.title || routedLink.domainResult.structured?.name,
773
+ url: link,
774
+ fallbackTitle: result.title || '',
775
+ keyword,
776
+ strategy: routedLink.strategy,
777
+ source: 'retailer-extractor',
778
+ fetchMethod: routedLink.fetchMethod,
779
+ profileUsed: routedLink.profileUsed,
780
+ })
781
+ : (routedLink.page ? extractEvidenceFromPage(routedLink.page, link, result.title || '', keyword, routedLink.strategy, routedLink.profileUsed) : null);
782
+ if (routedLink.page || routedLink.domainResult)
783
+ checkedProductPages += 1;
784
+ routing.push({
785
+ url: link,
786
+ store: routedLink.strategy.store,
787
+ domain: routedLink.strategy.domain,
788
+ difficulty: routedLink.strategy.difficulty,
789
+ route: routedLink.domainResult ? 'adapter-first' : 'peel',
790
+ source: routedLink.source,
791
+ fetchMethod: routedLink.fetchMethod,
792
+ profileUsed: routedLink.profileUsed,
793
+ evidenceFound: !!evidence,
794
+ });
795
+ if (!evidence)
796
+ continue;
797
+ if (!bestEvidence || evidence.matchScore > bestEvidence.matchScore || (evidence.matchScore === bestEvidence.matchScore && evidence.priceValue < bestEvidence.priceValue)) {
798
+ bestEvidence = evidence;
799
+ }
800
+ }
801
+ return { evidence: bestEvidence, checkedProductPages, routing };
802
+ }
803
+ function dedupeVerifiedEvidence(items) {
804
+ const seen = new Set();
805
+ return items
806
+ .sort((a, b) => {
807
+ if (b.matchScore !== a.matchScore)
808
+ return b.matchScore - a.matchScore;
809
+ if (a.source !== b.source)
810
+ return a.source === 'retailer-extractor' ? -1 : 1;
811
+ return a.priceValue - b.priceValue;
812
+ })
813
+ .filter(item => {
814
+ const key = `${item.store}|${normalizeMatchText(item.title)}|${item.price}`;
815
+ if (seen.has(key))
816
+ return false;
817
+ seen.add(key);
818
+ return true;
819
+ })
820
+ .slice(0, 6);
821
+ }
822
+ function buildVerifiedAnswer(keyword, evidence, requestedStore) {
823
+ const top = evidence.slice(0, 3).map((item, index) => {
824
+ const extras = [item.availability, item.condition].filter(Boolean).join('; ');
825
+ return `${index + 1}. ${item.title} — ${item.price} at ${item.store}${extras ? ` (${extras})` : ''} [${index + 1}]`;
826
+ }).join(' ');
827
+ if (requestedStore) {
828
+ return `I checked ${requestedStore.store} product pages for ${keyword}. Best verified option${evidence.length === 1 ? '' : 's'}: ${top}`;
829
+ }
830
+ return `I checked product pages for ${keyword}. Best verified options: ${top}`;
831
+ }
832
+ function buildFallbackAnswer(keyword, checkedProductPages, requestedStore) {
833
+ if (requestedStore) {
834
+ if (checkedProductPages > 0) {
835
+ return `I checked ${checkedProductPages} ${requestedStore.store} product page${checkedProductPages === 1 ? '' : 's'} for ${keyword}, but I could not verify a trustworthy live price from ${requestedStore.store}. I’m not going to substitute another store.`;
836
+ }
837
+ return `I couldn’t verify any actual ${requestedStore.store} product pages for ${keyword}, so I’m not going to substitute another store based on snippets.`;
838
+ }
839
+ if (checkedProductPages > 0) {
840
+ return `I checked ${checkedProductPages} product page${checkedProductPages === 1 ? '' : 's'} for ${keyword}, but I could not verify a trustworthy live price from those pages. I’m not going to guess from snippets.`;
841
+ }
842
+ return `I couldn’t verify any actual product pages for ${keyword}, so I’m not going to claim a live price from search snippets alone.`;
843
+ }
844
+ function buildLocalRetailSection(input) {
845
+ const { keyword, requestedStore, rawResults, verifiedEvidence, nearbyRetail } = input;
846
+ const requestedStoreResults = filterToRequestedStore(rawResults, requestedStore);
847
+ const requestedStoreEvidence = verifiedEvidence.filter(item => item.store === requestedStore.store);
848
+ const localInventoryEvidence = requestedStoreEvidence.find(item => item.localInventoryVerified && item.localInventoryStatus);
849
+ const catalogLine = requestedStoreEvidence.length > 0
850
+ ? `- Retailer catalog existence: **Verified** on a ${requestedStore.store} product page.`
851
+ : requestedStoreResults.length > 0
852
+ ? `- Retailer catalog existence: **Search-result evidence only** (${requestedStoreResults.length} ${requestedStore.store} hit${requestedStoreResults.length === 1 ? '' : 's'} found, but no trustworthy PDP verification).`
853
+ : `- Retailer catalog existence: **Not found** in the checked ${requestedStore.store} results.`;
854
+ const nearbyLine = nearbyRetail.status === 'resolved'
855
+ ? `- Nearby ${requestedStore.store} stores: **${nearbyRetail.stores.length} found** near ${nearbyRetail.location} (${nearbyRetail.source || 'local search'}).`
856
+ : nearbyRetail.status === 'needs-location'
857
+ ? `- Nearby ${requestedStore.store} stores: **Need location context** ("near me" alone is not enough on the server side).`
858
+ : nearbyRetail.status === 'not-found'
859
+ ? `- Nearby ${requestedStore.store} stores: **None found** near ${nearbyRetail.location}.`
860
+ : `- Nearby ${requestedStore.store} stores: **Lookup failed** (${nearbyRetail.message}).`;
861
+ const inventoryLine = localInventoryEvidence
862
+ ? `- Local inventory: **Verified from public retailer page signal** — ${localInventoryEvidence.localInventoryStatus}.`
863
+ : '- Local inventory: **Not publicly verifiable** from the retailer pages I checked.';
864
+ const nearbyStoresSection = nearbyRetail.status === 'resolved' && nearbyRetail.stores.length > 0
865
+ ? [
866
+ '',
867
+ '### Nearby stores',
868
+ ...nearbyRetail.stores.slice(0, 3).map((store, index) => {
869
+ const rating = store.rating ? ` · ⭐${store.rating}${store.reviewCount ? ` (${store.reviewCount.toLocaleString()} reviews)` : ''}` : '';
870
+ const openStatus = store.isOpen === true ? ' · 🟢 Open now' : (store.isOpen === false ? ' · 🔴 Closed' : '');
871
+ const mapsLink = store.googleMapsUrl ? ` · [Maps](${store.googleMapsUrl})` : '';
872
+ return `${index + 1}. **${store.name}**${rating}${openStatus}${mapsLink}${store.address ? ` — ${store.address}` : ''}`;
873
+ }),
874
+ ]
875
+ : [];
876
+ return [
877
+ '## Local retail check',
878
+ `- Requested retailer: **${requestedStore.store}**`,
879
+ `- Product: **${keyword}**`,
880
+ catalogLine,
881
+ nearbyLine,
882
+ inventoryLine,
883
+ ...nearbyStoresSection,
884
+ '',
885
+ ].join('\n');
886
+ }
887
+ function buildLocalRetailAnswer(input) {
888
+ const { keyword, requestedStore, rawResults, verifiedEvidence, nearbyRetail, checkedProductPages } = input;
889
+ const requestedStoreResults = filterToRequestedStore(rawResults, requestedStore);
890
+ const requestedStoreEvidence = verifiedEvidence.filter(item => item.store === requestedStore.store);
891
+ const localInventoryEvidence = requestedStoreEvidence.find(item => item.localInventoryVerified && item.localInventoryStatus);
892
+ const parts = [];
893
+ if (requestedStoreEvidence.length > 0) {
894
+ parts.push(`I verified that ${requestedStore.store} lists ${keyword} on a product page.`);
895
+ }
896
+ else if (requestedStoreResults.length > 0) {
897
+ parts.push(`I found ${requestedStore.store} search-result evidence for ${keyword}, but I could not fully verify a trustworthy ${requestedStore.store} product page.`);
898
+ }
899
+ else {
900
+ parts.push(`I could not find reliable ${requestedStore.store} catalog evidence for ${keyword}.`);
901
+ }
902
+ if (nearbyRetail.status === 'resolved') {
903
+ parts.push(`I found ${nearbyRetail.stores.length} nearby ${requestedStore.store} location${nearbyRetail.stores.length === 1 ? '' : 's'} near ${nearbyRetail.location}.`);
904
+ }
905
+ else if (nearbyRetail.status === 'needs-location') {
906
+ parts.push('I can see the nearby/local intent, but this route still needs a city or ZIP to resolve "near me" honestly.');
907
+ }
908
+ else if (nearbyRetail.status === 'not-found') {
909
+ parts.push(`I did not find nearby ${requestedStore.store} locations near ${nearbyRetail.location}.`);
910
+ }
911
+ else {
912
+ parts.push(`Nearby ${requestedStore.store} store lookup failed, so local store presence is unresolved.`);
913
+ }
914
+ if (localInventoryEvidence) {
915
+ parts.push(`Public local pickup/store inventory is visible: ${localInventoryEvidence.localInventoryStatus}.`);
916
+ }
917
+ else if (checkedProductPages > 0 || requestedStoreResults.length > 0) {
918
+ parts.push(`I could not verify store-specific local inventory from public retailer pages, so I cannot honestly say whether your local ${requestedStore.store} has it in stock.`);
919
+ }
920
+ return parts.join(' ');
921
+ }
922
+ function scoreSearchResultCandidate(result, keyword) {
923
+ const text = `${result.title || ''} ${result.snippet || ''} ${result.url || ''}`;
924
+ let score = scoreKeywordMatch(text, keyword);
925
+ if (isLikelyProductDetailUrl(result.url || ''))
926
+ score += 0.35;
927
+ if (isLikelyCategoryOrSearchUrl(result.url || '', result.title, result.snippet))
928
+ score -= 0.35;
929
+ return score;
930
+ }
931
+ export async function handleProductSearch(intent) {
932
+ const t0 = Date.now();
933
+ const requestedStore = getRequestedStorePreference(intent);
934
+ const keyword = buildProductKeyword(intent.query, requestedStore, intent.params || {});
935
+ const localRetailIntent = hasLocalRetailIntent(intent, requestedStore);
936
+ const nearbyRetailPromise = localRetailIntent && requestedStore
937
+ ? resolveNearbyRetailStores(intent, requestedStore)
938
+ : Promise.resolve(null);
939
+ // Parallel site-specific searches
940
+ const { provider: searchProvider } = getBestSearchProvider();
941
+ const isBulk = /\b(bulk|wholesale|1000|500|case|pallet|box of|pack of|carton)\b/i.test(intent.query);
942
+ const isGrocery = intent.params.isGrocery === 'true' || /\b(grocery|milk|eggs|bread|butter|cheese|chicken|produce)\b/i.test(intent.query);
943
+ const isCollectible = /\b(pokemon|pokémon|magic\s*the\s*gathering|mtg|yu-?gi-?oh|trading\s*card|tcg|baseball\s*card|sports\s*card|collectible\s*card|figurine|funko|hot\s*wheels|lego\s*set|vintage\s*toy|action\s*figure|comic\s*book|vinyl\s*record|rare\s*coin|stamp\s*collection)\b/i.test(intent.query);
944
+ let rawResults;
945
+ let redditResults;
946
+ if (requestedStore) {
947
+ const [storeSettled] = await Promise.allSettled([
948
+ searchProvider.searchWeb(buildRequestedStoreSearchQuery(requestedStore, keyword, isBulk, isGrocery, isCollectible), { count: 6 }),
949
+ ]);
950
+ rawResults = filterToRequestedStore(storeSettled.status === 'fulfilled' ? storeSettled.value : [], requestedStore);
951
+ redditResults = [];
952
+ }
953
+ else if (isCollectible) {
954
+ const [tcgSettled, ebaySettled, etsySettled, fbAmazonSettled, redditSettled] = await Promise.allSettled([
955
+ searchProvider.searchWeb(`${keyword} price site:tcgplayer.com`, { count: 2 }),
956
+ searchProvider.searchWeb(`${keyword} price site:ebay.com sold`, { count: 2 }),
957
+ searchProvider.searchWeb(`${keyword} price site:etsy.com OR site:mercari.com`, { count: 3 }),
958
+ searchProvider.searchWeb(`${keyword} price site:facebook.com/marketplace OR site:amazon.com`, { count: 3 }),
959
+ searchProvider.searchWeb(`${keyword} cheapest reddit where to buy`, { count: 3 }),
960
+ ]);
961
+ rawResults = [
962
+ ...(tcgSettled.status === 'fulfilled' ? tcgSettled.value : []),
963
+ ...(ebaySettled.status === 'fulfilled' ? ebaySettled.value : []),
964
+ ...(etsySettled.status === 'fulfilled' ? etsySettled.value : []),
965
+ ...(fbAmazonSettled.status === 'fulfilled' ? fbAmazonSettled.value : []),
966
+ ];
967
+ redditResults = redditSettled.status === 'fulfilled' ? redditSettled.value : [];
968
+ }
969
+ else if (isGrocery) {
970
+ // Search grocery-specific sites
971
+ const [instacartSettled, walmartGrocerySettled, freshSettled, redditGrocerySettled] = await Promise.allSettled([
972
+ searchProvider.searchWeb(`${keyword} price site:instacart.com`, { count: 2 }),
973
+ searchProvider.searchWeb(`${keyword} price site:walmart.com/grocery OR site:walmart.com`, { count: 2 }),
974
+ searchProvider.searchWeb(`${keyword} price site:freshdirect.com OR site:wholefoodsmarket.com`, { count: 3 }),
975
+ searchProvider.searchWeb(`${keyword} cheapest grocery store reddit`, { count: 3 }),
976
+ ]);
977
+ rawResults = [
978
+ ...(instacartSettled.status === 'fulfilled' ? instacartSettled.value : []),
979
+ ...(walmartGrocerySettled.status === 'fulfilled' ? walmartGrocerySettled.value : []),
980
+ ...(freshSettled.status === 'fulfilled' ? freshSettled.value : []),
981
+ ];
982
+ redditResults = redditGrocerySettled.status === 'fulfilled' ? redditGrocerySettled.value : [];
983
+ }
984
+ else {
985
+ const [amazonSettled, walmartSettled, bestbuySettled, targetSettled, redditSettled] = await Promise.allSettled([
986
+ searchProvider.searchWeb(`${keyword} site:amazon.com ${isBulk ? '' : 'price'}`, { count: 3 }),
987
+ searchProvider.searchWeb(`${keyword} site:walmart.com price`, { count: 2 }),
988
+ searchProvider.searchWeb(`${keyword} site:bestbuy.com OR site:target.com price`, { count: 2 }),
989
+ isBulk
990
+ ? searchProvider.searchWeb(`${keyword} wholesale bulk site:uline.com OR site:alibaba.com OR site:staples.com OR site:webstaurantstore.com`, { count: 3 })
991
+ : searchProvider.searchWeb(`${keyword} site:ebay.com OR site:etsy.com price`, { count: 3 }),
992
+ searchProvider.searchWeb(`${keyword} reddit review best worth it`, { count: 2 }),
993
+ ]);
994
+ rawResults = [
995
+ ...(amazonSettled.status === 'fulfilled' ? amazonSettled.value : []),
996
+ ...(walmartSettled.status === 'fulfilled' ? walmartSettled.value : []),
997
+ ...(bestbuySettled.status === 'fulfilled' ? bestbuySettled.value : []),
998
+ ...(targetSettled.status === 'fulfilled' ? targetSettled.value : []),
999
+ ];
1000
+ redditResults = redditSettled.status === 'fulfilled' ? redditSettled.value : [];
1001
+ }
1002
+ // Parse structured product listings from search results
1003
+ // DEEP SCRAPE: Visit top marketplace pages to extract real prices (collectibles only)
1004
+ let uniqueListings = [];
1005
+ if (isCollectible) {
1006
+ const scrapableUrls = rawResults
1007
+ .filter(r => r.url && (r.url.includes('tcgplayer.com') ||
1008
+ r.url.includes('ebay.com') ||
1009
+ r.url.includes('amazon.com') ||
1010
+ r.url.includes('etsy.com') ||
1011
+ r.url.includes('mercari.com')))
1012
+ .slice(0, 4)
1013
+ .map(r => r.url);
1014
+ const deepResults = await Promise.allSettled(scrapableUrls.map(url => peel(url, { render: false, timeout: 5000 })
1015
+ .then(result => ({ url, content: result.content, title: result.title, tokens: result.tokens }))
1016
+ .catch(() => null)));
1017
+ const deepListings = [];
1018
+ for (const settled of deepResults) {
1019
+ if (settled.status !== 'fulfilled' || !settled.value)
1020
+ continue;
1021
+ const { url, content: pageContent } = settled.value;
1022
+ if (!pageContent)
1023
+ continue;
1024
+ const sourceName = url.includes('tcgplayer') ? 'TCGPlayer'
1025
+ : url.includes('ebay') ? 'eBay'
1026
+ : url.includes('amazon') ? 'Amazon'
1027
+ : url.includes('etsy') ? 'Etsy'
1028
+ : url.includes('mercari') ? 'Mercari'
1029
+ : new URL(url).hostname;
1030
+ const lines = pageContent.split('\n');
1031
+ for (const line of lines) {
1032
+ const pm = line.match(/\$(\d{1,6}(?:\.\d{2})?)/);
1033
+ if (!pm)
1034
+ continue;
1035
+ const price = parseFloat(pm[1]);
1036
+ if (price < 0.5 || price > 50000)
1037
+ continue;
1038
+ const titleText = line.replace(/\$[\d,.]+/g, '').replace(/[|·\-–—]/g, ' ').trim().slice(0, 100);
1039
+ if (titleText.length < 5)
1040
+ continue;
1041
+ const conditionMatch = line.match(CONDITION_RE);
1042
+ deepListings.push({
1043
+ title: titleText,
1044
+ price: '$' + price.toFixed(2),
1045
+ priceValue: price,
1046
+ url,
1047
+ source: sourceName,
1048
+ condition: conditionMatch ? conditionMatch[1] : undefined,
1049
+ });
1050
+ }
1051
+ }
1052
+ deepListings.sort((a, b) => a.priceValue - b.priceValue);
1053
+ const seen = new Set();
1054
+ uniqueListings = deepListings.filter(l => {
1055
+ const key = l.price + l.source;
1056
+ if (seen.has(key))
1057
+ return false;
1058
+ seen.add(key);
1059
+ return true;
1060
+ }).slice(0, 6);
1061
+ }
1062
+ const fallbackListings = filterToRequestedStore(rawResults, requestedStore)
1063
+ .filter(r => r.url && getStoreInfo(r.url) !== null)
1064
+ .map(r => {
1065
+ const storeInfo = getStoreInfo(r.url);
1066
+ const textToSearch = `${r.title || ''} ${r.snippet || ''}`;
1067
+ // Extract price from snippet/title
1068
+ const price = parsePrice(textToSearch);
1069
+ // Extract rating from snippet
1070
+ const ratingMatch = (r.snippet || '').match(/(\d+(?:\.\d)?)\s*(?:out of 5|stars?|★)/i);
1071
+ const rating = ratingMatch ? parseFloat(ratingMatch[1]) : undefined;
1072
+ // Extract review count
1073
+ const reviewMatch = (r.snippet || '').match(/([\d,]+)\s*(?:ratings?|reviews?)/i);
1074
+ const reviewCount = reviewMatch ? reviewMatch[1].replace(/,/g, '') : undefined;
1075
+ // Clean up title
1076
+ const title = cleanProductTitle(r.title || '');
1077
+ // Extract brand from title — common patterns: "Brand Name Product..." or known brands
1078
+ const KNOWN_BRANDS = /\b(Sony|Bose|Apple|Samsung|LG|JBL|Sennheiser|Audio-Technica|Beats|Jabra|Anker|Soundcore|AKG|Shure|Skullcandy|Plantronics|HyperX|SteelSeries|Razer|Corsair|Logitech|Dell|HP|Lenovo|Asus|Acer|MSI|Microsoft|Google|Amazon|Kindle|Echo|Ring|Roku|Dyson|iRobot|Roomba|Ninja|KitchenAid|Instant Pot|Keurig|Breville|Philips|Panasonic|Canon|Nikon|GoPro|DJI|Fitbit|Garmin|Xiaomi|OnePlus|Nothing|Motorola|Nokia|TCL|Hisense|Vizio|Sonos|Marshall|Bang & Olufsen|B&O|Nike|Adidas|New Balance|Puma|Under Armour|North Face|Patagonia|Columbia|Levi's|Oakley|Ray-Ban|Gucci|Coach|Kate Spade|Michael Kors|Samsonite|Osprey|Yeti|Hydro Flask|Stanley|Weber|Traeger|DeWalt|Makita|Milwaukee|Bosch|Black\+Decker|Craftsman|Ryobi)\b/i;
1079
+ const brandMatch = (r.title || '').match(KNOWN_BRANDS);
1080
+ const brand = brandMatch ? brandMatch[1] : undefined;
1081
+ // Image from SearXNG (imageUrl field if available)
1082
+ const image = r.imageUrl ?? undefined;
1083
+ return {
1084
+ title,
1085
+ brand,
1086
+ price,
1087
+ rating,
1088
+ reviewCount,
1089
+ url: addAffiliateTag(r.url),
1090
+ rawUrl: r.url,
1091
+ snippet: r.snippet,
1092
+ store: storeInfo.store,
1093
+ image,
1094
+ checked: false,
1095
+ };
1096
+ })
1097
+ .slice(0, 10);
1098
+ let checkedProductPages = 0;
1099
+ let verifiedEvidence = [];
1100
+ let retailerRouting = [];
1101
+ if (!isCollectible) {
1102
+ const scoredCandidates = rawResults
1103
+ .filter(r => r.url && getStoreInfo(r.url) !== null)
1104
+ .map(result => ({ result, score: scoreSearchResultCandidate(result, keyword) }))
1105
+ .filter(item => item.score >= 0.4)
1106
+ .sort((a, b) => b.score - a.score);
1107
+ const pickedCandidateUrls = new Set();
1108
+ const coveredStores = new Set();
1109
+ const drillDownCandidates = [];
1110
+ for (const item of scoredCandidates) {
1111
+ const store = getStoreInfo(item.result.url)?.store || item.result.url;
1112
+ if (coveredStores.has(store))
1113
+ continue;
1114
+ coveredStores.add(store);
1115
+ pickedCandidateUrls.add(item.result.url);
1116
+ drillDownCandidates.push(item.result);
1117
+ if (drillDownCandidates.length >= 5)
1118
+ break;
1119
+ }
1120
+ for (const item of scoredCandidates) {
1121
+ if (pickedCandidateUrls.has(item.result.url))
1122
+ continue;
1123
+ pickedCandidateUrls.add(item.result.url);
1124
+ drillDownCandidates.push(item.result);
1125
+ if (drillDownCandidates.length >= 6)
1126
+ break;
1127
+ }
1128
+ const drillDownResults = await Promise.allSettled(drillDownCandidates.map(result => drillDownSearchResult(result, keyword)));
1129
+ for (const settled of drillDownResults) {
1130
+ if (settled.status !== 'fulfilled')
1131
+ continue;
1132
+ checkedProductPages += settled.value.checkedProductPages;
1133
+ retailerRouting.push(...settled.value.routing);
1134
+ if (settled.value.evidence)
1135
+ verifiedEvidence.push(settled.value.evidence);
1136
+ }
1137
+ verifiedEvidence = filterToRequestedStore(dedupeVerifiedEvidence(verifiedEvidence), requestedStore);
1138
+ const seenRouting = new Set();
1139
+ retailerRouting = filterToRequestedStore(retailerRouting, requestedStore).filter(item => {
1140
+ const key = `${item.url}|${item.route}|${item.fetchMethod || ''}|${item.profileUsed || ''}`;
1141
+ if (seenRouting.has(key))
1142
+ return false;
1143
+ seenRouting.add(key);
1144
+ return true;
1145
+ });
1146
+ }
1147
+ let listings = fallbackListings;
1148
+ // Replace listings with deep-scraped results for collectibles (if any found)
1149
+ if (isCollectible && uniqueListings.length > 0) {
1150
+ listings = uniqueListings.map(l => ({
1151
+ title: l.title,
1152
+ brand: undefined,
1153
+ price: l.price,
1154
+ rating: undefined,
1155
+ reviewCount: undefined,
1156
+ url: l.url,
1157
+ rawUrl: l.url,
1158
+ snippet: l.condition ? `Condition: ${l.condition}` : '',
1159
+ store: l.source,
1160
+ image: undefined,
1161
+ condition: l.condition,
1162
+ checked: true,
1163
+ }));
1164
+ }
1165
+ else if (verifiedEvidence.length > 0) {
1166
+ listings = verifiedEvidence.map(item => ({
1167
+ title: item.title,
1168
+ brand: item.brand,
1169
+ model: item.model,
1170
+ price: item.price,
1171
+ rating: undefined,
1172
+ reviewCount: undefined,
1173
+ url: item.url,
1174
+ rawUrl: item.rawUrl,
1175
+ snippet: [item.availability, item.condition, item.localInventoryStatus, item.source === 'retailer-extractor' ? 'adapter-first' : undefined].filter(Boolean).join(' • '),
1176
+ store: item.store,
1177
+ image: item.image,
1178
+ availability: item.availability,
1179
+ condition: item.condition,
1180
+ checked: true,
1181
+ difficulty: item.difficulty,
1182
+ source: item.source,
1183
+ fetchMethod: item.fetchMethod,
1184
+ profileUsed: item.profileUsed,
1185
+ }));
1186
+ }
1187
+ else {
1188
+ listings = fallbackListings.map(item => ({
1189
+ ...item,
1190
+ snippet: item.snippet ? `${item.snippet}${item.price ? '' : ' (search snippet only; price unverified)'}` : (item.price ? '' : 'Search snippet only; price unverified'),
1191
+ }));
1192
+ }
1193
+ const nearbyRetail = await nearbyRetailPromise;
1194
+ const localRetailSection = localRetailIntent && requestedStore && nearbyRetail
1195
+ ? `${buildLocalRetailSection({ keyword, requestedStore, rawResults, verifiedEvidence, nearbyRetail })}\n`
1196
+ : '';
1197
+ const sourceUrl = requestedStore
1198
+ ? buildRequestedStoreSourceUrl(requestedStore, keyword)
1199
+ : addAffiliateTag(`https://www.amazon.com/s?k=${encodeURIComponent(keyword)}`);
1200
+ const heading = requestedStore ? `${keyword} (${requestedStore.store})` : keyword;
1201
+ const content = listings.length > 0
1202
+ ? `# 🛍️ Products — ${heading}\n\n${localRetailSection}${listings.map((l, i) => `${i + 1}. **${l.title}** — ${l.price || 'see price'} [${l.store}](${l.url})${l.checked ? '' : ' _(unverified snippet)_'}\n ${l.snippet || ''}`).join('\n\n')}`
1203
+ : `# 🛍️ Products — ${heading}\n\n${localRetailSection}No structured listings found. Try a more specific query.`;
1204
+ // AI synthesis: recommend best value option
1205
+ let answer;
1206
+ try {
1207
+ const productInfo = listings.length > 0
1208
+ ? listings.slice(0, 5).map(l => `${l.brand ? l.brand + ' ' : ''}${l.title}: ${l.price || 'N/A'} at ${l.store}${l.rating ? `, ${l.rating}★` : ''}${l.reviewCount ? ` (${l.reviewCount} reviews)` : ''}`).join(', ')
1209
+ : 'no specific listings found';
1210
+ const redditSnippets = redditResults.slice(0, 2).map(r => `${r.title}: ${r.snippet || ''}`).join('\n');
1211
+ const deepPriceInfo = uniqueListings.length > 0
1212
+ ? '\n\nReal prices found:\n' + uniqueListings.slice(0, 5).map((l, i) => `${i + 1}. ${l.title} — ${l.price} on ${l.source}${l.condition ? ` (${l.condition})` : ''}`).join('\n')
1213
+ : '';
1214
+ if (isCollectible) {
1215
+ const aiPrompt = `${PROMPT_INJECTION_DEFENSE}You are a collectibles price expert. The user wants: "${sanitizeSearchQuery(intent.query)}". Products found: ${productInfo}.${deepPriceInfo} Reddit says: ${redditSnippets || 'none'}. List the cheapest options with exact prices, condition (near mint/lightly played/etc), and which store. Be specific with dollar amounts. Max 200 words. Cite sources inline as [1], [2], [3].`;
1216
+ const aiText = await callLLMQuick(aiPrompt, { maxTokens: 250, timeoutMs: 8000, temperature: 0.3 });
1217
+ if (aiText && aiText.length > 20)
1218
+ answer = aiText;
1219
+ }
1220
+ else if (localRetailIntent && requestedStore && nearbyRetail) {
1221
+ answer = buildLocalRetailAnswer({ keyword, requestedStore, rawResults, verifiedEvidence, nearbyRetail, checkedProductPages });
1222
+ }
1223
+ else if (verifiedEvidence.length > 0) {
1224
+ answer = buildVerifiedAnswer(keyword, verifiedEvidence, requestedStore);
1225
+ }
1226
+ else {
1227
+ answer = buildFallbackAnswer(keyword, checkedProductPages, requestedStore);
1228
+ }
1229
+ }
1230
+ catch (err) {
1231
+ console.warn('[product-search] LLM synthesis failed (graceful fallback):', err.message);
1232
+ if (!isCollectible) {
1233
+ answer = localRetailIntent && requestedStore && nearbyRetail
1234
+ ? buildLocalRetailAnswer({ keyword, requestedStore, rawResults, verifiedEvidence, nearbyRetail, checkedProductPages })
1235
+ : (verifiedEvidence.length > 0
1236
+ ? buildVerifiedAnswer(keyword, verifiedEvidence, requestedStore)
1237
+ : buildFallbackAnswer(keyword, checkedProductPages, requestedStore));
1238
+ }
1239
+ }
1240
+ return {
1241
+ type: 'products',
1242
+ source: localRetailIntent && requestedStore
1243
+ ? 'Checked product pages + local store search'
1244
+ : (verifiedEvidence.length > 0 ? 'Checked product pages' : (listings.length > 0 ? 'Shopping + Reddit' : 'Web')),
1245
+ sourceUrl,
1246
+ content,
1247
+ title: requestedStore ? `${keyword} — ${requestedStore.store}` : `${keyword} — Shopping`,
1248
+ structured: {
1249
+ requestedStore: requestedStore || undefined,
1250
+ localRetail: localRetailIntent && requestedStore ? {
1251
+ requested: true,
1252
+ location: nearbyRetail?.location,
1253
+ nearbyStoresStatus: nearbyRetail?.status,
1254
+ nearbyStoresSource: nearbyRetail?.source,
1255
+ nearbyStoresMessage: nearbyRetail?.message,
1256
+ nearbyStores: nearbyRetail?.stores?.map(store => ({
1257
+ name: store.name,
1258
+ address: store.address,
1259
+ rating: store.rating,
1260
+ reviewCount: store.reviewCount,
1261
+ isOpen: store.isOpen,
1262
+ googleMapsUrl: store.googleMapsUrl,
1263
+ hours: store.hours,
1264
+ })) || [],
1265
+ catalogExistence: requestedStore ? (verifiedEvidence.some(item => item.store === requestedStore.store)
1266
+ ? 'verified'
1267
+ : (filterToRequestedStore(rawResults, requestedStore).length > 0 ? 'search-result' : 'not-found')) : undefined,
1268
+ localInventoryStatus: verifiedEvidence.find(item => item.store === requestedStore?.store && item.localInventoryVerified && item.localInventoryStatus)?.localInventoryStatus,
1269
+ localInventoryVerified: verifiedEvidence.some(item => item.store === requestedStore?.store && item.localInventoryVerified),
1270
+ } : undefined,
1271
+ listings,
1272
+ checkedProductPages,
1273
+ retailerRouting,
1274
+ verifiedEvidence: verifiedEvidence.map(item => ({
1275
+ title: item.title,
1276
+ brand: item.brand,
1277
+ model: item.model,
1278
+ image: item.image,
1279
+ price: item.price,
1280
+ priceValue: item.priceValue,
1281
+ store: item.store,
1282
+ url: item.url,
1283
+ rawUrl: item.rawUrl,
1284
+ availability: item.availability,
1285
+ condition: item.condition,
1286
+ difficulty: item.difficulty,
1287
+ source: item.source,
1288
+ fetchMethod: item.fetchMethod,
1289
+ profileUsed: item.profileUsed,
1290
+ localInventoryVerified: item.localInventoryVerified,
1291
+ localInventoryStatus: item.localInventoryStatus,
1292
+ })),
1293
+ },
1294
+ tokens: content.split(' ').length,
1295
+ fetchTimeMs: Date.now() - t0,
1296
+ ...(answer !== undefined ? { answer } : {}),
1297
+ sources: [
1298
+ { type: 'shopping', count: listings.length, checkedProductPages },
1299
+ ...(localRetailIntent && requestedStore ? [{
1300
+ type: 'local-retail',
1301
+ requestedStore: requestedStore.store,
1302
+ status: nearbyRetail?.status,
1303
+ location: nearbyRetail?.location,
1304
+ count: nearbyRetail?.stores?.length || 0,
1305
+ }] : []),
1306
+ { type: 'reddit', threads: redditResults.slice(0, 3).map(r => ({ title: r.title, url: r.url, snippet: r.snippet })) },
1307
+ ],
1308
+ };
1309
+ }