@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,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job application pipeline ā stealth automated job applications.
|
|
3
|
+
* Uses the human behavior engine for natural interaction.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. Rate-limit check (daily limit)
|
|
7
|
+
* 2. Launch persistent browser (preserves login cookies)
|
|
8
|
+
* 3. Warmup browse (optional, looks human)
|
|
9
|
+
* 4. Navigate to job posting naturally
|
|
10
|
+
* 5. Detect & click Apply button
|
|
11
|
+
* 6. Scan form fields & categorize
|
|
12
|
+
* 7. Fill fields with human behavior
|
|
13
|
+
* 8. Handle multi-step forms
|
|
14
|
+
* 9. Review / dry-run / auto submit
|
|
15
|
+
* 10. Log to ~/.webpeel/applications.json
|
|
16
|
+
*/
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
import { chromium as stealthChromium } from 'playwright-extra';
|
|
22
|
+
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
23
|
+
import { humanClick, humanClearAndType, humanScroll, humanDelay, humanRead, humanUploadFile, warmupBrowse, } from './human.js';
|
|
24
|
+
// Apply stealth plugin once (idempotent)
|
|
25
|
+
stealthChromium.use(StealthPlugin());
|
|
26
|
+
const WEBPEEL_DIR = join(homedir(), '.webpeel');
|
|
27
|
+
const APPLICATIONS_FILE = join(WEBPEEL_DIR, 'applications.json');
|
|
28
|
+
function ensureWebpeelDir() {
|
|
29
|
+
if (!existsSync(WEBPEEL_DIR)) {
|
|
30
|
+
mkdirSync(WEBPEEL_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function loadApplications() {
|
|
34
|
+
ensureWebpeelDir();
|
|
35
|
+
if (!existsSync(APPLICATIONS_FILE))
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(APPLICATIONS_FILE, 'utf-8');
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function saveApplication(record) {
|
|
46
|
+
ensureWebpeelDir();
|
|
47
|
+
const existing = loadApplications();
|
|
48
|
+
existing.push(record);
|
|
49
|
+
writeFileSync(APPLICATIONS_FILE, JSON.stringify(existing, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
export function getApplicationsToday() {
|
|
52
|
+
const apps = loadApplications();
|
|
53
|
+
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
54
|
+
return apps.filter(a => a.appliedAt.startsWith(today)).length;
|
|
55
|
+
}
|
|
56
|
+
export function updateApplicationStatus(id, status) {
|
|
57
|
+
ensureWebpeelDir();
|
|
58
|
+
const apps = loadApplications();
|
|
59
|
+
const idx = apps.findIndex(a => a.id === id);
|
|
60
|
+
if (idx >= 0) {
|
|
61
|
+
apps[idx].status = status;
|
|
62
|
+
writeFileSync(APPLICATIONS_FILE, JSON.stringify(apps, null, 2), 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Categorize a field based on its label, type, name, and placeholder.
|
|
67
|
+
*/
|
|
68
|
+
function categorizeField(field) {
|
|
69
|
+
const label = field.label.toLowerCase();
|
|
70
|
+
const placeholder = field.placeholder.toLowerCase();
|
|
71
|
+
const name = field.name.toLowerCase();
|
|
72
|
+
const id = field.id.toLowerCase();
|
|
73
|
+
const combined = `${label} ${placeholder} ${name} ${id}`;
|
|
74
|
+
// Input type shortcuts (most reliable signal)
|
|
75
|
+
if (field.type === 'file')
|
|
76
|
+
return 'resume';
|
|
77
|
+
if (field.type === 'email')
|
|
78
|
+
return 'email';
|
|
79
|
+
if (field.type === 'tel')
|
|
80
|
+
return 'phone';
|
|
81
|
+
// Label/name matching ā more specific first
|
|
82
|
+
if (/\blinkedin\b/.test(combined))
|
|
83
|
+
return 'linkedin';
|
|
84
|
+
if (/\bwebsite\b|\bportfolio\b|\bpersonal\s+site\b/.test(combined))
|
|
85
|
+
return 'website';
|
|
86
|
+
if (/\bemail\b/.test(combined))
|
|
87
|
+
return 'email';
|
|
88
|
+
if (/\bphone\b|\bcell\b|\bmobile\b|\btelephone\b/.test(combined))
|
|
89
|
+
return 'phone';
|
|
90
|
+
if (/\bfull\s+name\b|\byour\s+name\b|\bfirst.*last\b/.test(combined))
|
|
91
|
+
return 'name';
|
|
92
|
+
if (/\bfirst\s+name\b/.test(combined))
|
|
93
|
+
return 'name';
|
|
94
|
+
if (/\blast\s+name\b|\bsurname\b/.test(combined))
|
|
95
|
+
return 'name';
|
|
96
|
+
if (/\bname\b/.test(combined))
|
|
97
|
+
return 'name';
|
|
98
|
+
if (/\blocation\b|\bcity\b|\bstate\b|\bzip\b|\bpostal\b|\baddress\b/.test(combined))
|
|
99
|
+
return 'location';
|
|
100
|
+
if (/\bwork\s+auth|\bauthoriz|\bsponsorship\b|\bvisa\b|\blegal\s+status\b/.test(combined))
|
|
101
|
+
return 'work-auth';
|
|
102
|
+
if (/\byears?\s+of\s+exp|\bexperience\b|\byears?\s+exp\b/.test(combined))
|
|
103
|
+
return 'experience';
|
|
104
|
+
if (/\bsalary\b|\bcompensation\b|\bpay\s+range\b|\bexpected\s+pay\b/.test(combined))
|
|
105
|
+
return 'salary';
|
|
106
|
+
if (/\beducation\b|\bdegree\b|\bschool\b|\buniversity\b|\bcollege\b/.test(combined))
|
|
107
|
+
return 'education';
|
|
108
|
+
if (/\bresume\b|\bcv\b/.test(combined))
|
|
109
|
+
return 'resume';
|
|
110
|
+
if (/\bcover\s+letter\b/.test(combined))
|
|
111
|
+
return 'cover-letter';
|
|
112
|
+
if (/\bskill\b/.test(combined))
|
|
113
|
+
return 'skills';
|
|
114
|
+
// Textarea with a meaningful label ā likely a custom question
|
|
115
|
+
if (field.type === 'textarea' && label.length > 10)
|
|
116
|
+
return 'custom-question';
|
|
117
|
+
// Select dropdown with options + meaningful label ā likely custom question
|
|
118
|
+
if (field.type === 'select' && field.options.length > 0 && label.length > 5)
|
|
119
|
+
return 'custom-question';
|
|
120
|
+
return 'unknown';
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Scan a page (or modal) for form fields and categorize them.
|
|
124
|
+
* Uses page.evaluate() to access DOM elements.
|
|
125
|
+
*
|
|
126
|
+
* Note: all DOM API calls inside page.evaluate() use `any` since the project
|
|
127
|
+
* does not include the DOM lib (lib: ["ES2022"] only). The code is correct at
|
|
128
|
+
* runtime because it executes in the browser context.
|
|
129
|
+
*/
|
|
130
|
+
async function detectFields(page) {
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
const rawFields = await page.evaluate(() => {
|
|
133
|
+
// All variables here are `any` ā this runs inside the browser, not Node.js
|
|
134
|
+
const doc = globalThis.document; // eslint-disable-line
|
|
135
|
+
const elements = Array.from(doc.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), select, textarea'));
|
|
136
|
+
return elements.map((el) => {
|
|
137
|
+
let label = '';
|
|
138
|
+
// Strategy 1: <label for="id">
|
|
139
|
+
if (el.id) {
|
|
140
|
+
const labelEl = doc.querySelector(`label[for="${el.id}"]`);
|
|
141
|
+
if (labelEl)
|
|
142
|
+
label = String(labelEl.textContent || '').trim();
|
|
143
|
+
}
|
|
144
|
+
// Strategy 2: parent <label>
|
|
145
|
+
if (!label) {
|
|
146
|
+
const parentLabel = el.closest('label');
|
|
147
|
+
if (parentLabel) {
|
|
148
|
+
label = String(parentLabel.textContent || '')
|
|
149
|
+
.replace(String(el.value || ''), '')
|
|
150
|
+
.trim();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Strategy 3: aria-label
|
|
154
|
+
if (!label)
|
|
155
|
+
label = String(el.getAttribute('aria-label') || '');
|
|
156
|
+
// Strategy 4: aria-labelledby
|
|
157
|
+
if (!label) {
|
|
158
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
159
|
+
if (labelledBy) {
|
|
160
|
+
const labelEl = doc.getElementById(String(labelledBy));
|
|
161
|
+
if (labelEl)
|
|
162
|
+
label = String(labelEl.textContent || '').trim();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Strategy 5: preceding sibling text
|
|
166
|
+
if (!label) {
|
|
167
|
+
const prev = el.previousElementSibling;
|
|
168
|
+
if (prev && !['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(String(prev.tagName))) {
|
|
169
|
+
label = String(prev.textContent || '').trim().slice(0, 100);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Build unique CSS selector
|
|
173
|
+
let selector = '';
|
|
174
|
+
const elId = String(el.id || '');
|
|
175
|
+
const elName = String(el.name || '');
|
|
176
|
+
const tagName = String(el.tagName).toLowerCase();
|
|
177
|
+
if (elId) {
|
|
178
|
+
selector = `#${elId}`;
|
|
179
|
+
}
|
|
180
|
+
else if (elName) {
|
|
181
|
+
selector = `${tagName}[name="${elName}"]`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const form = el.closest('form, [role="dialog"], [class*="modal"]');
|
|
185
|
+
const container = form || doc.body;
|
|
186
|
+
const sibs = Array.from(container.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), select, textarea'));
|
|
187
|
+
const idx = sibs.indexOf(el);
|
|
188
|
+
selector = `${tagName}:nth-child(${idx + 1})`;
|
|
189
|
+
}
|
|
190
|
+
// Collect <select> options
|
|
191
|
+
const options = [];
|
|
192
|
+
if (String(el.tagName) === 'SELECT') {
|
|
193
|
+
const optEls = Array.from(el.options || []);
|
|
194
|
+
for (const opt of optEls) {
|
|
195
|
+
if (opt.value && opt.text)
|
|
196
|
+
options.push(String(opt.text).trim());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const fieldType = String(el.tagName) === 'SELECT'
|
|
200
|
+
? 'select'
|
|
201
|
+
: String(el.tagName) === 'TEXTAREA'
|
|
202
|
+
? 'textarea'
|
|
203
|
+
: String(el.type || 'text');
|
|
204
|
+
return {
|
|
205
|
+
type: fieldType,
|
|
206
|
+
label: String(label).replace(/\s+/g, ' ').trim().slice(0, 150),
|
|
207
|
+
placeholder: String(el.placeholder || ''),
|
|
208
|
+
name: elName,
|
|
209
|
+
id: elId,
|
|
210
|
+
required: Boolean(el.hasAttribute('required')) ||
|
|
211
|
+
el.getAttribute('aria-required') === 'true',
|
|
212
|
+
options,
|
|
213
|
+
selector,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
return rawFields.map(raw => ({
|
|
218
|
+
type: raw.type,
|
|
219
|
+
label: raw.label || raw.placeholder || raw.name || raw.id || '(unlabeled)',
|
|
220
|
+
selector: raw.selector,
|
|
221
|
+
options: raw.options.length > 0 ? raw.options : undefined,
|
|
222
|
+
required: raw.required,
|
|
223
|
+
category: categorizeField(raw),
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
async function detectJobInfo(page) {
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
return page.evaluate(() => {
|
|
229
|
+
const doc = globalThis.document; // eslint-disable-line
|
|
230
|
+
const text = (sel) => String(doc.querySelector(sel)?.textContent || '').trim();
|
|
231
|
+
const title = text('.job-details-jobs-unified-top-card__job-title') ||
|
|
232
|
+
text('[data-testid="job-title"]') ||
|
|
233
|
+
text('h1.topcard__title') ||
|
|
234
|
+
text('h1') ||
|
|
235
|
+
String(doc.title || '').split('|')[0]?.trim() ||
|
|
236
|
+
'';
|
|
237
|
+
const company = text('.job-details-jobs-unified-top-card__company-name') ||
|
|
238
|
+
text('[data-testid="company-name"]') ||
|
|
239
|
+
text('.topcard__org-name-link');
|
|
240
|
+
const locationText = text('.job-details-jobs-unified-top-card__bullet') ||
|
|
241
|
+
text('[data-testid="job-location"]') ||
|
|
242
|
+
text('.topcard__flavor--bullet');
|
|
243
|
+
const salaryEl = doc.querySelector('[class*="salary"]') ||
|
|
244
|
+
doc.querySelector('[class*="compensation"]');
|
|
245
|
+
const salary = String(salaryEl?.textContent || '').trim() || undefined;
|
|
246
|
+
return {
|
|
247
|
+
title,
|
|
248
|
+
company,
|
|
249
|
+
location: locationText || undefined,
|
|
250
|
+
salary,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// āā LLM Integration āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
255
|
+
async function callLLMForAnswer(question, profile, jobTitle, company, fieldOptions, llmKey, llmProvider) {
|
|
256
|
+
const prompt = `You are filling out a job application. Answer this screening question concisely and professionally.
|
|
257
|
+
|
|
258
|
+
Job: ${jobTitle} at ${company}
|
|
259
|
+
Question: "${question}"
|
|
260
|
+
${fieldOptions ? `Options: ${fieldOptions.join(', ')}` : ''}
|
|
261
|
+
|
|
262
|
+
Applicant profile:
|
|
263
|
+
- Name: ${profile.name}
|
|
264
|
+
- Title: ${profile.currentTitle}
|
|
265
|
+
- Experience: ${profile.yearsExperience} years
|
|
266
|
+
- Skills: ${profile.skills.join(', ')}
|
|
267
|
+
- Summary: ${profile.summary}
|
|
268
|
+
|
|
269
|
+
Answer (keep it concise, 1-3 sentences for text fields, or pick the best matching option for select/radio):`;
|
|
270
|
+
const systemPrompt = 'You are a helpful job application assistant. Provide concise, professional answers.';
|
|
271
|
+
if (llmProvider === 'anthropic') {
|
|
272
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: {
|
|
275
|
+
'Content-Type': 'application/json',
|
|
276
|
+
'x-api-key': llmKey,
|
|
277
|
+
'anthropic-version': '2023-06-01',
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({
|
|
280
|
+
model: 'claude-3-5-haiku-latest',
|
|
281
|
+
system: systemPrompt,
|
|
282
|
+
messages: [{ role: 'user', content: prompt }],
|
|
283
|
+
max_tokens: 500,
|
|
284
|
+
temperature: 0.3,
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
if (!resp.ok)
|
|
288
|
+
throw new Error(`Anthropic API error: ${resp.status}`);
|
|
289
|
+
const json = await resp.json();
|
|
290
|
+
const blocks = Array.isArray(json?.content) ? json.content : [];
|
|
291
|
+
return blocks
|
|
292
|
+
.map(b => String(b?.text ?? ''))
|
|
293
|
+
.join('')
|
|
294
|
+
.trim();
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// Default: OpenAI
|
|
298
|
+
const resp = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: {
|
|
301
|
+
'Content-Type': 'application/json',
|
|
302
|
+
Authorization: `Bearer ${llmKey}`,
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify({
|
|
305
|
+
model: 'gpt-4o-mini',
|
|
306
|
+
messages: [
|
|
307
|
+
{ role: 'system', content: systemPrompt },
|
|
308
|
+
{ role: 'user', content: prompt },
|
|
309
|
+
],
|
|
310
|
+
temperature: 0.3,
|
|
311
|
+
max_tokens: 300,
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
if (!resp.ok)
|
|
315
|
+
throw new Error(`OpenAI API error: ${resp.status}`);
|
|
316
|
+
const json = await resp.json();
|
|
317
|
+
const choices = json?.choices;
|
|
318
|
+
const message = choices?.[0]?.message;
|
|
319
|
+
return String(message?.content ?? '').trim();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// āā Form Navigation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
323
|
+
/** Find the "Next" / "Continue" / "Review" button in a multi-step form. */
|
|
324
|
+
async function findNextButton(page) {
|
|
325
|
+
const selectors = [
|
|
326
|
+
'[aria-label="Continue to next step"]',
|
|
327
|
+
'[aria-label="Next"]',
|
|
328
|
+
'button:text("Next")',
|
|
329
|
+
'button:text("Continue")',
|
|
330
|
+
'button:text("Review")',
|
|
331
|
+
'[data-easy-apply-next-button]',
|
|
332
|
+
];
|
|
333
|
+
for (const sel of selectors) {
|
|
334
|
+
try {
|
|
335
|
+
const el = await page.$(sel);
|
|
336
|
+
if (el && await el.isVisible())
|
|
337
|
+
return sel;
|
|
338
|
+
}
|
|
339
|
+
catch { /* continue */ }
|
|
340
|
+
}
|
|
341
|
+
// Text-based fallback
|
|
342
|
+
const btns = await page.$$('button');
|
|
343
|
+
for (const btn of btns) {
|
|
344
|
+
const text = (await btn.textContent() || '').trim().toLowerCase();
|
|
345
|
+
if ((text === 'next' || text === 'continue' || text === 'review') && await btn.isVisible()) {
|
|
346
|
+
// Generate a selector for this button
|
|
347
|
+
const id = await btn.getAttribute('id');
|
|
348
|
+
if (id)
|
|
349
|
+
return `#${id}`;
|
|
350
|
+
const cls = await btn.getAttribute('class');
|
|
351
|
+
if (cls)
|
|
352
|
+
return `button.${cls.split(' ')[0]}`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
/** Find the "Submit" / "Submit Application" button. */
|
|
358
|
+
async function findSubmitButton(page) {
|
|
359
|
+
const selectors = [
|
|
360
|
+
'[aria-label="Submit application"]',
|
|
361
|
+
'button:text("Submit application")',
|
|
362
|
+
'button:text("Submit Application")',
|
|
363
|
+
'[data-easy-apply-submit-button]',
|
|
364
|
+
];
|
|
365
|
+
for (const sel of selectors) {
|
|
366
|
+
try {
|
|
367
|
+
const el = await page.$(sel);
|
|
368
|
+
if (el && await el.isVisible())
|
|
369
|
+
return sel;
|
|
370
|
+
}
|
|
371
|
+
catch { /* continue */ }
|
|
372
|
+
}
|
|
373
|
+
// Text-based fallback
|
|
374
|
+
const btns = await page.$$('button[type="submit"], button');
|
|
375
|
+
for (const btn of btns) {
|
|
376
|
+
const text = (await btn.textContent() || '').trim().toLowerCase();
|
|
377
|
+
if ((text.includes('submit') || text === 'apply') &&
|
|
378
|
+
!text.includes('next') &&
|
|
379
|
+
await btn.isVisible()) {
|
|
380
|
+
const id = await btn.getAttribute('id');
|
|
381
|
+
if (id)
|
|
382
|
+
return `#${id}`;
|
|
383
|
+
return 'button[type="submit"]';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
/** Find and click the Apply button on a job posting. Returns the type of apply flow detected. */
|
|
389
|
+
async function clickApplyButton(page) {
|
|
390
|
+
// LinkedIn Easy Apply
|
|
391
|
+
const easyApplySelectors = [
|
|
392
|
+
'.jobs-apply-button',
|
|
393
|
+
'[aria-label="Easy Apply"]',
|
|
394
|
+
'button:text("Easy Apply")',
|
|
395
|
+
];
|
|
396
|
+
for (const sel of easyApplySelectors) {
|
|
397
|
+
try {
|
|
398
|
+
const el = await page.$(sel);
|
|
399
|
+
if (el && await el.isVisible()) {
|
|
400
|
+
await humanClick(page, sel);
|
|
401
|
+
await humanDelay(1000, 2500);
|
|
402
|
+
return 'easy-apply';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch { /* continue */ }
|
|
406
|
+
}
|
|
407
|
+
// External apply button
|
|
408
|
+
const externalSelectors = [
|
|
409
|
+
'[data-testid="apply-button"]',
|
|
410
|
+
'a:text("Apply")',
|
|
411
|
+
'button:text("Apply")',
|
|
412
|
+
];
|
|
413
|
+
for (const sel of externalSelectors) {
|
|
414
|
+
try {
|
|
415
|
+
const el = await page.$(sel);
|
|
416
|
+
if (el && await el.isVisible()) {
|
|
417
|
+
await humanClick(page, sel);
|
|
418
|
+
await humanDelay(1000, 2000);
|
|
419
|
+
return 'external';
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch { /* continue */ }
|
|
423
|
+
}
|
|
424
|
+
return 'not-found';
|
|
425
|
+
}
|
|
426
|
+
// āā Field Filling āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
427
|
+
/** Get the value to fill for a field based on its category and the applicant's profile. */
|
|
428
|
+
async function getFieldValue(field, profile, jobTitle, company, llmKey, llmProvider) {
|
|
429
|
+
switch (field.category) {
|
|
430
|
+
case 'name': return profile.name;
|
|
431
|
+
case 'email': return profile.email;
|
|
432
|
+
case 'phone': return profile.phone;
|
|
433
|
+
case 'linkedin': return profile.linkedin ?? null;
|
|
434
|
+
case 'website': return profile.website ?? null;
|
|
435
|
+
case 'location': return profile.location;
|
|
436
|
+
case 'education': return profile.education;
|
|
437
|
+
case 'skills': return profile.skills.join(', ');
|
|
438
|
+
case 'experience': return String(profile.yearsExperience);
|
|
439
|
+
case 'resume': return null; // handled separately via file upload
|
|
440
|
+
case 'salary':
|
|
441
|
+
return profile.salaryRange ? String(profile.salaryRange.min) : null;
|
|
442
|
+
case 'work-auth': {
|
|
443
|
+
// Try to find matching option from the select's options list
|
|
444
|
+
if (field.options && field.options.length > 0) {
|
|
445
|
+
const target = profile.workAuthorization.toLowerCase();
|
|
446
|
+
const match = field.options.find(opt => opt.toLowerCase().includes(target) || target.includes(opt.toLowerCase().replace(/[^a-z\s]/g, '')));
|
|
447
|
+
return match ?? field.options[0] ?? profile.workAuthorization;
|
|
448
|
+
}
|
|
449
|
+
return profile.workAuthorization;
|
|
450
|
+
}
|
|
451
|
+
case 'cover-letter':
|
|
452
|
+
return [
|
|
453
|
+
profile.summary,
|
|
454
|
+
'',
|
|
455
|
+
`I am excited to apply for the ${jobTitle} position at ${company}. ` +
|
|
456
|
+
`With ${profile.yearsExperience} years of experience as ${profile.currentTitle}, ` +
|
|
457
|
+
`I am confident I would be a strong fit for this role.`,
|
|
458
|
+
].join('\n');
|
|
459
|
+
case 'custom-question':
|
|
460
|
+
if (llmKey) {
|
|
461
|
+
try {
|
|
462
|
+
return await callLLMForAnswer(field.label, profile, jobTitle, company, field.options, llmKey, llmProvider ?? 'openai');
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return null; // gracefully skip if LLM fails
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
case 'unknown':
|
|
470
|
+
default:
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/** Fill a single form field using appropriate human behavior functions. */
|
|
475
|
+
async function fillField(page, field, value, warnings) {
|
|
476
|
+
try {
|
|
477
|
+
if (field.type === 'select') {
|
|
478
|
+
// Try by label text first, then by value
|
|
479
|
+
await page.selectOption(field.selector, { label: value }).catch(async () => {
|
|
480
|
+
await page.selectOption(field.selector, value).catch(() => { });
|
|
481
|
+
});
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
if (field.type === 'file') {
|
|
485
|
+
await humanUploadFile(page, field.selector, value);
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
if (field.type === 'radio') {
|
|
489
|
+
// Try clicking a radio with matching value
|
|
490
|
+
const radioSel = `input[type="radio"][value="${value}"]`;
|
|
491
|
+
const el = await page.$(radioSel);
|
|
492
|
+
if (el && await el.isVisible()) {
|
|
493
|
+
await humanClick(page, radioSel);
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
// Try by label text
|
|
497
|
+
const label = await page.$(`label:text("${value}")`);
|
|
498
|
+
if (label) {
|
|
499
|
+
const box = await label.boundingBox();
|
|
500
|
+
if (box) {
|
|
501
|
+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (field.type === 'checkbox') {
|
|
508
|
+
const lower = value.toLowerCase();
|
|
509
|
+
if (lower === 'true' || lower === 'yes' || lower === '1') {
|
|
510
|
+
const el = await page.$(field.selector);
|
|
511
|
+
if (el && !(await el.isChecked())) {
|
|
512
|
+
await humanClick(page, field.selector);
|
|
513
|
+
}
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
// Text, textarea, email, tel ā use clear-and-type for reliability
|
|
519
|
+
await humanClearAndType(page, field.selector, value);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
warnings.push(`Failed to fill "${field.label}": ${err instanceof Error ? err.message : String(err)}`);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// āā Main Entry Point āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
528
|
+
/**
|
|
529
|
+
* Apply to a job with stealth human-like behavior.
|
|
530
|
+
*
|
|
531
|
+
* Default mode is 'review' ā it fills the form and waits for your approval
|
|
532
|
+
* before submitting. Use 'auto' for fully automated, 'dry-run' to see what
|
|
533
|
+
* would be filled without actually clicking submit.
|
|
534
|
+
*
|
|
535
|
+
* Requires a persistent browser session for login state preservation.
|
|
536
|
+
* On first run, the browser will open ā log into LinkedIn, then the session
|
|
537
|
+
* is saved to `~/.webpeel/sessions/linkedin/` for future runs.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* const result = await applyToJob({
|
|
542
|
+
* url: 'https://linkedin.com/jobs/view/...',
|
|
543
|
+
* profile: myProfile,
|
|
544
|
+
* mode: 'review',
|
|
545
|
+
* });
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
export async function applyToJob(options) {
|
|
549
|
+
const startTime = Date.now();
|
|
550
|
+
const { url, profile, mode = 'review', llmKey, llmProvider = 'openai', dailyLimit = 8, timeout = 120_000, warmup: doWarmup = true, warmupDuration, onProgress, } = options;
|
|
551
|
+
const progress = (stage, message, extra) => {
|
|
552
|
+
onProgress?.({ stage, message, ...extra });
|
|
553
|
+
};
|
|
554
|
+
const result = {
|
|
555
|
+
submitted: false,
|
|
556
|
+
job: { title: '', company: '' },
|
|
557
|
+
fieldsFilled: 0,
|
|
558
|
+
llmAnswers: 0,
|
|
559
|
+
fieldsSkipped: [],
|
|
560
|
+
warnings: [],
|
|
561
|
+
elapsed: 0,
|
|
562
|
+
};
|
|
563
|
+
// āā 1. Rate limit check āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
564
|
+
if (mode !== 'dry-run') {
|
|
565
|
+
const todayCount = getApplicationsToday();
|
|
566
|
+
if (todayCount >= dailyLimit) {
|
|
567
|
+
const msg = `Daily application limit reached (${todayCount}/${dailyLimit}). Try again tomorrow.`;
|
|
568
|
+
result.error = msg;
|
|
569
|
+
result.elapsed = Date.now() - startTime;
|
|
570
|
+
progress('error', msg);
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// āā 2. Determine session directory āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
575
|
+
const isLinkedIn = url.includes('linkedin.com');
|
|
576
|
+
const siteName = isLinkedIn
|
|
577
|
+
? 'linkedin'
|
|
578
|
+
: (() => {
|
|
579
|
+
try {
|
|
580
|
+
return new URL(url).hostname.replace('www.', '').split('.')[0] ?? 'generic';
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return 'generic';
|
|
584
|
+
}
|
|
585
|
+
})();
|
|
586
|
+
const sessionDir = options.sessionDir ?? join(homedir(), '.webpeel', 'sessions', siteName);
|
|
587
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
588
|
+
let context = null;
|
|
589
|
+
let page = null;
|
|
590
|
+
try {
|
|
591
|
+
// āā 3. Launch persistent browser āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
592
|
+
progress('navigating', 'Launching browser with persistent session...');
|
|
593
|
+
context = await stealthChromium.launchPersistentContext(sessionDir, {
|
|
594
|
+
headless: false, // visible so user can monitor (or log in on first run)
|
|
595
|
+
viewport: { width: 1440, height: 900 },
|
|
596
|
+
locale: 'en-US',
|
|
597
|
+
timezoneId: 'America/New_York',
|
|
598
|
+
args: [
|
|
599
|
+
'--no-sandbox',
|
|
600
|
+
'--disable-setuid-sandbox',
|
|
601
|
+
'--disable-blink-features=AutomationControlled',
|
|
602
|
+
],
|
|
603
|
+
});
|
|
604
|
+
// context is guaranteed non-null here ā assignment above throws on failure
|
|
605
|
+
const existingPages = context.pages();
|
|
606
|
+
page = existingPages.length > 0 ? existingPages[0] : await context.newPage();
|
|
607
|
+
// āā 4. Warmup phase āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
608
|
+
if (doWarmup) {
|
|
609
|
+
progress('warmup', 'Warming up with natural browsing...');
|
|
610
|
+
try {
|
|
611
|
+
const warmupUrl = isLinkedIn
|
|
612
|
+
? 'https://www.linkedin.com/feed/'
|
|
613
|
+
: new URL(url).origin;
|
|
614
|
+
await page.goto(warmupUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
615
|
+
await humanDelay(2000, 4000);
|
|
616
|
+
const warmupMs = warmupDuration ?? Math.round(15000 + Math.random() * 15000);
|
|
617
|
+
await warmupBrowse(page, warmupMs);
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
result.warnings.push('Warmup phase failed ā continuing without warmup');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// āā 5. Navigate to job posting āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
624
|
+
progress('navigating', `Navigating to job posting...`);
|
|
625
|
+
// Navigate via jobs home first (looks more natural than direct URL jump)
|
|
626
|
+
if (isLinkedIn) {
|
|
627
|
+
try {
|
|
628
|
+
await page.goto('https://www.linkedin.com/jobs/', {
|
|
629
|
+
waitUntil: 'domcontentloaded',
|
|
630
|
+
timeout: 30000,
|
|
631
|
+
});
|
|
632
|
+
await humanDelay(1500, 3000);
|
|
633
|
+
await humanScroll(page, { direction: 'down', amount: 200 });
|
|
634
|
+
await humanDelay(800, 1500);
|
|
635
|
+
}
|
|
636
|
+
catch { /* ignore, proceed to actual URL */ }
|
|
637
|
+
}
|
|
638
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
|
|
639
|
+
await humanDelay(1500, 3000);
|
|
640
|
+
// āā 6. Read the job posting āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
641
|
+
progress('reading', 'Reading job posting...');
|
|
642
|
+
await humanRead(page, Math.round(4000 + Math.random() * 6000)); // 4-10s
|
|
643
|
+
// Extract job info from the page
|
|
644
|
+
result.job = await detectJobInfo(page);
|
|
645
|
+
// āā 7. Click Apply button āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
646
|
+
progress('navigating', 'Clicking Apply button...');
|
|
647
|
+
const applyType = await clickApplyButton(page);
|
|
648
|
+
if (applyType === 'not-found') {
|
|
649
|
+
result.warnings.push('Could not find Apply button ā the form may be directly on the page');
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
result.warnings.push(`Apply type: ${applyType}`);
|
|
653
|
+
}
|
|
654
|
+
await humanDelay(1500, 3000);
|
|
655
|
+
// āā 8. Multi-step form filling āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
656
|
+
progress('filling', 'Scanning and filling form...');
|
|
657
|
+
let stepCount = 0;
|
|
658
|
+
const MAX_STEPS = 10;
|
|
659
|
+
const allAnswers = {};
|
|
660
|
+
while (stepCount < MAX_STEPS) {
|
|
661
|
+
stepCount++;
|
|
662
|
+
const fields = await detectFields(page);
|
|
663
|
+
progress('filling', `Step ${stepCount}: found ${fields.length} field(s)`, { fields });
|
|
664
|
+
for (const field of fields) {
|
|
665
|
+
if (field.category === 'unknown' && field.type !== 'select') {
|
|
666
|
+
result.fieldsSkipped.push(field.label);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
await humanDelay(300, 800);
|
|
670
|
+
// Resume file upload ā special handling
|
|
671
|
+
if (field.category === 'resume' && field.type === 'file') {
|
|
672
|
+
try {
|
|
673
|
+
await humanUploadFile(page, field.selector, profile.resumePath);
|
|
674
|
+
result.fieldsFilled++;
|
|
675
|
+
allAnswers[field.label] = `[File: ${profile.resumePath}]`;
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
result.warnings.push(`Resume upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
679
|
+
}
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const value = await getFieldValue(field, profile, result.job.title, result.job.company, llmKey, llmProvider);
|
|
683
|
+
if (value === null) {
|
|
684
|
+
result.fieldsSkipped.push(field.label);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (field.category === 'custom-question' && llmKey) {
|
|
688
|
+
result.llmAnswers++;
|
|
689
|
+
}
|
|
690
|
+
const filled = await fillField(page, field, value, result.warnings);
|
|
691
|
+
if (filled) {
|
|
692
|
+
result.fieldsFilled++;
|
|
693
|
+
allAnswers[field.label] = value.length > 80 ? value.slice(0, 77) + '...' : value;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
result.fieldsSkipped.push(field.label);
|
|
697
|
+
}
|
|
698
|
+
await humanDelay(500, 1500);
|
|
699
|
+
}
|
|
700
|
+
// Check for submit button
|
|
701
|
+
const submitBtn = await findSubmitButton(page);
|
|
702
|
+
if (submitBtn) {
|
|
703
|
+
progress('reviewing', 'Form complete ā ready to submit', { answers: allAnswers });
|
|
704
|
+
if (mode === 'dry-run') {
|
|
705
|
+
console.log('\nš DRY-RUN ā fields that would be filled:');
|
|
706
|
+
for (const [label, value] of Object.entries(allAnswers)) {
|
|
707
|
+
console.log(` ā ${label}: ${value}`);
|
|
708
|
+
}
|
|
709
|
+
if (result.fieldsSkipped.length > 0) {
|
|
710
|
+
console.log(`\n ā ļø Skipped: ${result.fieldsSkipped.join(', ')}`);
|
|
711
|
+
}
|
|
712
|
+
console.log('\n[Dry-run complete ā NOT submitted]\n');
|
|
713
|
+
result.submitted = false;
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
if (mode === 'review') {
|
|
717
|
+
console.log('\nš Review ā fields filled:');
|
|
718
|
+
for (const [label, value] of Object.entries(allAnswers)) {
|
|
719
|
+
console.log(` ā ${label}: ${value}`);
|
|
720
|
+
}
|
|
721
|
+
if (result.fieldsSkipped.length > 0) {
|
|
722
|
+
console.log(`\n ā ļø Skipped: ${result.fieldsSkipped.join(', ')}`);
|
|
723
|
+
}
|
|
724
|
+
console.log('\nPlease review the form in the browser window.');
|
|
725
|
+
console.log('Press Enter to submit, or Ctrl+C to abort...');
|
|
726
|
+
await new Promise(resolve => {
|
|
727
|
+
const onData = (_data) => {
|
|
728
|
+
process.stdin.removeListener('data', onData);
|
|
729
|
+
try {
|
|
730
|
+
process.stdin.setRawMode(false);
|
|
731
|
+
}
|
|
732
|
+
catch { /* not a TTY */ }
|
|
733
|
+
process.stdin.pause();
|
|
734
|
+
resolve();
|
|
735
|
+
};
|
|
736
|
+
try {
|
|
737
|
+
process.stdin.setRawMode(true);
|
|
738
|
+
}
|
|
739
|
+
catch { /* not a TTY */ }
|
|
740
|
+
process.stdin.resume();
|
|
741
|
+
process.stdin.once('data', onData);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
// āā 9. Submit āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
745
|
+
progress('submitting', 'Submitting application...');
|
|
746
|
+
await humanClick(page, submitBtn);
|
|
747
|
+
await humanDelay(2000, 4000);
|
|
748
|
+
// Check for success confirmation text
|
|
749
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
750
|
+
const submitted = await page.evaluate(() => {
|
|
751
|
+
const doc = globalThis.document; // eslint-disable-line
|
|
752
|
+
const body = String(doc.body?.textContent || '');
|
|
753
|
+
return (body.includes('Application submitted') ||
|
|
754
|
+
body.includes('application was sent') ||
|
|
755
|
+
body.includes('successfully applied') ||
|
|
756
|
+
body.includes('Thank you for applying') ||
|
|
757
|
+
doc.querySelector('[class*="post-apply"]') !== null ||
|
|
758
|
+
doc.querySelector('[aria-label*="Application submitted"]') !== null);
|
|
759
|
+
});
|
|
760
|
+
result.submitted = submitted;
|
|
761
|
+
if (!submitted) {
|
|
762
|
+
result.warnings.push('Could not confirm submission ā check the browser window');
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
// Look for Next/Continue button to advance to the next step
|
|
767
|
+
const nextBtn = await findNextButton(page);
|
|
768
|
+
if (!nextBtn) {
|
|
769
|
+
result.warnings.push('No Next or Submit button found ā stopping form traversal');
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
await humanClick(page, nextBtn);
|
|
773
|
+
await humanDelay(1500, 3000);
|
|
774
|
+
}
|
|
775
|
+
if (stepCount >= MAX_STEPS) {
|
|
776
|
+
result.warnings.push(`Reached form step limit (${MAX_STEPS}) ā form may be unusually long`);
|
|
777
|
+
}
|
|
778
|
+
// āā 10. Log application āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
779
|
+
if (mode !== 'dry-run') {
|
|
780
|
+
const record = {
|
|
781
|
+
id: randomUUID(),
|
|
782
|
+
url,
|
|
783
|
+
company: result.job.company,
|
|
784
|
+
title: result.job.title,
|
|
785
|
+
location: result.job.location,
|
|
786
|
+
salary: result.job.salary,
|
|
787
|
+
appliedAt: new Date().toISOString(),
|
|
788
|
+
mode,
|
|
789
|
+
status: 'applied',
|
|
790
|
+
fieldsFilled: result.fieldsFilled,
|
|
791
|
+
fieldsSkipped: result.fieldsSkipped,
|
|
792
|
+
warnings: result.warnings,
|
|
793
|
+
};
|
|
794
|
+
saveApplication(record);
|
|
795
|
+
}
|
|
796
|
+
progress('done', result.submitted
|
|
797
|
+
? 'ā
Application submitted!'
|
|
798
|
+
: mode === 'dry-run'
|
|
799
|
+
? 'š Dry-run complete (not submitted)'
|
|
800
|
+
: 'š Application completed');
|
|
801
|
+
}
|
|
802
|
+
catch (err) {
|
|
803
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
804
|
+
result.error = msg;
|
|
805
|
+
progress('error', `Error: ${msg}`);
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
// Close page but keep context alive (preserves session for next run)
|
|
809
|
+
if (page && !page.isClosed()) {
|
|
810
|
+
await page.close().catch(() => { });
|
|
811
|
+
}
|
|
812
|
+
// DO NOT close context ā this keeps the session/cookies alive
|
|
813
|
+
}
|
|
814
|
+
result.elapsed = Date.now() - startTime;
|
|
815
|
+
return result;
|
|
816
|
+
}
|