@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.
- package/LICENSE +15 -0
- package/README.md +313 -0
- package/dist/cache.d.ts +30 -0
- package/dist/cache.js +139 -0
- package/dist/cli/commands/auth.d.ts +5 -0
- package/dist/cli/commands/auth.js +411 -0
- package/dist/cli/commands/doctor.d.ts +37 -0
- package/dist/cli/commands/doctor.js +371 -0
- package/dist/cli/commands/fetch.d.ts +6 -0
- package/dist/cli/commands/fetch.js +1345 -0
- package/dist/cli/commands/guide.d.ts +2 -0
- package/dist/cli/commands/guide.js +183 -0
- package/dist/cli/commands/interact.d.ts +5 -0
- package/dist/cli/commands/interact.js +840 -0
- package/dist/cli/commands/jobs.d.ts +5 -0
- package/dist/cli/commands/jobs.js +997 -0
- package/dist/cli/commands/monitor.d.ts +12 -0
- package/dist/cli/commands/monitor.js +197 -0
- package/dist/cli/commands/observe.d.ts +12 -0
- package/dist/cli/commands/observe.js +158 -0
- package/dist/cli/commands/screenshot.d.ts +5 -0
- package/dist/cli/commands/screenshot.js +282 -0
- package/dist/cli/commands/search.d.ts +5 -0
- package/dist/cli/commands/search.js +1021 -0
- package/dist/cli/commands/setup.d.ts +13 -0
- package/dist/cli/commands/setup.js +244 -0
- package/dist/cli/commands/skill.d.ts +15 -0
- package/dist/cli/commands/skill.js +195 -0
- package/dist/cli/utils.d.ts +84 -0
- package/dist/cli/utils.js +806 -0
- package/dist/cli-auth.d.ts +75 -0
- package/dist/cli-auth.js +369 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +99 -0
- package/dist/core/actions.d.ts +69 -0
- package/dist/core/actions.js +495 -0
- package/dist/core/agent.d.ts +98 -0
- package/dist/core/agent.js +558 -0
- package/dist/core/answer.d.ts +42 -0
- package/dist/core/answer.js +395 -0
- package/dist/core/application-tracker.d.ts +84 -0
- package/dist/core/application-tracker.js +184 -0
- package/dist/core/apply.d.ts +162 -0
- package/dist/core/apply.js +816 -0
- package/dist/core/auth-detection.d.ts +35 -0
- package/dist/core/auth-detection.js +358 -0
- package/dist/core/auto-extract.d.ts +82 -0
- package/dist/core/auto-extract.js +604 -0
- package/dist/core/auto-interact.d.ts +23 -0
- package/dist/core/auto-interact.js +246 -0
- package/dist/core/bm25-filter.d.ts +66 -0
- package/dist/core/bm25-filter.js +288 -0
- package/dist/core/branding.d.ts +54 -0
- package/dist/core/branding.js +234 -0
- package/dist/core/browser-fetch.d.ts +323 -0
- package/dist/core/browser-fetch.js +1600 -0
- package/dist/core/browser-pool.d.ts +91 -0
- package/dist/core/browser-pool.js +550 -0
- package/dist/core/budget.d.ts +42 -0
- package/dist/core/budget.js +324 -0
- package/dist/core/business-intel.d.ts +47 -0
- package/dist/core/business-intel.js +279 -0
- package/dist/core/cache.d.ts +13 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cf-worker-proxy.d.ts +32 -0
- package/dist/core/cf-worker-proxy.js +87 -0
- package/dist/core/challenge-detection.d.ts +26 -0
- package/dist/core/challenge-detection.js +468 -0
- package/dist/core/change-tracking.d.ts +75 -0
- package/dist/core/change-tracking.js +276 -0
- package/dist/core/chunker.d.ts +46 -0
- package/dist/core/chunker.js +249 -0
- package/dist/core/chunking.d.ts +42 -0
- package/dist/core/chunking.js +181 -0
- package/dist/core/circuit-breaker.d.ts +44 -0
- package/dist/core/circuit-breaker.js +85 -0
- package/dist/core/content-pruner.d.ts +47 -0
- package/dist/core/content-pruner.js +425 -0
- package/dist/core/cookie-cache.d.ts +60 -0
- package/dist/core/cookie-cache.js +163 -0
- package/dist/core/crawl-checkpoint.d.ts +54 -0
- package/dist/core/crawl-checkpoint.js +104 -0
- package/dist/core/crawler.d.ts +84 -0
- package/dist/core/crawler.js +349 -0
- package/dist/core/cross-verify.d.ts +27 -0
- package/dist/core/cross-verify.js +93 -0
- package/dist/core/deep-fetch.d.ts +74 -0
- package/dist/core/deep-fetch.js +405 -0
- package/dist/core/deep-research.d.ts +141 -0
- package/dist/core/deep-research.js +972 -0
- package/dist/core/design-analysis.d.ts +70 -0
- package/dist/core/design-analysis.js +490 -0
- package/dist/core/design-compare.d.ts +38 -0
- package/dist/core/design-compare.js +264 -0
- package/dist/core/diff.d.ts +61 -0
- package/dist/core/diff.js +289 -0
- package/dist/core/dns-cache.d.ts +20 -0
- package/dist/core/dns-cache.js +198 -0
- package/dist/core/documents.d.ts +23 -0
- package/dist/core/documents.js +123 -0
- package/dist/core/domain-memory.d.ts +66 -0
- package/dist/core/domain-memory.js +163 -0
- package/dist/core/domain-verify.d.ts +40 -0
- package/dist/core/domain-verify.js +379 -0
- package/dist/core/engine-ranker.d.ts +112 -0
- package/dist/core/engine-ranker.js +395 -0
- package/dist/core/extract-inline.d.ts +38 -0
- package/dist/core/extract-inline.js +215 -0
- package/dist/core/extract-listings.d.ts +38 -0
- package/dist/core/extract-listings.js +461 -0
- package/dist/core/extract.d.ts +9 -0
- package/dist/core/extract.js +139 -0
- package/dist/core/fetch-cache.d.ts +57 -0
- package/dist/core/fetch-cache.js +95 -0
- package/dist/core/fetcher.d.ts +13 -0
- package/dist/core/fetcher.js +12 -0
- package/dist/core/google-cache.d.ts +29 -0
- package/dist/core/google-cache.js +180 -0
- package/dist/core/google-serp-parser.d.ts +82 -0
- package/dist/core/google-serp-parser.js +287 -0
- package/dist/core/hotel-search.d.ts +122 -0
- package/dist/core/hotel-search.js +382 -0
- package/dist/core/http-fetch.d.ts +72 -0
- package/dist/core/http-fetch.js +820 -0
- package/dist/core/human.d.ts +175 -0
- package/dist/core/human.js +680 -0
- package/dist/core/image-caption.d.ts +44 -0
- package/dist/core/image-caption.js +271 -0
- package/dist/core/jobs.d.ts +75 -0
- package/dist/core/jobs.js +634 -0
- package/dist/core/json-ld.d.ts +15 -0
- package/dist/core/json-ld.js +617 -0
- package/dist/core/language-detect.d.ts +18 -0
- package/dist/core/language-detect.js +135 -0
- package/dist/core/links.d.ts +10 -0
- package/dist/core/links.js +44 -0
- package/dist/core/llm-extract.d.ts +71 -0
- package/dist/core/llm-extract.js +507 -0
- package/dist/core/llm-provider.d.ts +100 -0
- package/dist/core/llm-provider.js +702 -0
- package/dist/core/local-search.d.ts +60 -0
- package/dist/core/local-search.js +308 -0
- package/dist/core/logger.d.ts +28 -0
- package/dist/core/logger.js +104 -0
- package/dist/core/map.d.ts +33 -0
- package/dist/core/map.js +127 -0
- package/dist/core/markdown.d.ts +92 -0
- package/dist/core/markdown.js +809 -0
- package/dist/core/metadata.d.ts +34 -0
- package/dist/core/metadata.js +422 -0
- package/dist/core/observe.d.ts +113 -0
- package/dist/core/observe.js +395 -0
- package/dist/core/ocr.d.ts +12 -0
- package/dist/core/ocr.js +33 -0
- package/dist/core/paginate.d.ts +31 -0
- package/dist/core/paginate.js +106 -0
- package/dist/core/pdf.d.ts +8 -0
- package/dist/core/pdf.js +25 -0
- package/dist/core/peel-tls.d.ts +25 -0
- package/dist/core/peel-tls.js +220 -0
- package/dist/core/pipeline.d.ts +132 -0
- package/dist/core/pipeline.js +1666 -0
- package/dist/core/profiles.d.ts +61 -0
- package/dist/core/profiles.js +350 -0
- package/dist/core/prompt-guard.d.ts +30 -0
- package/dist/core/prompt-guard.js +119 -0
- package/dist/core/proxy-config.d.ts +90 -0
- package/dist/core/proxy-config.js +172 -0
- package/dist/core/quick-answer.d.ts +53 -0
- package/dist/core/quick-answer.js +833 -0
- package/dist/core/rate-governor.d.ts +80 -0
- package/dist/core/rate-governor.js +238 -0
- package/dist/core/readability.d.ts +57 -0
- package/dist/core/readability.js +533 -0
- package/dist/core/research.d.ts +66 -0
- package/dist/core/research.js +270 -0
- package/dist/core/retry.d.ts +60 -0
- package/dist/core/retry.js +119 -0
- package/dist/core/safe-browsing.d.ts +30 -0
- package/dist/core/safe-browsing.js +206 -0
- package/dist/core/schema-extraction.d.ts +66 -0
- package/dist/core/schema-extraction.js +352 -0
- package/dist/core/schema-postprocess.d.ts +32 -0
- package/dist/core/schema-postprocess.js +469 -0
- package/dist/core/schema-templates.d.ts +19 -0
- package/dist/core/schema-templates.js +143 -0
- package/dist/core/screenshot.d.ts +224 -0
- package/dist/core/screenshot.js +207 -0
- package/dist/core/search-engines.d.ts +25 -0
- package/dist/core/search-engines.js +182 -0
- package/dist/core/search-provider.d.ts +243 -0
- package/dist/core/search-provider.js +1629 -0
- package/dist/core/searxng-provider.d.ts +35 -0
- package/dist/core/searxng-provider.js +105 -0
- package/dist/core/selective-evidence.d.ts +151 -0
- package/dist/core/selective-evidence.js +389 -0
- package/dist/core/site-search.d.ts +44 -0
- package/dist/core/site-search.js +252 -0
- package/dist/core/sitemap.d.ts +23 -0
- package/dist/core/sitemap.js +105 -0
- package/dist/core/source-credibility.d.ts +29 -0
- package/dist/core/source-credibility.js +584 -0
- package/dist/core/source-scoring.d.ts +166 -0
- package/dist/core/source-scoring.js +396 -0
- package/dist/core/stemmer.d.ts +38 -0
- package/dist/core/stemmer.js +509 -0
- package/dist/core/strategies.d.ts +104 -0
- package/dist/core/strategies.js +1044 -0
- package/dist/core/strategy-hooks.d.ts +145 -0
- package/dist/core/strategy-hooks.js +74 -0
- package/dist/core/structured-extract.d.ts +43 -0
- package/dist/core/structured-extract.js +550 -0
- package/dist/core/summarize.d.ts +17 -0
- package/dist/core/summarize.js +78 -0
- package/dist/core/synonyms.d.ts +42 -0
- package/dist/core/synonyms.js +184 -0
- package/dist/core/system-monitor.d.ts +61 -0
- package/dist/core/system-monitor.js +133 -0
- package/dist/core/table-format.d.ts +30 -0
- package/dist/core/table-format.js +146 -0
- package/dist/core/threat-feeds.d.ts +23 -0
- package/dist/core/threat-feeds.js +104 -0
- package/dist/core/timing.d.ts +21 -0
- package/dist/core/timing.js +33 -0
- package/dist/core/transcript-export.d.ts +47 -0
- package/dist/core/transcript-export.js +107 -0
- package/dist/core/user-agents.d.ts +82 -0
- package/dist/core/user-agents.js +239 -0
- package/dist/core/vertical-search.d.ts +54 -0
- package/dist/core/vertical-search.js +158 -0
- package/dist/core/watch-manager.d.ts +175 -0
- package/dist/core/watch-manager.js +416 -0
- package/dist/core/watch.d.ts +101 -0
- package/dist/core/watch.js +389 -0
- package/dist/core/youtube.d.ts +130 -0
- package/dist/core/youtube.js +1175 -0
- package/dist/ee/challenge-re-export.d.ts +1 -0
- package/dist/ee/challenge-re-export.js +1 -0
- package/dist/ee/challenge-solver.d.ts +72 -0
- package/dist/ee/challenge-solver.js +720 -0
- package/dist/ee/domain-extractors.d.ts +8 -0
- package/dist/ee/domain-extractors.js +8 -0
- package/dist/ee/domain-intel.d.ts +16 -0
- package/dist/ee/domain-intel.js +133 -0
- package/dist/ee/extractors/allrecipes.d.ts +2 -0
- package/dist/ee/extractors/allrecipes.js +120 -0
- package/dist/ee/extractors/amazon.d.ts +2 -0
- package/dist/ee/extractors/amazon.js +78 -0
- package/dist/ee/extractors/arxiv.d.ts +2 -0
- package/dist/ee/extractors/arxiv.js +137 -0
- package/dist/ee/extractors/bestbuy.d.ts +2 -0
- package/dist/ee/extractors/bestbuy.js +78 -0
- package/dist/ee/extractors/carscom.d.ts +2 -0
- package/dist/ee/extractors/carscom.js +121 -0
- package/dist/ee/extractors/coingecko.d.ts +2 -0
- package/dist/ee/extractors/coingecko.js +134 -0
- package/dist/ee/extractors/craigslist.d.ts +2 -0
- package/dist/ee/extractors/craigslist.js +92 -0
- package/dist/ee/extractors/devto.d.ts +2 -0
- package/dist/ee/extractors/devto.js +135 -0
- package/dist/ee/extractors/ebay.d.ts +2 -0
- package/dist/ee/extractors/ebay.js +90 -0
- package/dist/ee/extractors/espn.d.ts +2 -0
- package/dist/ee/extractors/espn.js +260 -0
- package/dist/ee/extractors/etsy.d.ts +2 -0
- package/dist/ee/extractors/etsy.js +52 -0
- package/dist/ee/extractors/facebook.d.ts +2 -0
- package/dist/ee/extractors/facebook.js +46 -0
- package/dist/ee/extractors/github.d.ts +2 -0
- package/dist/ee/extractors/github.js +196 -0
- package/dist/ee/extractors/google-flights.d.ts +2 -0
- package/dist/ee/extractors/google-flights.js +176 -0
- package/dist/ee/extractors/hackernews.d.ts +2 -0
- package/dist/ee/extractors/hackernews.js +147 -0
- package/dist/ee/extractors/imdb.d.ts +2 -0
- package/dist/ee/extractors/imdb.js +172 -0
- package/dist/ee/extractors/index.d.ts +26 -0
- package/dist/ee/extractors/index.js +247 -0
- package/dist/ee/extractors/instagram.d.ts +2 -0
- package/dist/ee/extractors/instagram.js +102 -0
- package/dist/ee/extractors/kalshi.d.ts +2 -0
- package/dist/ee/extractors/kalshi.js +121 -0
- package/dist/ee/extractors/kayak-cars.d.ts +2 -0
- package/dist/ee/extractors/kayak-cars.js +270 -0
- package/dist/ee/extractors/linkedin.d.ts +2 -0
- package/dist/ee/extractors/linkedin.js +113 -0
- package/dist/ee/extractors/medium.d.ts +2 -0
- package/dist/ee/extractors/medium.js +130 -0
- package/dist/ee/extractors/news.d.ts +4 -0
- package/dist/ee/extractors/news.js +173 -0
- package/dist/ee/extractors/npm.d.ts +2 -0
- package/dist/ee/extractors/npm.js +86 -0
- package/dist/ee/extractors/pdf.d.ts +2 -0
- package/dist/ee/extractors/pdf.js +108 -0
- package/dist/ee/extractors/pinterest.d.ts +2 -0
- package/dist/ee/extractors/pinterest.js +34 -0
- package/dist/ee/extractors/polymarket.d.ts +2 -0
- package/dist/ee/extractors/polymarket.js +358 -0
- package/dist/ee/extractors/producthunt.d.ts +2 -0
- package/dist/ee/extractors/producthunt.js +88 -0
- package/dist/ee/extractors/pubmed.d.ts +2 -0
- package/dist/ee/extractors/pubmed.js +162 -0
- package/dist/ee/extractors/pypi.d.ts +2 -0
- package/dist/ee/extractors/pypi.js +80 -0
- package/dist/ee/extractors/reddit.d.ts +2 -0
- package/dist/ee/extractors/reddit.js +438 -0
- package/dist/ee/extractors/redfin.d.ts +2 -0
- package/dist/ee/extractors/redfin.js +156 -0
- package/dist/ee/extractors/semanticscholar.d.ts +2 -0
- package/dist/ee/extractors/semanticscholar.js +131 -0
- package/dist/ee/extractors/shared.d.ts +12 -0
- package/dist/ee/extractors/shared.js +76 -0
- package/dist/ee/extractors/soundcloud.d.ts +2 -0
- package/dist/ee/extractors/soundcloud.js +34 -0
- package/dist/ee/extractors/sportsbetting.d.ts +2 -0
- package/dist/ee/extractors/sportsbetting.js +37 -0
- package/dist/ee/extractors/spotify.d.ts +2 -0
- package/dist/ee/extractors/spotify.js +34 -0
- package/dist/ee/extractors/stackoverflow.d.ts +2 -0
- package/dist/ee/extractors/stackoverflow.js +61 -0
- package/dist/ee/extractors/substack.d.ts +2 -0
- package/dist/ee/extractors/substack.js +115 -0
- package/dist/ee/extractors/substackroot.d.ts +2 -0
- package/dist/ee/extractors/substackroot.js +46 -0
- package/dist/ee/extractors/tiktok.d.ts +2 -0
- package/dist/ee/extractors/tiktok.js +29 -0
- package/dist/ee/extractors/tradingview.d.ts +2 -0
- package/dist/ee/extractors/tradingview.js +182 -0
- package/dist/ee/extractors/twitch.d.ts +2 -0
- package/dist/ee/extractors/twitch.js +36 -0
- package/dist/ee/extractors/twitter.d.ts +2 -0
- package/dist/ee/extractors/twitter.js +327 -0
- package/dist/ee/extractors/types.d.ts +14 -0
- package/dist/ee/extractors/types.js +1 -0
- package/dist/ee/extractors/walmart.d.ts +2 -0
- package/dist/ee/extractors/walmart.js +50 -0
- package/dist/ee/extractors/weather.d.ts +2 -0
- package/dist/ee/extractors/weather.js +133 -0
- package/dist/ee/extractors/wikipedia.d.ts +4 -0
- package/dist/ee/extractors/wikipedia.js +235 -0
- package/dist/ee/extractors/yelp.d.ts +2 -0
- package/dist/ee/extractors/yelp.js +216 -0
- package/dist/ee/extractors/youtube.d.ts +2 -0
- package/dist/ee/extractors/youtube.js +189 -0
- package/dist/ee/extractors/zillow.d.ts +54 -0
- package/dist/ee/extractors/zillow.js +247 -0
- package/dist/ee/extractors-re-export.d.ts +1 -0
- package/dist/ee/extractors-re-export.js +1 -0
- package/dist/ee/premium-hooks.d.ts +20 -0
- package/dist/ee/premium-hooks.js +50 -0
- package/dist/ee/spa-detection.d.ts +2 -0
- package/dist/ee/spa-detection.js +2 -0
- package/dist/ee/stability.d.ts +4 -0
- package/dist/ee/stability.js +29 -0
- package/dist/ee/swr-cache.d.ts +14 -0
- package/dist/ee/swr-cache.js +34 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +291 -0
- package/dist/integrations/index.d.ts +2 -0
- package/dist/integrations/index.js +2 -0
- package/dist/integrations/langchain.d.ts +64 -0
- package/dist/integrations/langchain.js +115 -0
- package/dist/integrations/llamaindex.d.ts +50 -0
- package/dist/integrations/llamaindex.js +91 -0
- package/dist/mcp/handlers/act.d.ts +5 -0
- package/dist/mcp/handlers/act.js +34 -0
- package/dist/mcp/handlers/definitions.d.ts +6 -0
- package/dist/mcp/handlers/definitions.js +395 -0
- package/dist/mcp/handlers/extract.d.ts +7 -0
- package/dist/mcp/handlers/extract.js +135 -0
- package/dist/mcp/handlers/fetch.d.ts +6 -0
- package/dist/mcp/handlers/fetch.js +98 -0
- package/dist/mcp/handlers/find.d.ts +5 -0
- package/dist/mcp/handlers/find.js +137 -0
- package/dist/mcp/handlers/index.d.ts +13 -0
- package/dist/mcp/handlers/index.js +63 -0
- package/dist/mcp/handlers/legacy.d.ts +25 -0
- package/dist/mcp/handlers/legacy.js +450 -0
- package/dist/mcp/handlers/meta.d.ts +6 -0
- package/dist/mcp/handlers/meta.js +40 -0
- package/dist/mcp/handlers/monitor.d.ts +5 -0
- package/dist/mcp/handlers/monitor.js +41 -0
- package/dist/mcp/handlers/observe.d.ts +8 -0
- package/dist/mcp/handlers/observe.js +37 -0
- package/dist/mcp/handlers/read.d.ts +6 -0
- package/dist/mcp/handlers/read.js +78 -0
- package/dist/mcp/handlers/see.d.ts +5 -0
- package/dist/mcp/handlers/see.js +75 -0
- package/dist/mcp/handlers/types.d.ts +29 -0
- package/dist/mcp/handlers/types.js +28 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +108 -0
- package/dist/mcp/smart-router.d.ts +23 -0
- package/dist/mcp/smart-router.js +178 -0
- package/dist/server/app.d.ts +14 -0
- package/dist/server/app.js +632 -0
- package/dist/server/auth-store.d.ts +28 -0
- package/dist/server/auth-store.js +88 -0
- package/dist/server/bull-queues.d.ts +60 -0
- package/dist/server/bull-queues.js +90 -0
- package/dist/server/email-service.d.ts +55 -0
- package/dist/server/email-service.js +291 -0
- package/dist/server/job-queue.d.ts +100 -0
- package/dist/server/job-queue.js +145 -0
- package/dist/server/logger.d.ts +10 -0
- package/dist/server/logger.js +37 -0
- package/dist/server/middleware/audit-log.d.ts +14 -0
- package/dist/server/middleware/audit-log.js +73 -0
- package/dist/server/middleware/auth.d.ts +35 -0
- package/dist/server/middleware/auth.js +225 -0
- package/dist/server/middleware/rate-limit.d.ts +50 -0
- package/dist/server/middleware/rate-limit.js +270 -0
- package/dist/server/middleware/scope-guard.d.ts +25 -0
- package/dist/server/middleware/scope-guard.js +45 -0
- package/dist/server/middleware/url-validator.d.ts +15 -0
- package/dist/server/middleware/url-validator.js +201 -0
- package/dist/server/openapi.yaml +6418 -0
- package/dist/server/pg-auth-store.d.ts +146 -0
- package/dist/server/pg-auth-store.js +576 -0
- package/dist/server/pg-job-queue.d.ts +59 -0
- package/dist/server/pg-job-queue.js +375 -0
- package/dist/server/routes/activity.d.ts +6 -0
- package/dist/server/routes/activity.js +79 -0
- package/dist/server/routes/admin-active.d.ts +7 -0
- package/dist/server/routes/admin-active.js +120 -0
- package/dist/server/routes/admin-stats.d.ts +7 -0
- package/dist/server/routes/admin-stats.js +176 -0
- package/dist/server/routes/agent.d.ts +24 -0
- package/dist/server/routes/agent.js +480 -0
- package/dist/server/routes/answer.d.ts +5 -0
- package/dist/server/routes/answer.js +125 -0
- package/dist/server/routes/ask.d.ts +28 -0
- package/dist/server/routes/ask.js +295 -0
- package/dist/server/routes/batch.d.ts +6 -0
- package/dist/server/routes/batch.js +493 -0
- package/dist/server/routes/cache-warm.d.ts +25 -0
- package/dist/server/routes/cache-warm.js +212 -0
- package/dist/server/routes/cli-usage.d.ts +6 -0
- package/dist/server/routes/cli-usage.js +127 -0
- package/dist/server/routes/compat.d.ts +23 -0
- package/dist/server/routes/compat.js +652 -0
- package/dist/server/routes/crawl.d.ts +13 -0
- package/dist/server/routes/crawl.js +287 -0
- package/dist/server/routes/deep-fetch.d.ts +8 -0
- package/dist/server/routes/deep-fetch.js +57 -0
- package/dist/server/routes/deep-research.d.ts +11 -0
- package/dist/server/routes/deep-research.js +232 -0
- package/dist/server/routes/demo.d.ts +24 -0
- package/dist/server/routes/demo.js +517 -0
- package/dist/server/routes/do.d.ts +8 -0
- package/dist/server/routes/do.js +72 -0
- package/dist/server/routes/extract.d.ts +14 -0
- package/dist/server/routes/extract.js +325 -0
- package/dist/server/routes/feed.d.ts +15 -0
- package/dist/server/routes/feed.js +311 -0
- package/dist/server/routes/fetch-queue.d.ts +13 -0
- package/dist/server/routes/fetch-queue.js +357 -0
- package/dist/server/routes/fetch.d.ts +7 -0
- package/dist/server/routes/fetch.js +1274 -0
- package/dist/server/routes/go.d.ts +14 -0
- package/dist/server/routes/go.js +81 -0
- package/dist/server/routes/health.d.ts +11 -0
- package/dist/server/routes/health.js +141 -0
- package/dist/server/routes/jobs.d.ts +7 -0
- package/dist/server/routes/jobs.js +574 -0
- package/dist/server/routes/map.d.ts +11 -0
- package/dist/server/routes/map.js +116 -0
- package/dist/server/routes/mcp.d.ts +14 -0
- package/dist/server/routes/mcp.js +197 -0
- package/dist/server/routes/metrics.d.ts +37 -0
- package/dist/server/routes/metrics.js +149 -0
- package/dist/server/routes/oauth.d.ts +9 -0
- package/dist/server/routes/oauth.js +396 -0
- package/dist/server/routes/playground.d.ts +17 -0
- package/dist/server/routes/playground.js +283 -0
- package/dist/server/routes/reader.d.ts +18 -0
- package/dist/server/routes/reader.js +192 -0
- package/dist/server/routes/research.d.ts +14 -0
- package/dist/server/routes/research.js +482 -0
- package/dist/server/routes/screenshot.d.ts +22 -0
- package/dist/server/routes/screenshot.js +820 -0
- package/dist/server/routes/search.d.ts +6 -0
- package/dist/server/routes/search.js +874 -0
- package/dist/server/routes/session.d.ts +17 -0
- package/dist/server/routes/session.js +548 -0
- package/dist/server/routes/share.d.ts +18 -0
- package/dist/server/routes/share.js +462 -0
- package/dist/server/routes/smart-search/handlers/cars.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/cars.js +102 -0
- package/dist/server/routes/smart-search/handlers/flights.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/flights.js +72 -0
- package/dist/server/routes/smart-search/handlers/general.d.ts +13 -0
- package/dist/server/routes/smart-search/handlers/general.js +717 -0
- package/dist/server/routes/smart-search/handlers/hotels.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/hotels.js +88 -0
- package/dist/server/routes/smart-search/handlers/products.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/products.js +1309 -0
- package/dist/server/routes/smart-search/handlers/rental.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/rental.js +154 -0
- package/dist/server/routes/smart-search/handlers/restaurants.d.ts +2 -0
- package/dist/server/routes/smart-search/handlers/restaurants.js +225 -0
- package/dist/server/routes/smart-search/handlers/transit-verdict.d.ts +41 -0
- package/dist/server/routes/smart-search/handlers/transit-verdict.js +224 -0
- package/dist/server/routes/smart-search/index.d.ts +19 -0
- package/dist/server/routes/smart-search/index.js +546 -0
- package/dist/server/routes/smart-search/intent.d.ts +3 -0
- package/dist/server/routes/smart-search/intent.js +264 -0
- package/dist/server/routes/smart-search/llm.d.ts +16 -0
- package/dist/server/routes/smart-search/llm.js +70 -0
- package/dist/server/routes/smart-search/sources/reddit.d.ts +18 -0
- package/dist/server/routes/smart-search/sources/reddit.js +34 -0
- package/dist/server/routes/smart-search/sources/yelp.d.ts +25 -0
- package/dist/server/routes/smart-search/sources/yelp.js +171 -0
- package/dist/server/routes/smart-search/sources/youtube.d.ts +8 -0
- package/dist/server/routes/smart-search/sources/youtube.js +9 -0
- package/dist/server/routes/smart-search/types.d.ts +81 -0
- package/dist/server/routes/smart-search/types.js +1 -0
- package/dist/server/routes/smart-search/utils.d.ts +20 -0
- package/dist/server/routes/smart-search/utils.js +146 -0
- package/dist/server/routes/stats.d.ts +6 -0
- package/dist/server/routes/stats.js +71 -0
- package/dist/server/routes/stripe.d.ts +15 -0
- package/dist/server/routes/stripe.js +296 -0
- package/dist/server/routes/transcript-export.d.ts +10 -0
- package/dist/server/routes/transcript-export.js +178 -0
- package/dist/server/routes/usage.d.ts +9 -0
- package/dist/server/routes/usage.js +279 -0
- package/dist/server/routes/users.d.ts +8 -0
- package/dist/server/routes/users.js +1867 -0
- package/dist/server/routes/watch.d.ts +15 -0
- package/dist/server/routes/watch.js +309 -0
- package/dist/server/routes/webhooks.d.ts +26 -0
- package/dist/server/routes/webhooks.js +170 -0
- package/dist/server/routes/youtube.d.ts +6 -0
- package/dist/server/routes/youtube.js +130 -0
- package/dist/server/sentry.d.ts +14 -0
- package/dist/server/sentry.js +104 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.js +7 -0
- package/dist/server/utils/response.d.ts +44 -0
- package/dist/server/utils/response.js +69 -0
- package/dist/server/utils/sse.d.ts +22 -0
- package/dist/server/utils/sse.js +38 -0
- package/dist/types.d.ts +552 -0
- package/dist/types.js +39 -0
- package/llms.txt +105 -0
- package/package.json +189 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI utilities — config, API client, output formatting, helpers.
|
|
3
|
+
* Imported by all command modules.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, resolve } from 'path';
|
|
8
|
+
// ─── CLI version ────────────────────────────────────────────────────────────
|
|
9
|
+
let _cliVersion = '0.0.0';
|
|
10
|
+
try {
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
// utils.ts compiles to dist/cli/utils.js; package.json is at dist/../../package.json
|
|
14
|
+
const pkgPath = resolve(__dirname, '..', '..', 'package.json');
|
|
15
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
16
|
+
_cliVersion = pkg.version;
|
|
17
|
+
}
|
|
18
|
+
catch { /* fallback to 0.0.0 */ }
|
|
19
|
+
export const cliVersion = _cliVersion;
|
|
20
|
+
// ─── Verb aliases ────────────────────────────────────────────────────────────
|
|
21
|
+
// Intercept verb-first syntax before Commander parses
|
|
22
|
+
// "webpeel fetch <url>" → "webpeel <url>"
|
|
23
|
+
// Note: 'read' is intentionally excluded — it's a registered subcommand.
|
|
24
|
+
export const VERB_ALIASES = new Set(['fetch', 'get', 'scrape', 'peel']);
|
|
25
|
+
// ─── Update check ────────────────────────────────────────────────────────────
|
|
26
|
+
export async function checkForUpdates() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch('https://registry.npmjs.org/webpeel/latest', {
|
|
29
|
+
signal: AbortSignal.timeout(2000),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
return;
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
const latest = data.version;
|
|
35
|
+
if (latest && latest !== cliVersion && cliVersion !== '0.0.0') {
|
|
36
|
+
// Skip update notice in silent mode
|
|
37
|
+
if (process.env.WEBPEEL_LOG_LEVEL !== 'silent') {
|
|
38
|
+
const msg = `Update available: ${cliVersion} → ${latest}`;
|
|
39
|
+
const cmd = 'npm i -g webpeel@latest';
|
|
40
|
+
const width = Math.max(msg.length, cmd.length) + 4;
|
|
41
|
+
const line = '─'.repeat(width);
|
|
42
|
+
console.error(`\n\x1b[33m╭${line}╮\x1b[0m`);
|
|
43
|
+
console.error(`\x1b[33m│\x1b[0m ${msg.padEnd(width - 2)} \x1b[33m│\x1b[0m`);
|
|
44
|
+
console.error(`\x1b[33m│\x1b[0m Run: \x1b[36m${cmd}\x1b[0m${' '.repeat(width - 6 - cmd.length)} \x1b[33m│\x1b[0m`);
|
|
45
|
+
console.error(`\x1b[33m╰${line}╯\x1b[0m\n`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* silently ignore — don't slow down the user */ }
|
|
50
|
+
}
|
|
51
|
+
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
52
|
+
export const NO_COLOR = process.env.NO_COLOR !== undefined || !process.stdout.isTTY;
|
|
53
|
+
export const bold = (s) => NO_COLOR ? s : `\x1b[1m${s}\x1b[0m`;
|
|
54
|
+
export const dim = (s) => NO_COLOR ? s : `\x1b[2m${s}\x1b[0m`;
|
|
55
|
+
export const cyan = (s) => NO_COLOR ? s : `\x1b[36m${s}\x1b[0m`;
|
|
56
|
+
// ─── Parse page actions ──────────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Parse action strings into PageAction array
|
|
59
|
+
* Formats:
|
|
60
|
+
* click:.selector — click an element
|
|
61
|
+
* type:.selector=text — type text into an input
|
|
62
|
+
* fill:.selector=text — fill an input (replaces existing value)
|
|
63
|
+
* scroll:down:500 — scroll direction + amount
|
|
64
|
+
* scroll:bottom — scroll to bottom (legacy)
|
|
65
|
+
* scroll:top — scroll to top (legacy)
|
|
66
|
+
* wait:2000 — wait N ms
|
|
67
|
+
* press:Enter — press a keyboard key
|
|
68
|
+
* hover:.selector — hover over an element
|
|
69
|
+
* waitFor:.selector — wait for a selector to appear
|
|
70
|
+
* select:.selector=value — select dropdown option
|
|
71
|
+
* screenshot — take a screenshot
|
|
72
|
+
*/
|
|
73
|
+
export function parseActions(actionStrings) {
|
|
74
|
+
return actionStrings.map(str => {
|
|
75
|
+
const [type, ...rest] = str.split(':');
|
|
76
|
+
const value = rest.join(':');
|
|
77
|
+
switch (type) {
|
|
78
|
+
case 'wait':
|
|
79
|
+
return { type: 'wait', ms: parseInt(value) || 1000 };
|
|
80
|
+
case 'click':
|
|
81
|
+
return { type: 'click', selector: value };
|
|
82
|
+
case 'scroll': {
|
|
83
|
+
// scroll:down:500 or scroll:bottom or scroll:500 or scroll:0,1500
|
|
84
|
+
const parts = value.split(':');
|
|
85
|
+
const dir = parts[0];
|
|
86
|
+
// Handle scroll:x,y format (e.g., scroll:0,1500)
|
|
87
|
+
if (dir && dir.includes(',')) {
|
|
88
|
+
const [x, y] = dir.split(',').map(Number);
|
|
89
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
90
|
+
return { type: 'scroll', to: { x, y } };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (dir === 'top' || dir === 'bottom') {
|
|
94
|
+
return { type: 'scroll', to: dir };
|
|
95
|
+
}
|
|
96
|
+
if (dir === 'down' || dir === 'up' || dir === 'left' || dir === 'right') {
|
|
97
|
+
const amount = parseInt(parts[1] || '500', 10);
|
|
98
|
+
return { type: 'scroll', direction: dir, amount };
|
|
99
|
+
}
|
|
100
|
+
// Bare number: absolute position
|
|
101
|
+
const num = parseInt(dir, 10);
|
|
102
|
+
if (!isNaN(num)) {
|
|
103
|
+
return { type: 'scroll', to: num };
|
|
104
|
+
}
|
|
105
|
+
// Default: scroll to bottom
|
|
106
|
+
return { type: 'scroll', to: 'bottom' };
|
|
107
|
+
}
|
|
108
|
+
case 'type': {
|
|
109
|
+
const [sel, ...text] = value.split('=');
|
|
110
|
+
return { type: 'type', selector: sel, value: text.join('=') };
|
|
111
|
+
}
|
|
112
|
+
case 'fill': {
|
|
113
|
+
const [sel, ...text] = value.split('=');
|
|
114
|
+
return { type: 'fill', selector: sel, value: text.join('=') };
|
|
115
|
+
}
|
|
116
|
+
case 'select': {
|
|
117
|
+
const [sel, ...vals] = value.split('=');
|
|
118
|
+
return { type: 'select', selector: sel, value: vals.join('=') };
|
|
119
|
+
}
|
|
120
|
+
case 'press':
|
|
121
|
+
return { type: 'press', key: value };
|
|
122
|
+
case 'hover':
|
|
123
|
+
return { type: 'hover', selector: value };
|
|
124
|
+
case 'waitFor':
|
|
125
|
+
return { type: 'waitForSelector', selector: value };
|
|
126
|
+
case 'wait-for':
|
|
127
|
+
return { type: 'waitForSelector', selector: value, timeout: 10000 };
|
|
128
|
+
case 'screenshot':
|
|
129
|
+
return { type: 'screenshot' };
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`Unknown action type: ${type}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// ─── Format error ────────────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Format an error with actionable suggestions based on error type
|
|
138
|
+
*/
|
|
139
|
+
export function formatError(error, _url, options) {
|
|
140
|
+
const msg = error.message || String(error);
|
|
141
|
+
const errorType = error.errorType || '';
|
|
142
|
+
const lines = [`\x1b[31m✖ ${msg}\x1b[0m`];
|
|
143
|
+
// Check structured errorType from API first (takes precedence over message heuristics)
|
|
144
|
+
if (errorType === 'timeout' || msg.includes('took too long') || msg.includes('timeout') || msg.includes('Timeout') || msg.includes('Navigation timeout')) {
|
|
145
|
+
lines.push('\x1b[33m💡 Try increasing timeout: --timeout 60000\x1b[0m');
|
|
146
|
+
if (!options.render) {
|
|
147
|
+
lines.push('\x1b[33m💡 Site may need browser rendering: --render\x1b[0m');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (errorType === 'blocked' || msg.includes('blocking automated') || msg.includes('bot protection') || msg.includes('blocked') || msg.includes('403') || msg.includes('Access Denied') || msg.includes('challenge')) {
|
|
151
|
+
if (!options.stealth) {
|
|
152
|
+
lines.push('\x1b[33m💡 Try stealth mode to bypass bot detection: --stealth\x1b[0m');
|
|
153
|
+
}
|
|
154
|
+
lines.push('\x1b[33m💡 Try a different user agent: --ua "Mozilla/5.0..."\x1b[0m');
|
|
155
|
+
}
|
|
156
|
+
else if (errorType === 'not_found' || msg.includes('domain may not exist') || msg.includes('not found') || msg.includes('ENOTFOUND') || msg.includes('net::ERR_') || msg.includes('ECONNREFUSED')) {
|
|
157
|
+
lines.push('\x1b[33m💡 Check the URL is correct and the site is accessible.\x1b[0m');
|
|
158
|
+
}
|
|
159
|
+
else if (errorType === 'network' || msg.includes('Could not reach') || msg.includes('could not connect') || msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND')) {
|
|
160
|
+
lines.push('\x1b[33m💡 Check the URL is correct and the site is accessible.\x1b[0m');
|
|
161
|
+
}
|
|
162
|
+
else if (errorType === 'server_error' || msg.includes('server error')) {
|
|
163
|
+
lines.push('\x1b[33m💡 The target site returned a server error. Try again in a moment.\x1b[0m');
|
|
164
|
+
}
|
|
165
|
+
else if (msg.includes('empty') || msg.includes('no content') || msg.includes('0 tokens')) {
|
|
166
|
+
if (!options.render) {
|
|
167
|
+
lines.push('\x1b[33m💡 Page may be JavaScript-rendered. Try: --render\x1b[0m');
|
|
168
|
+
}
|
|
169
|
+
else if (!options.stealth) {
|
|
170
|
+
lines.push('\x1b[33m💡 Content may be behind bot detection. Try: --stealth\x1b[0m');
|
|
171
|
+
}
|
|
172
|
+
lines.push('\x1b[33m💡 Try waiting longer for content: --wait 5000\x1b[0m');
|
|
173
|
+
}
|
|
174
|
+
else if (msg.includes('captcha') || msg.includes('CAPTCHA') || msg.includes('Captcha')) {
|
|
175
|
+
lines.push('\x1b[33m💡 This site requires CAPTCHA solving. Try a browser profile: --profile mysite --headed\x1b[0m');
|
|
176
|
+
}
|
|
177
|
+
else if (msg.includes('rate limit') || msg.includes('429')) {
|
|
178
|
+
lines.push('\x1b[33m💡 Rate limited. Wait a moment and try again, or use --proxy.\x1b[0m');
|
|
179
|
+
}
|
|
180
|
+
else if (msg.toLowerCase().includes('enotfound') || msg.toLowerCase().includes('getaddrinfo')) {
|
|
181
|
+
lines.push('\x1b[33m💡 Could not resolve hostname. Check the URL is correct.\x1b[0m');
|
|
182
|
+
}
|
|
183
|
+
else if (msg.toLowerCase().includes('certificate') || msg.toLowerCase().includes('ssl') || msg.toLowerCase().includes('tls')) {
|
|
184
|
+
lines.push('\x1b[33m💡 SSL/TLS error. The site may have an invalid certificate.\x1b[0m');
|
|
185
|
+
}
|
|
186
|
+
else if (msg.toLowerCase().includes('usage') || msg.toLowerCase().includes('quota') || msg.toLowerCase().includes('limit')) {
|
|
187
|
+
lines.push('\x1b[33m💡 Run `webpeel usage` to check your quota, or `webpeel login` to authenticate.\x1b[0m');
|
|
188
|
+
}
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
// ─── API-based fetch ─────────────────────────────────────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Routes ALL fetch requests through the WebPeel API.
|
|
194
|
+
* CLI is a pure API client — no local Playwright.
|
|
195
|
+
*/
|
|
196
|
+
export async function fetchViaApi(url, options, apiKey, apiUrl) {
|
|
197
|
+
// --format is a CLI output flag; API format is always the content extraction format
|
|
198
|
+
const apiFormat = (['text', 'html', 'markdown', 'md'].includes((options.format || '').toLowerCase()))
|
|
199
|
+
? (options.format.toLowerCase() === 'md' ? 'markdown' : options.format.toLowerCase())
|
|
200
|
+
: (options.html ? 'html' : options.text ? 'text' : 'markdown');
|
|
201
|
+
const params = new URLSearchParams({ url, format: apiFormat });
|
|
202
|
+
if (options.render)
|
|
203
|
+
params.set('render', 'true');
|
|
204
|
+
if (options.stealth)
|
|
205
|
+
params.set('stealth', 'true');
|
|
206
|
+
if (options.wait)
|
|
207
|
+
params.set('wait', String(options.wait));
|
|
208
|
+
if (options.selector)
|
|
209
|
+
params.set('selector', options.selector);
|
|
210
|
+
if (options.readable)
|
|
211
|
+
params.set('readable', 'true');
|
|
212
|
+
if (options.summary)
|
|
213
|
+
params.set('summary', 'true');
|
|
214
|
+
if (options.budget)
|
|
215
|
+
params.set('budget', String(options.budget));
|
|
216
|
+
if (options.question)
|
|
217
|
+
params.set('question', options.question);
|
|
218
|
+
if (options.noDomainApi)
|
|
219
|
+
params.set('noDomainApi', 'true');
|
|
220
|
+
if (options.highlightQuery)
|
|
221
|
+
params.set('highlightQuery', options.highlightQuery);
|
|
222
|
+
if (options.highlightMaxChars)
|
|
223
|
+
params.set('highlightMaxChars', String(options.highlightMaxChars));
|
|
224
|
+
const res = await fetch(`${apiUrl}/v1/fetch?${params}`, {
|
|
225
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
226
|
+
signal: AbortSignal.timeout(60000),
|
|
227
|
+
});
|
|
228
|
+
if (res.status === 401) {
|
|
229
|
+
throw Object.assign(new Error('API key invalid or expired. Run: webpeel auth <new-key>'), { code: 'AUTH_FAILED' });
|
|
230
|
+
}
|
|
231
|
+
if (res.status === 429) {
|
|
232
|
+
throw Object.assign(new Error('Rate limit exceeded. Check your plan at https://app.webpeel.dev/billing'), { code: 'RATE_LIMITED' });
|
|
233
|
+
}
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
const body = await res.text().catch(() => '');
|
|
236
|
+
// Sanitize error message — don't expose raw HTML (e.g. Cloudflare 502 pages)
|
|
237
|
+
const isHtml = body.trimStart().startsWith('<') || body.includes('<!DOCTYPE') || body.includes('<html');
|
|
238
|
+
let errorMsg;
|
|
239
|
+
let errorType;
|
|
240
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
241
|
+
errorMsg = `Could not reach this website. The site may be blocking our server or timing out.`;
|
|
242
|
+
errorType = res.status === 504 ? 'timeout' : 'network';
|
|
243
|
+
}
|
|
244
|
+
else if (isHtml) {
|
|
245
|
+
errorMsg = `Server returned an error page (${res.status})`;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Try to parse a structured JSON error response
|
|
249
|
+
try {
|
|
250
|
+
const json = JSON.parse(body);
|
|
251
|
+
const errObj = json?.error;
|
|
252
|
+
if (errObj && typeof errObj === 'object') {
|
|
253
|
+
errorMsg = typeof errObj.message === 'string' ? errObj.message : (body.slice(0, 200) || 'Unknown error');
|
|
254
|
+
if (typeof errObj.type === 'string')
|
|
255
|
+
errorType = errObj.type;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
errorMsg = body.slice(0, 200) || 'Unknown error';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
errorMsg = body.slice(0, 200) || 'Unknown error';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const err = new Error(`${errorMsg}`);
|
|
266
|
+
if (errorType)
|
|
267
|
+
err.errorType = errorType;
|
|
268
|
+
err.statusCode = res.status;
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
let data = await res.json();
|
|
272
|
+
// Handle async job queue mode — API returns { jobId, pollUrl } and we need to poll
|
|
273
|
+
if (data.jobId && data.pollUrl && !data.content) {
|
|
274
|
+
const pollEndpoint = `${apiUrl}${data.pollUrl}`;
|
|
275
|
+
const maxPollMs = 90_000; // 90s max
|
|
276
|
+
const pollInterval = 1_000; // 1s intervals
|
|
277
|
+
const start = Date.now();
|
|
278
|
+
while (Date.now() - start < maxPollMs) {
|
|
279
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
280
|
+
const pollRes = await fetch(pollEndpoint, {
|
|
281
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
282
|
+
signal: AbortSignal.timeout(10_000),
|
|
283
|
+
});
|
|
284
|
+
if (!pollRes.ok) {
|
|
285
|
+
throw new Error(`Job poll failed: HTTP ${pollRes.status}`);
|
|
286
|
+
}
|
|
287
|
+
const pollData = await pollRes.json();
|
|
288
|
+
if (pollData.status === 'completed' || pollData.content) {
|
|
289
|
+
data = pollData.result || pollData;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (pollData.status === 'failed' || pollData.status === 'error') {
|
|
293
|
+
throw new Error(pollData.error?.message || pollData.error || 'Job failed on server');
|
|
294
|
+
}
|
|
295
|
+
// Still processing — keep polling
|
|
296
|
+
}
|
|
297
|
+
// If we exited the loop without data, warn
|
|
298
|
+
if (!data.content && data.jobId) {
|
|
299
|
+
throw new Error('Job timed out waiting for server response. Try again or use local mode (unset WEBPEEL_API_KEY).');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Map API response to PeelResult shape that the CLI already handles
|
|
303
|
+
// Spread all API fields first, then override with normalized names
|
|
304
|
+
return {
|
|
305
|
+
...data,
|
|
306
|
+
url: data.url || url,
|
|
307
|
+
title: data.metadata?.title || data.title || '',
|
|
308
|
+
content: data.content || '',
|
|
309
|
+
method: data.method || 'simple',
|
|
310
|
+
tokens: data.tokenCount || data.tokens || 0,
|
|
311
|
+
elapsed: data.fetchTimeMs || data.elapsed || 0,
|
|
312
|
+
tokenSavingsPercent: data.tokenSavingsPercent,
|
|
313
|
+
rawTokenEstimate: data.rawTokenEstimate,
|
|
314
|
+
metadata: data.metadata || {},
|
|
315
|
+
links: data.links || [],
|
|
316
|
+
answer: data.answer,
|
|
317
|
+
summary: data.summary,
|
|
318
|
+
format: options.format || 'markdown',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// ─── Help formatting ─────────────────────────────────────────────────────────
|
|
322
|
+
/**
|
|
323
|
+
* Reconstruct the standard Commander help layout for --help-all and subcommands.
|
|
324
|
+
* This mirrors Commander's own default formatHelp() so subcommand help keeps working.
|
|
325
|
+
*/
|
|
326
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
327
|
+
export function buildCommanderHelp(cmd, helper) {
|
|
328
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
329
|
+
const helpWidth = helper.helpWidth ?? 80;
|
|
330
|
+
const pad = ' ';
|
|
331
|
+
const formatItem = (term, description) => {
|
|
332
|
+
if (description) {
|
|
333
|
+
const full = `${term.padEnd(termWidth + 2)}${description}`;
|
|
334
|
+
return helper.wrap(full, helpWidth - pad.length, termWidth + 2);
|
|
335
|
+
}
|
|
336
|
+
return term;
|
|
337
|
+
};
|
|
338
|
+
const formatList = (items) => items.join('\n').replace(/^/gm, pad);
|
|
339
|
+
let out = [`Usage: ${helper.commandUsage(cmd)}`, ''];
|
|
340
|
+
const desc = helper.commandDescription(cmd);
|
|
341
|
+
if (desc.length > 0) {
|
|
342
|
+
out = out.concat([helper.wrap(desc, helpWidth, 0), '']);
|
|
343
|
+
}
|
|
344
|
+
// Arguments
|
|
345
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
346
|
+
const args = helper.visibleArguments(cmd).map(a => formatItem(helper.argumentTerm(a), helper.argumentDescription(a)));
|
|
347
|
+
if (args.length > 0)
|
|
348
|
+
out = out.concat(['Arguments:', formatList(args), '']);
|
|
349
|
+
// Options
|
|
350
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
351
|
+
const opts = helper.visibleOptions(cmd).map(o => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
|
|
352
|
+
if (opts.length > 0)
|
|
353
|
+
out = out.concat(['Options:', formatList(opts), '']);
|
|
354
|
+
// Subcommands
|
|
355
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
356
|
+
const cmds = helper.visibleCommands(cmd).map(c => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
|
|
357
|
+
if (cmds.length > 0)
|
|
358
|
+
out = out.concat(['Commands:', formatList(cmds), '']);
|
|
359
|
+
// Append grouped option sections only on root command (--help-all)
|
|
360
|
+
if (cmd.parent === null) {
|
|
361
|
+
out = out.concat([`
|
|
362
|
+
Output Formats:
|
|
363
|
+
--json JSON output with full metadata
|
|
364
|
+
--html Raw HTML output
|
|
365
|
+
--text Plain text output
|
|
366
|
+
--csv / --table Tabular output for extractions
|
|
367
|
+
-s, --silent No spinner or progress output
|
|
368
|
+
|
|
369
|
+
Content Control:
|
|
370
|
+
--readable Reader mode — clean article content only
|
|
371
|
+
--budget <n> Smart token budget (no LLM key needed)
|
|
372
|
+
--focus <query> BM25 query-focused filtering
|
|
373
|
+
--selector <css> Extract specific CSS selector
|
|
374
|
+
--only-main-content Just main/article content
|
|
375
|
+
--full-content Disable content pruning
|
|
376
|
+
-q, --question <q> Ask a question about the content
|
|
377
|
+
|
|
378
|
+
Rendering:
|
|
379
|
+
-r, --render Browser rendering for JS-heavy sites
|
|
380
|
+
--stealth Stealth mode for bot-protected sites
|
|
381
|
+
--profile <path> Persistent browser profile
|
|
382
|
+
--headed Visible browser (for debugging)
|
|
383
|
+
--action <actions> Browser automation (click, type, scroll...)
|
|
384
|
+
|
|
385
|
+
Extraction:
|
|
386
|
+
--extract <json> CSS selector extraction
|
|
387
|
+
--extract-all Auto-detect listing items
|
|
388
|
+
--schema <name> Named extraction schema
|
|
389
|
+
--llm-extract [inst] LLM-powered extraction (BYOK)
|
|
390
|
+
|
|
391
|
+
Examples:
|
|
392
|
+
$ webpeel "https://example.com" Basic fetch
|
|
393
|
+
$ webpeel "https://youtube.com/watch?v=..." --json YouTube transcript
|
|
394
|
+
$ webpeel "https://openai.com/pricing" -q "GPT-4 cost?" Quick answer
|
|
395
|
+
$ webpeel "https://nytimes.com/article" --readable Reader mode
|
|
396
|
+
$ webpeel search "best restaurants in NYC" Web search
|
|
397
|
+
$ webpeel hotels "Manhattan" --checkin tomorrow Hotel search
|
|
398
|
+
|
|
399
|
+
Agent Integration:
|
|
400
|
+
$ webpeel mcp Start MCP server
|
|
401
|
+
$ cat urls.txt | webpeel batch Batch from stdin
|
|
402
|
+
$ webpeel pipe "https://example.com" | jq .content Pipe-friendly JSON
|
|
403
|
+
$ webpeel "https://site.com" --json --silent Same as pipe
|
|
404
|
+
$ curl https://webpeel.dev/llms.txt AI-readable docs
|
|
405
|
+
`]);
|
|
406
|
+
}
|
|
407
|
+
return out.join('\n');
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Condensed, Anthropic-style help for the root command (default --help).
|
|
411
|
+
*/
|
|
412
|
+
export function buildCondensedHelp() {
|
|
413
|
+
const v = cliVersion;
|
|
414
|
+
return [
|
|
415
|
+
'',
|
|
416
|
+
` ${bold('◆ WebPeel')} ${dim(`v${v}`)}`,
|
|
417
|
+
` ${dim('The web data platform for AI agents')}`,
|
|
418
|
+
'',
|
|
419
|
+
` ${bold('Usage:')} webpeel [url] [options]`,
|
|
420
|
+
` webpeel <command> [options]`,
|
|
421
|
+
'',
|
|
422
|
+
` ${bold('Examples:')}`,
|
|
423
|
+
` webpeel https://example.com ${dim('Clean content (reader mode)')}`,
|
|
424
|
+
` webpeel read https://example.com ${dim('Explicit reader mode')}`,
|
|
425
|
+
` webpeel screenshot https://example.com ${dim('Screenshot any page')}`,
|
|
426
|
+
` webpeel ask https://news.com "summary" ${dim('Ask about any page')}`,
|
|
427
|
+
` webpeel search "webpeel vs jina" ${dim('Web search')}`,
|
|
428
|
+
` echo "url" | webpeel ${dim('Pipe mode (auto JSON)')}`,
|
|
429
|
+
'',
|
|
430
|
+
` ${bold('Commands:')}`,
|
|
431
|
+
` fetch (default) Fetch a URL as clean markdown`,
|
|
432
|
+
` read <url> Reader mode (article content only)`,
|
|
433
|
+
` screenshot <url> Take a screenshot`,
|
|
434
|
+
` ask <url> <question> Ask about any page`,
|
|
435
|
+
` search <query> Search the web (DuckDuckGo + sources)`,
|
|
436
|
+
` crawl <url> Crawl a website`,
|
|
437
|
+
` mcp Start MCP server for AI tools`,
|
|
438
|
+
` ${dim('... (use --help-all for all 25+ commands)')}`,
|
|
439
|
+
'',
|
|
440
|
+
` ${bold('Common Options:')}`,
|
|
441
|
+
` -r, --render Browser rendering (JS-heavy sites)`,
|
|
442
|
+
` --stealth Stealth mode (anti-bot bypass)`,
|
|
443
|
+
` --raw Full page (disable auto reader mode)`,
|
|
444
|
+
` --full Full page, no budget limit`,
|
|
445
|
+
` --json JSON output with metadata`,
|
|
446
|
+
` --budget <n> Token budget (default: 4000 in pipe mode)`,
|
|
447
|
+
` -q, --question <q> Ask about the content`,
|
|
448
|
+
` -s, --silent No spinner output`,
|
|
449
|
+
'',
|
|
450
|
+
` Use ${cyan("'webpeel <command> --help'")} for command-specific options.`,
|
|
451
|
+
` Use ${cyan("'webpeel --help-all'")} for the full option reference.`,
|
|
452
|
+
'',
|
|
453
|
+
` Docs: ${cyan('https://webpeel.dev/docs')}`,
|
|
454
|
+
'',
|
|
455
|
+
].join('\n');
|
|
456
|
+
}
|
|
457
|
+
// ─── Time formatting ─────────────────────────────────────────────────────────
|
|
458
|
+
/**
|
|
459
|
+
* Format a past Date relative to now (e.g. "2h ago", "5m ago").
|
|
460
|
+
*/
|
|
461
|
+
export function formatRelativeTime(past) {
|
|
462
|
+
const diffMs = Date.now() - past.getTime();
|
|
463
|
+
const diffSec = Math.round(diffMs / 1000);
|
|
464
|
+
if (diffSec < 60)
|
|
465
|
+
return `${diffSec}s ago`;
|
|
466
|
+
const diffMin = Math.round(diffSec / 60);
|
|
467
|
+
if (diffMin < 60)
|
|
468
|
+
return `${diffMin}m ago`;
|
|
469
|
+
const diffHr = Math.round(diffMin / 60);
|
|
470
|
+
if (diffHr < 24)
|
|
471
|
+
return `${diffHr}h ago`;
|
|
472
|
+
const diffDay = Math.round(diffHr / 24);
|
|
473
|
+
return `${diffDay}d ago`;
|
|
474
|
+
}
|
|
475
|
+
// ─── Error classification ─────────────────────────────────────────────────────
|
|
476
|
+
export function classifyErrorCode(error) {
|
|
477
|
+
if (!(error instanceof Error))
|
|
478
|
+
return 'FETCH_FAILED';
|
|
479
|
+
// Check for our custom _code first (set in pre-fetch validation)
|
|
480
|
+
if (error._code)
|
|
481
|
+
return error._code;
|
|
482
|
+
// Check for structured errorType from API responses (set by fetchViaApi)
|
|
483
|
+
const errorType = error.errorType;
|
|
484
|
+
if (errorType) {
|
|
485
|
+
const typeMap = {
|
|
486
|
+
timeout: 'TIMEOUT',
|
|
487
|
+
blocked: 'BLOCKED',
|
|
488
|
+
not_found: 'NOT_FOUND',
|
|
489
|
+
server_error: 'SERVER_ERROR',
|
|
490
|
+
network: 'NETWORK',
|
|
491
|
+
unknown: 'FETCH_FAILED',
|
|
492
|
+
};
|
|
493
|
+
if (typeMap[errorType])
|
|
494
|
+
return typeMap[errorType];
|
|
495
|
+
}
|
|
496
|
+
const msg = error.message.toLowerCase();
|
|
497
|
+
const name = error.name || '';
|
|
498
|
+
if (name === 'TimeoutError' || msg.includes('timeout') || msg.includes('timed out') || msg.includes('took too long')) {
|
|
499
|
+
return 'TIMEOUT';
|
|
500
|
+
}
|
|
501
|
+
if (name === 'BlockedError' || msg.includes('blocked') || msg.includes('403') || msg.includes('cloudflare') || msg.includes('bot protection')) {
|
|
502
|
+
return 'BLOCKED';
|
|
503
|
+
}
|
|
504
|
+
if (msg.includes('domain may not exist') || msg.includes('enotfound') || msg.includes('getaddrinfo') || msg.includes('dns resolution failed')) {
|
|
505
|
+
return 'NOT_FOUND';
|
|
506
|
+
}
|
|
507
|
+
if (msg.includes('http 404') || msg.includes('page was not found')) {
|
|
508
|
+
return 'NOT_FOUND';
|
|
509
|
+
}
|
|
510
|
+
if (msg.includes('invalid url') || msg.includes('invalid hostname') || msg.includes('only http')) {
|
|
511
|
+
return 'INVALID_URL';
|
|
512
|
+
}
|
|
513
|
+
if (msg.includes('could not reach') || msg.includes('could not connect') || msg.includes('econnrefused')) {
|
|
514
|
+
return 'NETWORK';
|
|
515
|
+
}
|
|
516
|
+
return 'FETCH_FAILED';
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Build a unified PeelEnvelope from a PeelResult.
|
|
520
|
+
*
|
|
521
|
+
* All existing PeelResult fields are spread first (backward compatibility),
|
|
522
|
+
* then canonical envelope fields override/extend them.
|
|
523
|
+
*/
|
|
524
|
+
export function buildEnvelope(result, extra) {
|
|
525
|
+
const envelope = {
|
|
526
|
+
// Spread all PeelResult fields for backward compatibility
|
|
527
|
+
...result,
|
|
528
|
+
// Required envelope fields (override PeelResult where they overlap)
|
|
529
|
+
url: result.url,
|
|
530
|
+
status: 200,
|
|
531
|
+
content: result.content,
|
|
532
|
+
metadata: {
|
|
533
|
+
title: result.title,
|
|
534
|
+
...result.metadata,
|
|
535
|
+
},
|
|
536
|
+
tokens: result.tokens,
|
|
537
|
+
cached: extra.cached ?? false,
|
|
538
|
+
elapsed: result.elapsed,
|
|
539
|
+
};
|
|
540
|
+
// Optional envelope fields — only include when meaningful
|
|
541
|
+
if (extra.structured !== undefined)
|
|
542
|
+
envelope.structured = extra.structured;
|
|
543
|
+
if (extra.truncated)
|
|
544
|
+
envelope.truncated = true;
|
|
545
|
+
if (extra.totalAvailable !== undefined)
|
|
546
|
+
envelope.totalAvailable = extra.totalAvailable;
|
|
547
|
+
return envelope;
|
|
548
|
+
}
|
|
549
|
+
// ─── Output result ───────────────────────────────────────────────────────────
|
|
550
|
+
export async function outputResult(result, options, extra = {}) {
|
|
551
|
+
// --links: output only links
|
|
552
|
+
if (options.links) {
|
|
553
|
+
if (options.json) {
|
|
554
|
+
const jsonStr = JSON.stringify(result.links, null, 2);
|
|
555
|
+
await writeStdout(jsonStr + '\n');
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
for (const link of result.links) {
|
|
559
|
+
await writeStdout(link + '\n');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// --images: output only image URLs
|
|
565
|
+
if (options.images) {
|
|
566
|
+
// Extract image URLs from links that point to images
|
|
567
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico'];
|
|
568
|
+
const imageUrls = result.links.filter(link => {
|
|
569
|
+
const urlLower = link.toLowerCase();
|
|
570
|
+
return imageExtensions.some(ext => urlLower.includes(ext));
|
|
571
|
+
});
|
|
572
|
+
if (options.json) {
|
|
573
|
+
const jsonStr = JSON.stringify(imageUrls, null, 2);
|
|
574
|
+
await writeStdout(jsonStr + '\n');
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
for (const imageUrl of imageUrls) {
|
|
578
|
+
await writeStdout(imageUrl + '\n');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// --meta: output only metadata
|
|
584
|
+
if (options.meta) {
|
|
585
|
+
const meta = {
|
|
586
|
+
url: result.url,
|
|
587
|
+
title: result.title,
|
|
588
|
+
method: result.method,
|
|
589
|
+
elapsed: result.elapsed,
|
|
590
|
+
tokens: result.tokens,
|
|
591
|
+
cached: extra.cached ?? false,
|
|
592
|
+
...result.metadata,
|
|
593
|
+
};
|
|
594
|
+
if (options.json) {
|
|
595
|
+
await writeStdout(JSON.stringify(meta, null, 2) + '\n');
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
console.log(`Title: ${meta.title || '(none)'}`);
|
|
599
|
+
console.log(`URL: ${meta.url}`);
|
|
600
|
+
if (meta.description)
|
|
601
|
+
console.log(`Description: ${meta.description}`);
|
|
602
|
+
if (meta.author)
|
|
603
|
+
console.log(`Author: ${meta.author}`);
|
|
604
|
+
if (meta.published)
|
|
605
|
+
console.log(`Published: ${meta.published}`);
|
|
606
|
+
if (meta.canonical)
|
|
607
|
+
console.log(`Canonical: ${meta.canonical}`);
|
|
608
|
+
if (meta.image)
|
|
609
|
+
console.log(`OG Image: ${meta.image}`);
|
|
610
|
+
console.log(`Method: ${meta.method}`);
|
|
611
|
+
console.log(`Elapsed: ${meta.elapsed}ms`);
|
|
612
|
+
console.log(`Tokens: ${meta.tokens}`);
|
|
613
|
+
console.log(`Cached: ${meta.cached}`);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
// Default: full output
|
|
618
|
+
if (options.json) {
|
|
619
|
+
// Build clean JSON output with guaranteed top-level fields
|
|
620
|
+
// Note: elapsed/method/tokens are placed at the END so `tail -3` shows perf metrics
|
|
621
|
+
const output = {
|
|
622
|
+
url: result.url,
|
|
623
|
+
title: result.metadata?.title || result.title || null,
|
|
624
|
+
fetchedAt: new Date().toISOString(),
|
|
625
|
+
content: result.content,
|
|
626
|
+
};
|
|
627
|
+
// Add optional fields only if present (filter out undefined/null values from metadata)
|
|
628
|
+
if (result.metadata) {
|
|
629
|
+
const cleanMeta = {};
|
|
630
|
+
for (const [k, v] of Object.entries(result.metadata)) {
|
|
631
|
+
if (v !== undefined && v !== null)
|
|
632
|
+
cleanMeta[k] = v;
|
|
633
|
+
}
|
|
634
|
+
if (Object.keys(cleanMeta).length > 0)
|
|
635
|
+
output.metadata = cleanMeta;
|
|
636
|
+
}
|
|
637
|
+
if (result.links?.length)
|
|
638
|
+
output.links = result.links;
|
|
639
|
+
if (result.tokenSavingsPercent !== undefined)
|
|
640
|
+
output.tokenSavingsPercent = result.tokenSavingsPercent;
|
|
641
|
+
if (result.rawTokenEstimate !== undefined)
|
|
642
|
+
output.rawTokenEstimate = result.rawTokenEstimate;
|
|
643
|
+
if (result.images?.length)
|
|
644
|
+
output.images = result.images;
|
|
645
|
+
if (result.structured)
|
|
646
|
+
output.structured = result.structured;
|
|
647
|
+
if (result.domainData)
|
|
648
|
+
output.domainData = result.domainData;
|
|
649
|
+
if (result.readability)
|
|
650
|
+
output.readability = result.readability;
|
|
651
|
+
if (result.quickAnswer)
|
|
652
|
+
output.quickAnswer = result.quickAnswer;
|
|
653
|
+
if (result.quality)
|
|
654
|
+
output.quality = result.quality;
|
|
655
|
+
if (result.contentType)
|
|
656
|
+
output.contentType = result.contentType;
|
|
657
|
+
if (result.chunks)
|
|
658
|
+
output.chunks = result.chunks;
|
|
659
|
+
if (result.totalChunks)
|
|
660
|
+
output.totalChunks = result.totalChunks;
|
|
661
|
+
if (result.warning)
|
|
662
|
+
output.warning = result.warning;
|
|
663
|
+
if (result.focusQuery)
|
|
664
|
+
output.focusQuery = result.focusQuery;
|
|
665
|
+
if (result.focusReduction)
|
|
666
|
+
output.focusReduction = result.focusReduction;
|
|
667
|
+
if (result.extracted)
|
|
668
|
+
output.extracted = result.extracted;
|
|
669
|
+
if (result.trust)
|
|
670
|
+
output.trust = result.trust;
|
|
671
|
+
if (result.safeBrowsing)
|
|
672
|
+
output.safeBrowsing = result.safeBrowsing;
|
|
673
|
+
if (extra.cached)
|
|
674
|
+
output.cached = true;
|
|
675
|
+
if (extra.truncated)
|
|
676
|
+
output.truncated = true;
|
|
677
|
+
if (extra.totalAvailable !== undefined)
|
|
678
|
+
output.totalAvailable = extra.totalAvailable;
|
|
679
|
+
output._meta = { version: cliVersion, method: result.method || 'simple', timing: result.timing, serverMarkdown: result.serverMarkdown || false };
|
|
680
|
+
// Perf metrics at the end — `tail -3` shows: elapsed | method | tokens
|
|
681
|
+
output.elapsed = result.elapsed;
|
|
682
|
+
output.method = result.method || 'simple';
|
|
683
|
+
output.tokens = result.tokens || 0;
|
|
684
|
+
await writeStdout(JSON.stringify(output, null, 2) + '\n');
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
// Smart terminal header (interactive mode only)
|
|
688
|
+
const isTerminalOutput = process.stdout.isTTY && !options.silent;
|
|
689
|
+
if (isTerminalOutput) {
|
|
690
|
+
const meta = result.metadata || {};
|
|
691
|
+
const parts = [];
|
|
692
|
+
if (meta.title || result.title)
|
|
693
|
+
parts.push(`\x1b[1m${meta.title || result.title}\x1b[0m`);
|
|
694
|
+
if (meta.author)
|
|
695
|
+
parts.push(`By ${meta.author}`);
|
|
696
|
+
if (meta.wordCount)
|
|
697
|
+
parts.push(`${meta.wordCount} words`);
|
|
698
|
+
const totalMs = result.timing?.total ?? result.elapsed;
|
|
699
|
+
if (totalMs)
|
|
700
|
+
parts.push(`${totalMs}ms`);
|
|
701
|
+
if (parts.length > 0) {
|
|
702
|
+
await writeStdout(`\n ${parts.join(' · ')}\n`);
|
|
703
|
+
await writeStdout(' ' + '─'.repeat(60) + '\n\n');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Stream content immediately to stdout — consumer gets it without waiting
|
|
707
|
+
await writeStdout(result.content + '\n');
|
|
708
|
+
// Append timing summary to stderr (always — doesn't pollute stdout pipe)
|
|
709
|
+
{
|
|
710
|
+
const totalMs = result.timing?.total ?? result.elapsed;
|
|
711
|
+
const method = result.method || 'simple';
|
|
712
|
+
process.stderr.write(`\n--- ${totalMs}ms | ${method} | ${result.tokens} tokens ---\n`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// ─── Write helpers ────────────────────────────────────────────────────────────
|
|
717
|
+
export function writeStdout(data) {
|
|
718
|
+
return new Promise((resolve, reject) => {
|
|
719
|
+
process.stdout.write(data, (err) => {
|
|
720
|
+
if (err)
|
|
721
|
+
reject(err);
|
|
722
|
+
else
|
|
723
|
+
resolve();
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
// ─── Listings / CSV / table helpers ──────────────────────────────────────────
|
|
728
|
+
/**
|
|
729
|
+
* Convert an array of listing items to CSV.
|
|
730
|
+
*/
|
|
731
|
+
export function formatListingsCsv(items) {
|
|
732
|
+
if (items.length === 0)
|
|
733
|
+
return '';
|
|
734
|
+
// Collect all keys
|
|
735
|
+
const keySet = new Set();
|
|
736
|
+
for (const item of items) {
|
|
737
|
+
for (const key of Object.keys(item)) {
|
|
738
|
+
if (item[key] !== undefined)
|
|
739
|
+
keySet.add(key);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const keys = Array.from(keySet);
|
|
743
|
+
const escapeCsv = (s) => {
|
|
744
|
+
if (s === undefined || s === null)
|
|
745
|
+
return '""';
|
|
746
|
+
const str = String(s);
|
|
747
|
+
if (str.includes('"') || str.includes(',') || str.includes('\n') || str.includes('\r')) {
|
|
748
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
749
|
+
}
|
|
750
|
+
return '"' + str + '"';
|
|
751
|
+
};
|
|
752
|
+
const lines = [keys.join(',')];
|
|
753
|
+
for (const item of items) {
|
|
754
|
+
lines.push(keys.map(k => escapeCsv(item[k])).join(','));
|
|
755
|
+
}
|
|
756
|
+
return lines.join('\n') + '\n';
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Normalise the result of --extract (which may be a flat object or contain
|
|
760
|
+
* arrays) into an array of row objects suitable for CSV / table rendering.
|
|
761
|
+
*/
|
|
762
|
+
export function normaliseExtractedToRows(extracted) {
|
|
763
|
+
// If every value is an array of the same length, zip them into rows
|
|
764
|
+
const values = Object.values(extracted);
|
|
765
|
+
const allArrays = values.length > 0 && values.every(v => Array.isArray(v));
|
|
766
|
+
if (allArrays) {
|
|
767
|
+
const length = values[0].length;
|
|
768
|
+
const rows = [];
|
|
769
|
+
for (let i = 0; i < length; i++) {
|
|
770
|
+
const row = {};
|
|
771
|
+
for (const key of Object.keys(extracted)) {
|
|
772
|
+
const val = extracted[key][i];
|
|
773
|
+
row[key] = val != null ? String(val) : undefined;
|
|
774
|
+
}
|
|
775
|
+
rows.push(row);
|
|
776
|
+
}
|
|
777
|
+
return rows;
|
|
778
|
+
}
|
|
779
|
+
// Otherwise treat as a single row
|
|
780
|
+
const row = {};
|
|
781
|
+
for (const [k, v] of Object.entries(extracted)) {
|
|
782
|
+
row[k] = v != null ? String(v) : undefined;
|
|
783
|
+
}
|
|
784
|
+
return [row];
|
|
785
|
+
}
|
|
786
|
+
// ─── Branding helpers ────────────────────────────────────────────────────────
|
|
787
|
+
/** Helper function to extract colors from content */
|
|
788
|
+
export function extractColors(content) {
|
|
789
|
+
const colors = [];
|
|
790
|
+
const hexRegex = /#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}/g;
|
|
791
|
+
const matches = content.match(hexRegex);
|
|
792
|
+
if (matches) {
|
|
793
|
+
colors.push(...[...new Set(matches)].slice(0, 10));
|
|
794
|
+
}
|
|
795
|
+
return colors;
|
|
796
|
+
}
|
|
797
|
+
/** Helper function to extract font information */
|
|
798
|
+
export function extractFonts(content) {
|
|
799
|
+
const fonts = [];
|
|
800
|
+
const fontRegex = /font-family:\s*([^;}"'\n]+)/gi;
|
|
801
|
+
let match;
|
|
802
|
+
while ((match = fontRegex.exec(content)) !== null) {
|
|
803
|
+
fonts.push(match[1].trim());
|
|
804
|
+
}
|
|
805
|
+
return [...new Set(fonts)].slice(0, 5);
|
|
806
|
+
}
|