@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,997 @@
1
+ /**
2
+ * Jobs commands: serve, mcp, jobs, queue, job, apply, profile, hotels
3
+ */
4
+ import ora from 'ora';
5
+ import { readFileSync } from 'fs';
6
+ import { loadConfig } from '../../cli-auth.js';
7
+ import { writeStdout, formatRelativeTime } from '../utils.js';
8
+ import { listProfiles, deleteProfile, createProfile, getProfilePath } from '../../core/profiles.js';
9
+ import { cleanup } from '../../index.js';
10
+ // ─── Shared job-search logic ─────────────────────────────────────────────────
11
+ async function runJobSearch(keywords, options) {
12
+ const spinner = options.silent ? null : ora('Searching jobs...').start();
13
+ try {
14
+ const { searchJobs } = await import('../../core/jobs.js');
15
+ const VALID_SOURCES = ['glassdoor', 'indeed', 'linkedin', 'upwork'];
16
+ const source = (VALID_SOURCES.includes((options.source ?? 'linkedin'))
17
+ ? options.source
18
+ : 'linkedin');
19
+ const limit = Math.min(Math.max(parseInt(options.limit ?? '25', 10) || 25, 1), 100);
20
+ const fetchDetails = Math.min(Math.max(parseInt(options.details ?? '0', 10) || 0, 0), limit);
21
+ const timeout = parseInt(options.timeout ?? '30000', 10) || 30000;
22
+ const result = await searchJobs({
23
+ keywords,
24
+ location: options.location,
25
+ source,
26
+ limit,
27
+ fetchDetails,
28
+ timeout,
29
+ });
30
+ if (spinner)
31
+ spinner.stop();
32
+ if (options.json) {
33
+ await writeStdout(JSON.stringify(result, null, 2) + '\n');
34
+ process.exit(0);
35
+ }
36
+ const totalLabel = result.totalFound >= 1000
37
+ ? `${(result.totalFound / 1000).toFixed(0).replace(/\.0$/, '')}k+`
38
+ : String(result.totalFound);
39
+ const locationLabel = options.location ? ` in ${options.location}` : '';
40
+ console.log(`\n🔍 Found ${totalLabel} ${keywords} jobs${locationLabel} (${result.source})\n`);
41
+ if (result.jobs.length === 0) {
42
+ console.log(' No jobs found.\n');
43
+ process.exit(0);
44
+ }
45
+ const colNum = 3;
46
+ const colTitle = 40;
47
+ const colCompany = 18;
48
+ const colLocation = 16;
49
+ const colSalary = 14;
50
+ const colPosted = 10;
51
+ const pad = (s, w) => s.length > w ? s.slice(0, w - 1) + '…' : s.padEnd(w);
52
+ const rpad = (s, w) => s.padStart(w);
53
+ console.log(` ${rpad('#', colNum)} ${pad('Title', colTitle)} ${pad('Company', colCompany)} ${pad('Location', colLocation)} ${pad('Salary/Budget', colSalary)} ${pad('Posted', colPosted)}`);
54
+ result.jobs.forEach((job, i) => {
55
+ const titleStr = job.title + (job.remote ? ' 🏠' : '');
56
+ const salaryStr = job.salary ?? ('budget' in job ? job.budget : '') ?? '';
57
+ console.log(` ${rpad(String(i + 1), colNum)} ${pad(titleStr, colTitle)} ${pad(job.company, colCompany)} ${pad(job.location, colLocation)} ${pad(salaryStr, colSalary)} ${pad(job.postedAt ?? '', colPosted)}`);
58
+ });
59
+ const timeSec = (result.timeTakenMs / 1000).toFixed(1);
60
+ const detailsNote = fetchDetails > 0 ? ` | Details: ${result.detailsFetched} fetched` : '';
61
+ console.log(`\nFetched ${result.jobs.length} jobs in ${timeSec}s${detailsNote}\n`);
62
+ const detailedJobs = result.jobs.filter((j) => 'description' in j);
63
+ for (let i = 0; i < detailedJobs.length; i++) {
64
+ const job = detailedJobs[i];
65
+ console.log(`━━━ Job #${i + 1}: ${job.title} ━━━`);
66
+ const metaParts = [`Company: ${job.company}`, `Location: ${job.location}`];
67
+ if (job.salary)
68
+ metaParts.push(`Salary: ${job.salary}`);
69
+ console.log(metaParts.join(' | '));
70
+ const typeParts = [];
71
+ if (job.employmentType)
72
+ typeParts.push(`Type: ${job.employmentType}`);
73
+ if (job.experienceLevel)
74
+ typeParts.push(`Level: ${job.experienceLevel}`);
75
+ if (job.postedAt)
76
+ typeParts.push(`Posted: ${job.postedAt}`);
77
+ if (typeParts.length > 0)
78
+ console.log(typeParts.join(' | '));
79
+ if (job.description) {
80
+ console.log(`\nDescription:\n ${job.description.slice(0, 500).replace(/\n/g, '\n ')}`);
81
+ }
82
+ if (job.requirements && job.requirements.length > 0) {
83
+ console.log(`\nRequirements:`);
84
+ job.requirements.forEach(r => console.log(` • ${r}`));
85
+ }
86
+ if (job.responsibilities && job.responsibilities.length > 0) {
87
+ console.log(`\nResponsibilities:`);
88
+ job.responsibilities.forEach(r => console.log(` • ${r}`));
89
+ }
90
+ if (job.benefits && job.benefits.length > 0) {
91
+ console.log(`\nBenefits:`);
92
+ job.benefits.forEach(b => console.log(` • ${b}`));
93
+ }
94
+ if (job.applyUrl) {
95
+ console.log(`\nApply: ${job.applyUrl}`);
96
+ }
97
+ console.log('');
98
+ }
99
+ process.exit(0);
100
+ }
101
+ catch (error) {
102
+ if (spinner)
103
+ spinner.fail?.('Job search failed');
104
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ // ─── registerJobsCommands ────────────────────────────────────────────────────
109
+ export function registerJobsCommands(program) {
110
+ // ── serve command ─────────────────────────────────────────────────────────
111
+ program
112
+ .command('serve')
113
+ .description('Start API server')
114
+ .option('-p, --port <port>', 'Port number', '3000')
115
+ .action(async (options) => {
116
+ const { startServer } = await import('../../server/app.js');
117
+ startServer({ port: parseInt(options.port, 10) });
118
+ });
119
+ // ── mcp command ───────────────────────────────────────────────────────────
120
+ program
121
+ .command('mcp')
122
+ .description('Start MCP server for Claude Desktop / Cursor')
123
+ .action(async () => {
124
+ await import('../../mcp/server.js');
125
+ });
126
+ // ── jobs command group ────────────────────────────────────────────────────
127
+ const jobsCmd = program
128
+ .command('jobs')
129
+ .description('Job board operations: search listings and auto-apply (LinkedIn, Indeed, Glassdoor, Upwork)')
130
+ .argument('[keywords]', 'Search keywords — shorthand for "jobs search <keywords>"')
131
+ .option('-l, --location <location>', 'Location filter')
132
+ .option('-s, --source <source>', 'Job board: glassdoor, indeed, linkedin, or upwork (default: linkedin)', 'linkedin')
133
+ .option('-n, --limit <number>', 'Max results (default: 25)', '25')
134
+ .option('-d, --details <number>', 'Fetch full details for top N results (default: 0)', '0')
135
+ .option('--json', 'Output raw JSON')
136
+ .option('--timeout <ms>', 'Request timeout in ms (default: 30000)', '30000')
137
+ .option('--silent', 'Silent mode (no spinner)')
138
+ .action(async (keywords, options) => {
139
+ // Default action: when called as `webpeel jobs <keywords>`, act as search
140
+ if (!keywords) {
141
+ jobsCmd.help();
142
+ process.exit(0);
143
+ }
144
+ // Delegate to shared search logic
145
+ await runJobSearch(keywords, options);
146
+ });
147
+ // jobs search <keywords> — explicit subcommand (same logic as default action)
148
+ jobsCmd
149
+ .command('search <keywords>')
150
+ .description('Search job boards for listings (LinkedIn, Indeed, Glassdoor, Upwork)')
151
+ .alias('s')
152
+ .option('-l, --location <location>', 'Location filter')
153
+ .option('-s, --source <source>', 'Job board: glassdoor, indeed, linkedin, or upwork (default: linkedin)', 'linkedin')
154
+ .option('-n, --limit <number>', 'Max results (default: 25)', '25')
155
+ .option('-d, --details <number>', 'Fetch full details for top N results (default: 0)', '0')
156
+ .option('--json', 'Output raw JSON')
157
+ .option('--timeout <ms>', 'Request timeout in ms (default: 30000)', '30000')
158
+ .option('--silent', 'Silent mode (no spinner)')
159
+ .action(async (keywords, options) => {
160
+ await runJobSearch(keywords, options);
161
+ });
162
+ // jobs apply <url> — stealth automated job application
163
+ jobsCmd
164
+ .command('apply <url>')
165
+ .description('Stealth automated job application using human behavior simulation')
166
+ .option('--profile <path>', 'Path to profile JSON file', `${process.env.HOME ?? '~'}/.webpeel/profile.json`)
167
+ .option('--resume <path>', 'Path to resume PDF (overrides profile.resumePath)')
168
+ .option('--mode <mode>', 'Submission mode: auto | review | dry-run (default: review)', 'review')
169
+ .option('--session-dir <path>', 'Browser session directory (preserves login cookies)')
170
+ .option('--llm-key <key>', 'LLM API key for custom question answers')
171
+ .option('--llm-provider <name>', 'LLM provider: openai | anthropic (default: openai)', 'openai')
172
+ .option('--daily-limit <n>', 'Max applications per day (default: 8)', '8')
173
+ .option('--no-warmup', 'Skip browsing warmup phase')
174
+ .option('--json', 'Output result as JSON')
175
+ .option('--silent', 'Minimal output')
176
+ .action(async (url, options) => {
177
+ const isSilent = options.silent;
178
+ const isJson = options.json;
179
+ const mode = (['auto', 'review', 'dry-run'].includes(options.mode)
180
+ ? options.mode
181
+ : 'review');
182
+ if (!isSilent) {
183
+ console.log(`\n🤖 WebPeel Auto-Apply — mode: ${mode}`);
184
+ console.log(` URL: ${url}\n`);
185
+ }
186
+ // Load profile
187
+ const profilePath = options.profile;
188
+ let profile;
189
+ try {
190
+ const raw = readFileSync(profilePath, 'utf-8');
191
+ profile = JSON.parse(raw);
192
+ }
193
+ catch {
194
+ console.error(`Error: Could not load profile from ${profilePath}`);
195
+ console.error(`Run "webpeel jobs apply-setup" to create a profile.`);
196
+ process.exit(1);
197
+ }
198
+ if (options.resume) {
199
+ profile.resumePath = options.resume;
200
+ }
201
+ const spinner = isSilent ? null : ora('Applying...').start();
202
+ try {
203
+ const { applyToJob } = await import('../../core/apply.js');
204
+ const result = await applyToJob({
205
+ url,
206
+ profile,
207
+ mode,
208
+ sessionDir: options.sessionDir,
209
+ llmKey: options.llmKey,
210
+ llmProvider: options.llmProvider,
211
+ dailyLimit: parseInt(options.dailyLimit, 10) || 8,
212
+ warmup: options.warmup !== false,
213
+ onProgress: isSilent
214
+ ? undefined
215
+ : (event) => {
216
+ if (spinner)
217
+ spinner.text = `[${event.stage}] ${event.message}`;
218
+ else
219
+ console.log(` [${event.stage}] ${event.message}`);
220
+ },
221
+ });
222
+ if (spinner)
223
+ spinner.stop();
224
+ if (isJson) {
225
+ await writeStdout(JSON.stringify(result, null, 2) + '\n');
226
+ process.exit(result.error ? 1 : 0);
227
+ }
228
+ const statusIcon = result.submitted ? '✅' : result.error ? '❌' : '📋';
229
+ console.log(`\n${statusIcon} ${result.submitted
230
+ ? 'Application submitted!'
231
+ : result.error
232
+ ? `Error: ${result.error}`
233
+ : 'Application completed (not submitted)'}`);
234
+ if (result.job.title || result.job.company) {
235
+ console.log(` ${result.job.title}${result.job.company ? ` @ ${result.job.company}` : ''}`);
236
+ }
237
+ console.log(`\n Fields filled: ${result.fieldsFilled}`);
238
+ if (result.llmAnswers > 0)
239
+ console.log(` LLM answers: ${result.llmAnswers}`);
240
+ if (result.fieldsSkipped.length > 0)
241
+ console.log(` Skipped: ${result.fieldsSkipped.join(', ')}`);
242
+ if (result.warnings.length > 0 && !isSilent) {
243
+ console.log(`\n Warnings:`);
244
+ result.warnings.forEach(w => console.log(` ⚠️ ${w}`));
245
+ }
246
+ console.log(` Time: ${(result.elapsed / 1000).toFixed(1)}s\n`);
247
+ process.exit(result.error ? 1 : 0);
248
+ }
249
+ catch (error) {
250
+ if (spinner)
251
+ spinner.fail('Application failed');
252
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
253
+ process.exit(1);
254
+ }
255
+ });
256
+ // jobs apply-setup — interactive wizard to create ~/.webpeel/profile.json
257
+ jobsCmd
258
+ .command('apply-setup')
259
+ .description('Interactive setup wizard — creates ~/.webpeel/profile.json')
260
+ .action(async () => {
261
+ const { createInterface } = await import('readline');
262
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
263
+ const ask = (q) => new Promise(resolve => rl.question(q, ans => resolve(ans.trim())));
264
+ console.log('\n🤖 WebPeel Apply Setup — Create your applicant profile\n');
265
+ console.log('This creates ~/.webpeel/profile.json used by "webpeel jobs apply".\n');
266
+ try {
267
+ const name = await ask('Full name: ');
268
+ const email = await ask('Email address: ');
269
+ const phone = await ask('Phone number: ');
270
+ const linkedin = await ask('LinkedIn URL (optional, press Enter to skip): ');
271
+ const website = await ask('Portfolio/website URL (optional): ');
272
+ const location = await ask('City, State (e.g. San Francisco, CA): ');
273
+ const workAuth = await ask('Work authorization (e.g. US Citizen, Permanent Resident, H-1B, Need Sponsorship): ');
274
+ const yearsExp = await ask('Years of experience: ');
275
+ const currentTitle = await ask('Current/most recent job title: ');
276
+ const skills = await ask('Skills (comma-separated, e.g. TypeScript, React, Node.js): ');
277
+ const education = await ask('Education (e.g. B.S. Computer Science, MIT): ');
278
+ const resumePath = await ask('Path to resume PDF (e.g. /Users/you/resume.pdf): ');
279
+ const summary = await ask('Professional summary (1-3 sentences): ');
280
+ const salaryMin = await ask('Minimum desired salary (optional, e.g. 120000): ');
281
+ const salaryMax = await ask('Maximum desired salary (optional, e.g. 180000): ');
282
+ const relocate = await ask('Willing to relocate? (y/n): ');
283
+ const sponsorship = await ask('Need visa sponsorship? (y/n): ');
284
+ rl.close();
285
+ const profileData = {
286
+ name,
287
+ email,
288
+ phone,
289
+ ...(linkedin ? { linkedin } : {}),
290
+ ...(website ? { website } : {}),
291
+ location,
292
+ workAuthorization: workAuth,
293
+ yearsExperience: parseInt(yearsExp, 10) || 0,
294
+ currentTitle,
295
+ skills: skills.split(',').map(s => s.trim()).filter(Boolean),
296
+ education,
297
+ resumePath,
298
+ summary,
299
+ ...(salaryMin && salaryMax
300
+ ? { salaryRange: { min: parseInt(salaryMin, 10), max: parseInt(salaryMax, 10) } }
301
+ : {}),
302
+ willingToRelocate: relocate.toLowerCase().startsWith('y'),
303
+ needsSponsorship: sponsorship.toLowerCase().startsWith('y'),
304
+ };
305
+ const { mkdirSync: mk, writeFileSync: wf, existsSync: ex } = await import('fs');
306
+ const { join: j } = await import('path');
307
+ const { homedir: hd } = await import('os');
308
+ const webpeelDir = j(hd(), '.webpeel');
309
+ if (!ex(webpeelDir))
310
+ mk(webpeelDir, { recursive: true });
311
+ const applyProfilePath = j(webpeelDir, 'profile.json');
312
+ wf(applyProfilePath, JSON.stringify(profileData, null, 2), 'utf-8');
313
+ console.log(`\n✅ Profile saved to: ${applyProfilePath}`);
314
+ console.log('\nNext steps:');
315
+ console.log(' 1. Apply to a job: webpeel jobs apply https://linkedin.com/jobs/view/...');
316
+ console.log(' (First run opens a browser — log in to LinkedIn, then the session is saved)\n');
317
+ }
318
+ catch (error) {
319
+ rl.close();
320
+ console.error(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`);
321
+ process.exit(1);
322
+ }
323
+ });
324
+ // jobs apply-history — view application history
325
+ jobsCmd
326
+ .command('apply-history')
327
+ .description('View application history from ~/.webpeel/applications.json')
328
+ .option('--json', 'Output as JSON')
329
+ .option('--limit <n>', 'Number of recent applications to show (default: 20)', '20')
330
+ .action(async (options) => {
331
+ const isJson = options.json;
332
+ const limit = parseInt(options.limit, 10) || 20;
333
+ try {
334
+ const { loadApplications } = await import('../../core/apply.js');
335
+ const allApps = loadApplications();
336
+ const apps = allApps.slice().reverse().slice(0, limit);
337
+ if (isJson) {
338
+ await writeStdout(JSON.stringify(apps, null, 2) + '\n');
339
+ process.exit(0);
340
+ }
341
+ if (apps.length === 0) {
342
+ console.log('\nNo applications yet. Use "webpeel jobs apply <url>" to start.\n');
343
+ process.exit(0);
344
+ }
345
+ console.log(`\n📋 Application History (${apps.length} of ${allApps.length} total)\n`);
346
+ const colDate = 22;
347
+ const colStatus = 10;
348
+ const colTitle = 35;
349
+ const colCompany = 20;
350
+ const colMode = 8;
351
+ const pad = (s, w) => (s.length > w ? s.slice(0, w - 1) + '…' : s.padEnd(w));
352
+ console.log(` ${pad('Applied', colDate)} ${pad('Status', colStatus)} ${pad('Title', colTitle)} ${pad('Company', colCompany)} ${pad('Mode', colMode)}`);
353
+ console.log(` ${'-'.repeat(colDate)} ${'-'.repeat(colStatus)} ${'-'.repeat(colTitle)} ${'-'.repeat(colCompany)} ${'-'.repeat(colMode)}`);
354
+ for (const app of apps) {
355
+ const date = new Date(app.appliedAt).toLocaleString('en-US', {
356
+ month: 'short',
357
+ day: 'numeric',
358
+ year: 'numeric',
359
+ hour: '2-digit',
360
+ minute: '2-digit',
361
+ });
362
+ const statusEmoji = { applied: '📤', interview: '🎯', offer: '🎉', rejected: '❌', withdrawn: '🚫' }[app.status] ?? '';
363
+ console.log(` ${pad(date, colDate)} ${pad(`${statusEmoji} ${app.status}`, colStatus)} ${pad(app.title, colTitle)} ${pad(app.company, colCompany)} ${pad(app.mode, colMode)}`);
364
+ }
365
+ const today = new Date().toISOString().slice(0, 10);
366
+ const todayCount = allApps.filter(a => a.appliedAt.startsWith(today)).length;
367
+ console.log(`\n Today: ${todayCount} application(s)\n`);
368
+ process.exit(0);
369
+ }
370
+ catch (error) {
371
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
372
+ process.exit(1);
373
+ }
374
+ });
375
+ // ── queue command ─────────────────────────────────────────────────────────
376
+ program
377
+ .command('queue')
378
+ .description('List active async jobs (crawl, batch)')
379
+ .option('--json', 'Output as JSON')
380
+ .action(async (options) => {
381
+ try {
382
+ const config = loadConfig();
383
+ if (!config.apiKey) {
384
+ console.error('Error: API key required. Run `webpeel login` first.');
385
+ process.exit(1);
386
+ }
387
+ const { fetch: undiciFetch } = await import('undici');
388
+ const response = await undiciFetch(`${process.env.WEBPEEL_API_URL || 'https://api.webpeel.dev'}/v1/jobs`, {
389
+ headers: {
390
+ 'Authorization': `Bearer ${config.apiKey}`,
391
+ },
392
+ });
393
+ if (!response.ok) {
394
+ throw new Error(`API error: HTTP ${response.status}`);
395
+ }
396
+ const data = await response.json();
397
+ const jobs = data.jobs || data;
398
+ if (options.json) {
399
+ console.log(JSON.stringify(data, null, 2));
400
+ }
401
+ else {
402
+ if (!Array.isArray(jobs) || jobs.length === 0) {
403
+ console.log('No active jobs.');
404
+ }
405
+ else {
406
+ console.log(`Active Jobs (${jobs.length}):\n`);
407
+ for (const job of jobs) {
408
+ console.log(`ID: ${job.id}`);
409
+ console.log(`Type: ${job.type}`);
410
+ console.log(`Status: ${job.status}`);
411
+ console.log(`URL: ${job.url}`);
412
+ console.log(`Created: ${job.createdAt}`);
413
+ console.log('---');
414
+ }
415
+ }
416
+ }
417
+ process.exit(0);
418
+ }
419
+ catch (error) {
420
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
421
+ process.exit(1);
422
+ }
423
+ });
424
+ // ── job command ───────────────────────────────────────────────────────────
425
+ program
426
+ .command('job <id>')
427
+ .description('Get status of a specific job')
428
+ .option('--json', 'Output as JSON')
429
+ .action(async (id, options) => {
430
+ try {
431
+ const config = loadConfig();
432
+ if (!config.apiKey) {
433
+ console.error('Error: API key required. Run `webpeel login` first.');
434
+ process.exit(1);
435
+ }
436
+ const { fetch: undiciFetch } = await import('undici');
437
+ const response = await undiciFetch(`${process.env.WEBPEEL_API_URL || 'https://api.webpeel.dev'}/v1/jobs/${id}`, {
438
+ headers: {
439
+ 'Authorization': `Bearer ${config.apiKey}`,
440
+ },
441
+ });
442
+ if (!response.ok) {
443
+ throw new Error(`API error: HTTP ${response.status}`);
444
+ }
445
+ const job = await response.json();
446
+ if (options.json) {
447
+ console.log(JSON.stringify(job, null, 2));
448
+ }
449
+ else {
450
+ console.log(`Job ID: ${job.id}`);
451
+ console.log(`Type: ${job.type}`);
452
+ console.log(`Status: ${job.status}`);
453
+ console.log(`URL: ${job.url}`);
454
+ console.log(`Created: ${job.createdAt}`);
455
+ if (job.completedAt) {
456
+ console.log(`Completed: ${job.completedAt}`);
457
+ }
458
+ if (job.error) {
459
+ console.log(`Error: ${job.error}`);
460
+ }
461
+ if (job.results) {
462
+ console.log(`\nResults: ${job.results.length} items`);
463
+ if (job.type === 'crawl' && job.results.length > 0) {
464
+ console.log('\nFirst 5 URLs:');
465
+ for (const result of job.results.slice(0, 5)) {
466
+ console.log(` - ${result.url}`);
467
+ }
468
+ }
469
+ }
470
+ }
471
+ process.exit(0);
472
+ }
473
+ catch (error) {
474
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
475
+ process.exit(1);
476
+ }
477
+ });
478
+ // ── apply command group ───────────────────────────────────────────────────
479
+ const applyCmd = program
480
+ .command('apply')
481
+ .description('Auto-apply pipeline: submit applications, track history, manage rate limits');
482
+ // apply submit <url> — auto-apply to a job posting
483
+ applyCmd
484
+ .command('submit <url>')
485
+ .description('Auto-apply to a job posting')
486
+ .alias('s')
487
+ .option('--profile-path <path>', 'Path to apply profile JSON', `${process.env.HOME ?? '~'}/.webpeel/profile.json`)
488
+ .option('--browser-profile <path>', 'Path to persistent browser data dir', `${process.env.HOME ?? '~'}/.webpeel/browser-profile`)
489
+ .option('--headed', 'Run browser visibly (default for apply)')
490
+ .option('--headless', 'Run browser invisibly')
491
+ .option('--confirm', 'Pause for confirmation before submit (default: true)')
492
+ .option('--no-confirm', 'Skip confirmation, auto-submit')
493
+ .option('--dry-run', 'Go through flow but do not submit')
494
+ .option('--generate-cover', 'Generate tailored cover letter (needs OPENAI_API_KEY)')
495
+ .option('--timeout <ms>', 'Timeout in ms (default: 300000)', '300000')
496
+ .option('--json', 'Output result as JSON')
497
+ .option('--silent', 'Silent mode')
498
+ .action(async (url, options) => {
499
+ const isSilent = options.silent;
500
+ const isJson = options.json;
501
+ // Load profile
502
+ const profilePath = options.profilePath;
503
+ let profile;
504
+ try {
505
+ const raw = readFileSync(profilePath, 'utf-8');
506
+ profile = JSON.parse(raw);
507
+ }
508
+ catch {
509
+ const msg = `Could not load profile from ${profilePath}. Run "webpeel apply init" to create one.`;
510
+ if (isJson) {
511
+ await writeStdout(JSON.stringify({ success: false, error: { type: 'fetch_failed', message: msg } }) + '\n');
512
+ }
513
+ else {
514
+ console.error(`Error: ${msg}`);
515
+ }
516
+ process.exit(1);
517
+ }
518
+ const spinner = isSilent ? null : ora('Applying...').start();
519
+ try {
520
+ const { applyToJob } = await import('../../core/apply.js');
521
+ const result = await applyToJob({
522
+ url,
523
+ profile,
524
+ // Use sessionDir for persistent session storage (renamed from browserProfile)
525
+ sessionDir: options.browserProfile,
526
+ // Map dryRun flag → mode: 'dry-run'
527
+ mode: (options.dryRun ? 'dry-run' : (options.noConfirm ? 'auto' : 'review')),
528
+ timeout: parseInt(options.timeout, 10) || 300_000,
529
+ });
530
+ if (spinner)
531
+ spinner.stop();
532
+ // Normalize result to a consistent output shape
533
+ const success = result.submitted && !result.error;
534
+ const jobTitle = result.job?.title ?? '';
535
+ const jobCompany = result.job?.company ?? '';
536
+ if (isJson) {
537
+ await writeStdout(JSON.stringify(result, null, 2) + '\n');
538
+ process.exit(success ? 0 : 1);
539
+ }
540
+ const icon = success ? '✅' : '❌';
541
+ console.log(`\n${icon} ${success ? 'Application submitted!' : `Failed: ${result.error ?? 'Unknown error'}`}`);
542
+ if (jobTitle)
543
+ console.log(` ${jobTitle}${jobCompany ? ` @ ${jobCompany}` : ''}`);
544
+ if (options.dryRun)
545
+ console.log(' (Dry run — not submitted)');
546
+ console.log(` Time: ${(result.elapsed / 1000).toFixed(1)}s\n`);
547
+ process.exit(success ? 0 : 1);
548
+ }
549
+ catch (error) {
550
+ if (spinner)
551
+ spinner.fail('Application failed');
552
+ const msg = error instanceof Error ? error.message : 'Unknown error';
553
+ if (isJson) {
554
+ await writeStdout(JSON.stringify({ success: false, error: { type: 'fetch_failed', message: msg } }) + '\n');
555
+ }
556
+ else {
557
+ console.error(`Error: ${msg}`);
558
+ }
559
+ process.exit(1);
560
+ }
561
+ });
562
+ // apply init — interactive profile setup
563
+ applyCmd
564
+ .command('init')
565
+ .description('Interactive profile setup — creates ~/.webpeel/profile.json')
566
+ .action(async () => {
567
+ const { createInterface } = await import('readline');
568
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
569
+ const ask = (q) => new Promise((resolve) => rl.question(q, (ans) => resolve(ans.trim())));
570
+ console.log('\n🤖 WebPeel Apply Setup — Create your applicant profile\n');
571
+ console.log('This creates ~/.webpeel/profile.json used by "webpeel apply submit".\n');
572
+ try {
573
+ const name = await ask('Full name: ');
574
+ const email = await ask('Email address: ');
575
+ const phone = await ask('Phone number (optional): ');
576
+ const resumePath = await ask('Path to resume PDF (e.g. /Users/you/resume.pdf): ');
577
+ const currentTitle = await ask('Current/most recent job title: ');
578
+ const yearsExp = await ask('Years of experience: ');
579
+ const skills = await ask('Skills (comma-separated, e.g. TypeScript, React, Node.js): ');
580
+ const education = await ask('Education (e.g. B.S. Computer Science, MIT): ');
581
+ const location = await ask('City, State (e.g. San Francisco, CA): ');
582
+ const workAuth = await ask('Work authorization (e.g. US Citizen, Permanent Resident, H-1B, Need Sponsorship): ');
583
+ const linkedinUrl = await ask('LinkedIn URL (optional): ');
584
+ const websiteUrl = await ask('Portfolio/website URL (optional): ');
585
+ const desiredSalary = await ask('Desired salary (optional, e.g. $150,000): ');
586
+ rl.close();
587
+ const { mkdirSync: mk, writeFileSync: wf } = await import('fs');
588
+ const { join: j } = await import('path');
589
+ const { homedir: hd } = await import('os');
590
+ const webpeelDir = j(hd(), '.webpeel');
591
+ mk(webpeelDir, { recursive: true });
592
+ const applyInitProfile = {
593
+ name,
594
+ email,
595
+ ...(phone ? { phone } : {}),
596
+ resumePath,
597
+ currentTitle,
598
+ yearsExperience: parseInt(yearsExp, 10) || 0,
599
+ skills: skills.split(',').map((s) => s.trim()).filter(Boolean),
600
+ education,
601
+ location,
602
+ workAuthorization: workAuth,
603
+ ...(linkedinUrl ? { linkedinUrl } : {}),
604
+ ...(websiteUrl ? { websiteUrl } : {}),
605
+ ...(desiredSalary ? { desiredSalary } : {}),
606
+ };
607
+ const initProfilePath = j(webpeelDir, 'profile.json');
608
+ wf(initProfilePath, JSON.stringify(applyInitProfile, null, 2), 'utf-8');
609
+ console.log(`\n✅ Profile saved to: ${initProfilePath}`);
610
+ console.log('\nNext steps:');
611
+ console.log(' • Apply to a job: webpeel apply submit <url>');
612
+ console.log(' • Dry run first: webpeel apply submit <url> --dry-run');
613
+ console.log(' • View stats: webpeel apply status\n');
614
+ }
615
+ catch (error) {
616
+ rl.close();
617
+ console.error(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`);
618
+ process.exit(1);
619
+ }
620
+ });
621
+ // apply status — application stats summary
622
+ applyCmd
623
+ .command('status')
624
+ .description('Show application stats')
625
+ .option('--json', 'Output as JSON')
626
+ .action(async (options) => {
627
+ try {
628
+ const { ApplicationTracker } = await import('../../core/application-tracker.js');
629
+ const tracker = new ApplicationTracker();
630
+ const stats = tracker.stats();
631
+ if (options.json) {
632
+ await writeStdout(JSON.stringify(stats, null, 2) + '\n');
633
+ process.exit(0);
634
+ }
635
+ console.log('\n📊 Application Stats\n');
636
+ console.log(` Total: ${stats.total}`);
637
+ console.log(` Today: ${stats.today}`);
638
+ console.log(` This week: ${stats.thisWeek}`);
639
+ if (Object.keys(stats.byPlatform).length > 0) {
640
+ console.log('\n By Platform:');
641
+ for (const [platform, count] of Object.entries(stats.byPlatform)) {
642
+ console.log(` ${platform.padEnd(12)} ${count}`);
643
+ }
644
+ }
645
+ if (Object.keys(stats.byStatus).length > 0) {
646
+ console.log('\n By Status:');
647
+ for (const [status, count] of Object.entries(stats.byStatus)) {
648
+ console.log(` ${status.padEnd(12)} ${count}`);
649
+ }
650
+ }
651
+ console.log('');
652
+ process.exit(0);
653
+ }
654
+ catch (error) {
655
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
656
+ process.exit(1);
657
+ }
658
+ });
659
+ // apply list — list applications with optional filters
660
+ applyCmd
661
+ .command('list')
662
+ .description('List tracked applications')
663
+ .option('--platform <platform>', 'Filter by platform (e.g. linkedin, upwork)')
664
+ .option('--status <status>', 'Filter by status (applied, interview, rejected, offer, ...)')
665
+ .option('--since <date>', 'Filter to applications on or after this date (YYYY-MM-DD)')
666
+ .option('--json', 'Output as JSON')
667
+ .option('--limit <n>', 'Max records to show (default: 50)', '50')
668
+ .action(async (options) => {
669
+ try {
670
+ const { ApplicationTracker } = await import('../../core/application-tracker.js');
671
+ const tracker = new ApplicationTracker();
672
+ const limit = parseInt(options.limit, 10) || 50;
673
+ const records = tracker.list({
674
+ platform: options.platform,
675
+ status: options.status,
676
+ since: options.since,
677
+ }).slice(0, limit);
678
+ if (options.json) {
679
+ await writeStdout(JSON.stringify(records, null, 2) + '\n');
680
+ process.exit(0);
681
+ }
682
+ if (records.length === 0) {
683
+ console.log('\nNo applications found.\n');
684
+ process.exit(0);
685
+ }
686
+ console.log(`\n📋 Applications (${records.length})\n`);
687
+ const colDate = 12;
688
+ const colStatus = 10;
689
+ const colTitle = 35;
690
+ const colCompany = 20;
691
+ const pad = (s, w) => s.length > w ? s.slice(0, w - 1) + '…' : s.padEnd(w);
692
+ console.log(` ${'Date'.padEnd(colDate)} ${'Status'.padEnd(colStatus)} ${'Title'.padEnd(colTitle)} ${'Company'.padEnd(colCompany)}`);
693
+ console.log(` ${'-'.repeat(colDate)} ${'-'.repeat(colStatus)} ${'-'.repeat(colTitle)} ${'-'.repeat(colCompany)}`);
694
+ for (const r of records) {
695
+ const dateStr = r.appliedAt.slice(0, 10);
696
+ console.log(` ${pad(dateStr, colDate)} ${pad(r.status, colStatus)} ${pad(r.title, colTitle)} ${pad(r.company, colCompany)}`);
697
+ }
698
+ console.log('');
699
+ process.exit(0);
700
+ }
701
+ catch (error) {
702
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
703
+ process.exit(1);
704
+ }
705
+ });
706
+ // apply rate — rate governor status
707
+ applyCmd
708
+ .command('rate')
709
+ .description('Show rate governor status (daily limits, cooldown, next allowed time)')
710
+ .option('--json', 'Output as JSON')
711
+ .option('--reset-cooldown', 'Clear any active cooldown (manual override)')
712
+ .action(async (options) => {
713
+ try {
714
+ const { RateGovernor, formatDuration } = await import('../../core/rate-governor.js');
715
+ const governor = new RateGovernor();
716
+ if (options.resetCooldown) {
717
+ governor.resetCooldown();
718
+ console.log('✅ Cooldown cleared.');
719
+ process.exit(0);
720
+ }
721
+ const state = governor.getState();
722
+ const config = governor.getConfig();
723
+ const check = governor.canApply();
724
+ if (options.json) {
725
+ await writeStdout(JSON.stringify({
726
+ state,
727
+ config,
728
+ canApply: check.allowed,
729
+ reason: check.reason,
730
+ waitMs: check.waitMs,
731
+ nextDelayMs: governor.getNextDelay(),
732
+ }, null, 2) + '\n');
733
+ process.exit(0);
734
+ }
735
+ console.log('\n⏱ Rate Governor Status\n');
736
+ console.log(` Today's applications: ${state.todayCount} / ${config.maxPerDay}`);
737
+ console.log(` Total applications: ${state.totalApplications}`);
738
+ console.log(` Can apply now: ${check.allowed ? '✅ Yes' : '❌ No'}`);
739
+ if (!check.allowed && check.reason) {
740
+ console.log(` Reason: ${check.reason}`);
741
+ }
742
+ if (!check.allowed && check.waitMs) {
743
+ console.log(` Wait time: ${formatDuration(check.waitMs)}`);
744
+ }
745
+ if (state.cooldownUntil > 0) {
746
+ const remaining = state.cooldownUntil - Date.now();
747
+ console.log(` Cooldown: Active (${formatDuration(Math.max(0, remaining))} remaining)`);
748
+ }
749
+ console.log(` Min delay: ${formatDuration(config.minDelayMs)}`);
750
+ console.log(` Max delay: ${formatDuration(config.maxDelayMs)}`);
751
+ console.log(` Active hours: ${config.activeHours[0]}:00 – ${config.activeHours[1]}:00`);
752
+ console.log(` Weekdays only: ${config.weekdaysOnly ? 'Yes' : 'No'}`);
753
+ console.log('');
754
+ process.exit(0);
755
+ }
756
+ catch (error) {
757
+ console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
758
+ process.exit(1);
759
+ }
760
+ });
761
+ // ── profile command group ─────────────────────────────────────────────────
762
+ const profileCmd = program
763
+ .command('profile')
764
+ .description('Manage named browser profiles (saved login sessions)');
765
+ profileCmd
766
+ .command('create <name>')
767
+ .description('Create a new profile interactively (launches browser, log in, press Ctrl+C when done)')
768
+ .option('--description <text>', 'Optional description for this profile')
769
+ .action(async (name, opts) => {
770
+ try {
771
+ await createProfile(name, opts.description);
772
+ process.exit(0);
773
+ }
774
+ catch (error) {
775
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
776
+ process.exit(1);
777
+ }
778
+ });
779
+ profileCmd
780
+ .command('list')
781
+ .description('List all saved browser profiles')
782
+ .action(() => {
783
+ const profiles = listProfiles();
784
+ if (profiles.length === 0) {
785
+ console.log('No profiles found.');
786
+ console.log('');
787
+ console.log('Create one with:');
788
+ console.log(' webpeel profile create <name>');
789
+ console.log('');
790
+ console.log('Then use it with:');
791
+ console.log(' webpeel <url> --profile <name>');
792
+ process.exit(0);
793
+ }
794
+ console.log('');
795
+ console.log('Saved profiles:');
796
+ console.log('');
797
+ // Column widths
798
+ const nameW = Math.max(8, ...profiles.map((p) => p.name.length));
799
+ const domainsW = Math.max(10, ...profiles.map((p) => (p.domains.join(', ') || '(none)').length));
800
+ const header = 'Name'.padEnd(nameW) + ' ' +
801
+ 'Domains'.padEnd(domainsW) + ' ' +
802
+ 'Last Used'.padEnd(12) + ' ' +
803
+ 'Created';
804
+ console.log(header);
805
+ console.log('─'.repeat(header.length + 4));
806
+ for (const p of profiles) {
807
+ const domainsStr = p.domains.length > 0 ? p.domains.join(', ') : '(none)';
808
+ const lastUsed = formatRelativeTime(new Date(p.lastUsed));
809
+ const created = new Date(p.created).toISOString().split('T')[0];
810
+ console.log(p.name.padEnd(nameW) + ' ' +
811
+ domainsStr.padEnd(domainsW) + ' ' +
812
+ lastUsed.padEnd(12) + ' ' +
813
+ created);
814
+ }
815
+ console.log('');
816
+ process.exit(0);
817
+ });
818
+ profileCmd
819
+ .command('show <name>')
820
+ .description('Show details for a profile')
821
+ .action((name) => {
822
+ const profilePath = getProfilePath(name);
823
+ if (!profilePath) {
824
+ console.error(`Error: Profile "${name}" not found.`);
825
+ console.error('Run "webpeel profile list" to see available profiles.');
826
+ process.exit(1);
827
+ }
828
+ try {
829
+ const meta = JSON.parse(readFileSync(`${profilePath}/metadata.json`, 'utf-8'));
830
+ console.log('');
831
+ console.log(`Profile: ${meta.name}`);
832
+ if (meta.description)
833
+ console.log(`Description: ${meta.description}`);
834
+ console.log(`Created: ${new Date(meta.created).toLocaleString()}`);
835
+ console.log(`Last used: ${new Date(meta.lastUsed).toLocaleString()}`);
836
+ console.log(`Domains: ${meta.domains.length > 0 ? meta.domains.join(', ') : '(none)'}`);
837
+ console.log(`Directory: ${profilePath}`);
838
+ console.log('');
839
+ process.exit(0);
840
+ }
841
+ catch (e) {
842
+ console.error(`Error reading profile: ${e instanceof Error ? e.message : String(e)}`);
843
+ process.exit(1);
844
+ }
845
+ });
846
+ profileCmd
847
+ .command('delete <name>')
848
+ .description('Delete a saved profile')
849
+ .action((name) => {
850
+ const deleted = deleteProfile(name);
851
+ if (deleted) {
852
+ console.log(`Profile "${name}" deleted.`);
853
+ process.exit(0);
854
+ }
855
+ else {
856
+ console.error(`Error: Profile "${name}" not found.`);
857
+ console.error('Run "webpeel profile list" to see available profiles.');
858
+ process.exit(1);
859
+ }
860
+ });
861
+ // ── hotels command ────────────────────────────────────────────────────────
862
+ program
863
+ .command('hotels <destination>')
864
+ .description('Search multiple travel sites for hotels (Kayak, Booking.com, Google Travel)')
865
+ .option('--checkin <date>', 'Check-in date (ISO or relative, e.g. "tomorrow", "2026-02-20"). Default: tomorrow')
866
+ .option('--checkout <date>', 'Check-out date (ISO or relative). Default: checkin + 1 day')
867
+ .option('--sort <method>', 'Sort by: price, rating, value (default: price)', 'price')
868
+ .option('--limit <n>', 'Max results (default: 20)', '20')
869
+ .option('--source <name...>', 'Only use specific source(s): kayak, booking, google (repeatable)')
870
+ .option('--json', 'Output as JSON')
871
+ .option('--stealth', 'Use stealth mode for all sources')
872
+ .option('--proxy <url>', 'Proxy URL for requests (http://host:port, socks5://user:pass@host:port)')
873
+ .option('-s, --silent', 'Suppress progress messages')
874
+ .action(async (destination, options) => {
875
+ const isJson = options.json;
876
+ const isSilent = options.silent;
877
+ // Build checkin/checkout
878
+ const { parseDate, addDays: hotelAddDays } = await import('../../core/hotel-search.js');
879
+ let checkinStr;
880
+ let checkoutStr;
881
+ try {
882
+ checkinStr = parseDate(options.checkin ?? 'tomorrow');
883
+ checkoutStr = options.checkout
884
+ ? parseDate(options.checkout)
885
+ : hotelAddDays(checkinStr, 1);
886
+ }
887
+ catch (err) {
888
+ const msg = err instanceof Error ? err.message : String(err);
889
+ if (isJson) {
890
+ await writeStdout(JSON.stringify({ success: false, error: { type: 'invalid_request', message: msg } }) + '\n');
891
+ }
892
+ else {
893
+ console.error(`Error: ${msg}`);
894
+ }
895
+ process.exit(1);
896
+ }
897
+ const sortMethod = (['price', 'rating', 'value'].includes(options.sort)
898
+ ? options.sort
899
+ : 'price');
900
+ const limit = Math.max(1, parseInt(options.limit, 10) || 20);
901
+ const sources = options.source
902
+ ? (Array.isArray(options.source) ? options.source : [options.source])
903
+ : undefined;
904
+ // Spinner per-source progress (non-silent, non-JSON)
905
+ let searchSpinner = null;
906
+ if (!isSilent && !isJson) {
907
+ searchSpinner = ora(`Searching hotels in ${destination}...`).start();
908
+ }
909
+ else if (!isSilent && !isJson) {
910
+ console.error(`⏳ Searching kayak.com...`);
911
+ console.error(`⏳ Searching booking.com...`);
912
+ console.error(`⏳ Searching google.com...`);
913
+ }
914
+ try {
915
+ const { searchHotels } = await import('../../core/hotel-search.js');
916
+ const result = await searchHotels({
917
+ destination,
918
+ checkin: checkinStr,
919
+ checkout: checkoutStr,
920
+ sort: sortMethod,
921
+ limit,
922
+ sources,
923
+ stealth: options.stealth,
924
+ silent: isSilent,
925
+ proxy: options.proxy,
926
+ });
927
+ if (searchSpinner)
928
+ searchSpinner.stop();
929
+ // Show per-source status
930
+ if (!isSilent && !isJson) {
931
+ for (const src of result.sources) {
932
+ if (src.status === 'ok') {
933
+ console.error(`✅ ${src.name}: ${src.count} hotels found`);
934
+ }
935
+ else {
936
+ console.error(`❌ ${src.name}: ${src.status}${src.error ? ' — ' + src.error : ''}`);
937
+ }
938
+ }
939
+ }
940
+ if (isJson) {
941
+ await writeStdout(JSON.stringify(result, null, 2) + '\n');
942
+ await cleanup();
943
+ process.exit(0);
944
+ }
945
+ // Human-readable table output
946
+ const { formatDate: fmtDate } = {
947
+ formatDate: (iso) => {
948
+ const d = new Date(iso + 'T12:00:00Z');
949
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
950
+ },
951
+ };
952
+ const ci = fmtDate(result.checkin);
953
+ const co = fmtDate(result.checkout);
954
+ console.log(`\n🏨 Hotels in ${result.destination}`);
955
+ console.log(` ${ci} → ${co} | Sorted by ${sortMethod}\n`);
956
+ if (result.results.length === 0) {
957
+ console.log(' No hotels found.\n');
958
+ }
959
+ else {
960
+ const colNum = 3;
961
+ const colName = 42;
962
+ const colPrice = 8;
963
+ const colRating = 8;
964
+ const colSource = 10;
965
+ const padEnd = (s, w) => s.length > w ? s.slice(0, w - 1) + '…' : s.padEnd(w);
966
+ const padStart = (s, w) => s.padStart(w);
967
+ console.log(` ${padStart('#', colNum)} ${padEnd('Hotel', colName)} ${padEnd('Price', colPrice)} ${padEnd('Rating', colRating)} ${padEnd('Source', colSource)}`);
968
+ result.results.forEach((hotel, i) => {
969
+ const priceStr = hotel.priceDisplay || '—';
970
+ const ratingStr = hotel.rating !== null ? String(hotel.rating) : '—';
971
+ console.log(` ${padStart(String(i + 1), colNum)} ${padEnd(hotel.name, colName)} ${padEnd(priceStr, colPrice)} ${padEnd(ratingStr, colRating)} ${padEnd(hotel.source, colSource)}`);
972
+ });
973
+ console.log('');
974
+ const sourceSummary = result.sources
975
+ .map(s => `${s.name} (${s.count} ${s.status === 'ok' ? '✅' : s.status === 'blocked' ? '🚫' : '❌'})`)
976
+ .join(' | ');
977
+ console.log(`Sources: ${sourceSummary}`);
978
+ }
979
+ console.log('');
980
+ await cleanup();
981
+ process.exit(0);
982
+ }
983
+ catch (error) {
984
+ if (searchSpinner)
985
+ searchSpinner.fail('Hotel search failed');
986
+ const msg = error instanceof Error ? error.message : 'Unknown error';
987
+ if (isJson) {
988
+ await writeStdout(JSON.stringify({ success: false, error: { type: 'fetch_failed', message: msg } }) + '\n');
989
+ }
990
+ else {
991
+ console.error(`\nError: ${msg}`);
992
+ }
993
+ await cleanup();
994
+ process.exit(1);
995
+ }
996
+ });
997
+ }