@wenathlan/saddle 1.8.17 → 2.0.2
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/Dockerfile +1339 -0
- package/README.md +68 -22
- package/alternatives.ts +1218 -0
- package/biome.json +153 -0
- package/boards.json +1517 -0
- package/compute.ts +3144 -0
- package/cores.json +1801 -0
- package/dist/acquisition.d.ts +359 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +629 -0
- package/dist/acquisition.js.map +1 -0
- package/dist/alternatives.d.ts +509 -0
- package/dist/alternatives.d.ts.map +1 -0
- package/dist/alternatives.js +951 -0
- package/dist/alternatives.js.map +1 -0
- package/dist/automation.d.ts +426 -0
- package/dist/automation.d.ts.map +1 -0
- package/dist/automation.js +561 -0
- package/dist/automation.js.map +1 -0
- package/dist/browser.d.ts +562 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1057 -0
- package/dist/browser.js.map +1 -0
- package/dist/capacitor.config.d.ts +13 -0
- package/dist/capacitor.config.d.ts.map +1 -0
- package/dist/capacitor.config.js +26 -0
- package/dist/capacitor.config.js.map +1 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/communication.d.ts +192 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +418 -0
- package/dist/communication.js.map +1 -0
- package/dist/compute.d.ts +1161 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +2273 -0
- package/dist/compute.js.map +1 -0
- package/dist/distribution.d.ts +529 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +811 -0
- package/dist/distribution.js.map +1 -0
- package/dist/execution.d.ts +530 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +761 -0
- package/dist/execution.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +60 -0
- package/dist/format.js.map +1 -0
- package/dist/foundation.d.ts +176 -0
- package/dist/foundation.d.ts.map +1 -0
- package/dist/foundation.js +309 -0
- package/dist/foundation.js.map +1 -0
- package/dist/index.d.ts +733 -122
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -122
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +236 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +236 -0
- package/dist/integration.js.map +1 -0
- package/dist/intelligence.d.ts +94 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +145 -0
- package/dist/intelligence.js.map +1 -0
- package/dist/isolation.d.ts +129 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +96 -0
- package/dist/isolation.js.map +1 -0
- package/dist/media.d.ts +344 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +986 -0
- package/dist/media.js.map +1 -0
- package/dist/modes.d.ts +359 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +134 -0
- package/dist/modes.js.map +1 -0
- package/dist/{library/public.d.ts → operations.d.ts} +15 -1
- package/dist/operations.d.ts.map +1 -0
- package/dist/{library/public.js → operations.js} +18 -7
- package/dist/operations.js.map +1 -0
- package/dist/orchestrator.d.ts +1339 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +4308 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/performance.d.ts +416 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +827 -0
- package/dist/performance.js.map +1 -0
- package/dist/quantum.d.ts +810 -0
- package/dist/quantum.d.ts.map +1 -0
- package/dist/quantum.js +2073 -0
- package/dist/quantum.js.map +1 -0
- package/dist/render.d.ts +381 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1073 -0
- package/dist/render.js.map +1 -0
- package/dist/scheduler.d.ts +596 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +1259 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +501 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1170 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/{server/node.js → server.js} +16 -2
- package/dist/server.js.map +1 -0
- package/dist/tiers.d.ts +1002 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +2165 -0
- package/dist/tiers.js.map +1 -0
- package/dist/virtual.d.ts +641 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1285 -0
- package/dist/virtual.js.map +1 -0
- package/dist/virtualcpu.d.ts +316 -0
- package/dist/virtualcpu.d.ts.map +1 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualcpu.js.map +1 -0
- package/dist/virtualgpu.d.ts +345 -0
- package/dist/virtualgpu.d.ts.map +1 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualgpu.js.map +1 -0
- package/dist/virtualization.d.ts +607 -0
- package/dist/virtualization.d.ts.map +1 -0
- package/dist/virtualization.js +1187 -0
- package/dist/virtualization.js.map +1 -0
- package/dist/virtualmemory.d.ts +818 -0
- package/dist/virtualmemory.d.ts.map +1 -0
- package/dist/virtualmemory.js +1796 -0
- package/dist/virtualmemory.js.map +1 -0
- package/dist/webscrape.d.ts +720 -0
- package/dist/webscrape.d.ts.map +1 -0
- package/dist/webscrape.js +2697 -0
- package/dist/webscrape.js.map +1 -0
- package/docker.config +769 -0
- package/docs/CONVERSA.txt +1523 -0
- package/docs/alternatives.md +499 -0
- package/docs/architecture-1.8.19.md +76 -0
- package/docs/architecture-1.8.19.mmd +33 -0
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/architecture.md +355 -0
- package/docs/artifactavailability.md +6 -0
- package/docs/brancharchive-2026-08-18.md +18 -0
- package/docs/browser.md +7 -0
- package/docs/consolidation.md +70 -0
- package/docs/e2ugh-engine.md +528 -0
- package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
- package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
- package/docs/hardware.md +342 -0
- package/docs/optimization.md +126 -0
- package/docs/performance.md +499 -0
- package/docs/planning.1.8.18.md +94 -0
- package/docs/planning.1.8.19.md +49 -0
- package/docs/releasenotes-1.8.17.md +6 -0
- package/docs/releasenotes-1.8.18.md +32 -0
- package/docs/releasenotes-1.8.19.md +35 -0
- package/docs/research-1.8.18-isolation.md +43 -0
- package/docs/research-1.8.19-virtual-browser.md +27 -0
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- package/docs/security.md +454 -0
- package/docs/todo-1.8.16.md +56 -1
- package/docs/todo-1.8.18.md +164 -0
- package/docs/todo-1.8.19.md +241 -0
- package/docs/viability.md +777 -0
- package/docs/virtualization.md +335 -0
- package/docs/web-duplicatecleanup.md +9 -0
- package/docs/web-ideas.md +106 -0
- package/docs/workflowimprovements-2026-08-19.md +70 -0
- package/gpumonitor.cpp +1543 -0
- package/gpus.json +2408 -0
- package/index.ts +1478 -0
- package/media.ts +1337 -0
- package/mttg.config +1549 -0
- package/orchestrator.ts +5462 -0
- package/package.json +304 -55
- package/passage.config +1331 -0
- package/performance.ts +1049 -0
- package/processors.json +2217 -0
- package/qemu.config +1062 -0
- package/qemubridge.py +1340 -0
- package/quantum.ts +2393 -0
- package/render.ts +1325 -0
- package/scheduler.ts +1600 -0
- package/security.ts +1449 -0
- package/tiers.ts +2871 -0
- package/tsconfig.json +29 -0
- package/virtualcpu.ts +1273 -0
- package/virtualgpu.ts +1181 -0
- package/virtualhardware.c +1011 -0
- package/virtualhardware.json +739 -0
- package/virtualization.ts +1628 -0
- package/virtualizationcore.cpp +4635 -0
- package/virtualmemory.ts +2303 -0
- package/vm.config.json +1910 -0
- package/{extension → web/extension}/manifest.json +1 -1
- package/web/readme.md +382 -0
- package/web/tsconfig.json +25 -0
- package/dist/adapters/forge.d.ts +0 -17
- package/dist/adapters/forge.d.ts.map +0 -1
- package/dist/adapters/forge.js +0 -19
- package/dist/adapters/forge.js.map +0 -1
- package/dist/adapters/forgejo.d.ts +0 -49
- package/dist/adapters/forgejo.d.ts.map +0 -1
- package/dist/adapters/forgejo.js +0 -8
- package/dist/adapters/forgejo.js.map +0 -1
- package/dist/adapters/github.d.ts +0 -12
- package/dist/adapters/github.d.ts.map +0 -1
- package/dist/adapters/github.js +0 -20
- package/dist/adapters/github.js.map +0 -1
- package/dist/adapters/gitlab.d.ts +0 -17
- package/dist/adapters/gitlab.d.ts.map +0 -1
- package/dist/adapters/gitlab.js +0 -10
- package/dist/adapters/gitlab.js.map +0 -1
- package/dist/adapters/huggingface.d.ts +0 -17
- package/dist/adapters/huggingface.d.ts.map +0 -1
- package/dist/adapters/huggingface.js +0 -6
- package/dist/adapters/huggingface.js.map +0 -1
- package/dist/adapters/socket.d.ts +0 -11
- package/dist/adapters/socket.d.ts.map +0 -1
- package/dist/adapters/socket.js +0 -17
- package/dist/adapters/socket.js.map +0 -1
- package/dist/adapters/transport.d.ts +0 -7
- package/dist/adapters/transport.d.ts.map +0 -1
- package/dist/adapters/transport.js +0 -36
- package/dist/adapters/transport.js.map +0 -1
- package/dist/ai/chunk.d.ts +0 -2
- package/dist/ai/chunk.d.ts.map +0 -1
- package/dist/ai/chunk.js +0 -36
- package/dist/ai/chunk.js.map +0 -1
- package/dist/ai/llmstxt.d.ts +0 -6
- package/dist/ai/llmstxt.d.ts.map +0 -1
- package/dist/ai/llmstxt.js +0 -12
- package/dist/ai/llmstxt.js.map +0 -1
- package/dist/ai/provenance.d.ts +0 -22
- package/dist/ai/provenance.d.ts.map +0 -1
- package/dist/ai/provenance.js +0 -25
- package/dist/ai/provenance.js.map +0 -1
- package/dist/ai/rag.d.ts +0 -26
- package/dist/ai/rag.d.ts.map +0 -1
- package/dist/ai/rag.js +0 -22
- package/dist/ai/rag.js.map +0 -1
- package/dist/ai/tokens.d.ts +0 -15
- package/dist/ai/tokens.d.ts.map +0 -1
- package/dist/ai/tokens.js +0 -9
- package/dist/ai/tokens.js.map +0 -1
- package/dist/api/auth.d.ts +0 -16
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js +0 -19
- package/dist/api/auth.js.map +0 -1
- package/dist/api/contracts.d.ts +0 -28
- package/dist/api/contracts.d.ts.map +0 -1
- package/dist/api/contracts.js +0 -14
- package/dist/api/contracts.js.map +0 -1
- package/dist/api/control.d.ts +0 -10
- package/dist/api/control.d.ts.map +0 -1
- package/dist/api/control.js +0 -35
- package/dist/api/control.js.map +0 -1
- package/dist/api/http.d.ts +0 -7
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/http.js +0 -12
- package/dist/api/http.js.map +0 -1
- package/dist/api/rate.d.ts +0 -28
- package/dist/api/rate.d.ts.map +0 -1
- package/dist/api/rate.js +0 -36
- package/dist/api/rate.js.map +0 -1
- package/dist/api/security.d.ts +0 -12
- package/dist/api/security.d.ts.map +0 -1
- package/dist/api/security.js +0 -56
- package/dist/api/security.js.map +0 -1
- package/dist/api/service.d.ts +0 -5
- package/dist/api/service.d.ts.map +0 -1
- package/dist/api/service.js +0 -70
- package/dist/api/service.js.map +0 -1
- package/dist/apps/registry.d.ts +0 -33
- package/dist/apps/registry.d.ts.map +0 -1
- package/dist/apps/registry.js +0 -24
- package/dist/apps/registry.js.map +0 -1
- package/dist/binary/archive.d.ts +0 -27
- package/dist/binary/archive.d.ts.map +0 -1
- package/dist/binary/archive.js +0 -46
- package/dist/binary/archive.js.map +0 -1
- package/dist/binary/build.d.ts +0 -29
- package/dist/binary/build.d.ts.map +0 -1
- package/dist/binary/build.js +0 -17
- package/dist/binary/build.js.map +0 -1
- package/dist/binary/transform.d.ts +0 -70
- package/dist/binary/transform.d.ts.map +0 -1
- package/dist/binary/transform.js +0 -105
- package/dist/binary/transform.js.map +0 -1
- package/dist/bot/adapter.d.ts +0 -5
- package/dist/bot/adapter.d.ts.map +0 -1
- package/dist/bot/adapter.js +0 -11
- package/dist/bot/adapter.js.map +0 -1
- package/dist/bot/bot.d.ts +0 -28
- package/dist/bot/bot.d.ts.map +0 -1
- package/dist/bot/bot.js +0 -52
- package/dist/bot/bot.js.map +0 -1
- package/dist/bot/commands.d.ts +0 -8
- package/dist/bot/commands.d.ts.map +0 -1
- package/dist/bot/commands.js +0 -19
- package/dist/bot/commands.js.map +0 -1
- package/dist/bot/permissions.d.ts +0 -13
- package/dist/bot/permissions.d.ts.map +0 -1
- package/dist/bot/permissions.js +0 -16
- package/dist/bot/permissions.js.map +0 -1
- package/dist/browser/actions.d.ts +0 -33
- package/dist/browser/actions.d.ts.map +0 -1
- package/dist/browser/actions.js +0 -44
- package/dist/browser/actions.js.map +0 -1
- package/dist/browser/agent.d.ts +0 -21
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js +0 -13
- package/dist/browser/agent.js.map +0 -1
- package/dist/browser/context.d.ts +0 -43
- package/dist/browser/context.d.ts.map +0 -1
- package/dist/browser/context.js +0 -54
- package/dist/browser/context.js.map +0 -1
- package/dist/browser/fingerprint.d.ts +0 -4
- package/dist/browser/fingerprint.d.ts.map +0 -1
- package/dist/browser/fingerprint.js +0 -12
- package/dist/browser/fingerprint.js.map +0 -1
- package/dist/browser/index.d.ts +0 -11
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -11
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/playwright.d.ts +0 -11
- package/dist/browser/playwright.d.ts.map +0 -1
- package/dist/browser/playwright.js +0 -24
- package/dist/browser/playwright.js.map +0 -1
- package/dist/browser/recorder.d.ts +0 -21
- package/dist/browser/recorder.d.ts.map +0 -1
- package/dist/browser/recorder.js +0 -58
- package/dist/browser/recorder.js.map +0 -1
- package/dist/browser/session.d.ts +0 -14
- package/dist/browser/session.d.ts.map +0 -1
- package/dist/browser/session.js +0 -21
- package/dist/browser/session.js.map +0 -1
- package/dist/browser/snapshot.d.ts +0 -69
- package/dist/browser/snapshot.d.ts.map +0 -1
- package/dist/browser/snapshot.js +0 -133
- package/dist/browser/snapshot.js.map +0 -1
- package/dist/captcha/contract.d.ts +0 -35
- package/dist/captcha/contract.d.ts.map +0 -1
- package/dist/captcha/contract.js +0 -17
- package/dist/captcha/contract.js.map +0 -1
- package/dist/captcha/evidence.d.ts +0 -10
- package/dist/captcha/evidence.d.ts.map +0 -1
- package/dist/captcha/evidence.js +0 -9
- package/dist/captcha/evidence.js.map +0 -1
- package/dist/captcha/guard.d.ts +0 -23
- package/dist/captcha/guard.d.ts.map +0 -1
- package/dist/captcha/guard.js +0 -12
- package/dist/captcha/guard.js.map +0 -1
- package/dist/cli/main.d.ts +0 -4
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -46
- package/dist/cli/main.js.map +0 -1
- package/dist/core/errors.d.ts +0 -84
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/errors.js +0 -54
- package/dist/core/errors.js.map +0 -1
- package/dist/core/events.d.ts +0 -9
- package/dist/core/events.d.ts.map +0 -1
- package/dist/core/events.js +0 -21
- package/dist/core/events.js.map +0 -1
- package/dist/core/hash.d.ts +0 -10
- package/dist/core/hash.d.ts.map +0 -1
- package/dist/core/hash.js +0 -82
- package/dist/core/hash.js.map +0 -1
- package/dist/core/ids.d.ts +0 -10
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -15
- package/dist/core/ids.js.map +0 -1
- package/dist/delivery/manifest.d.ts +0 -35
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/manifest.js +0 -68
- package/dist/delivery/manifest.js.map +0 -1
- package/dist/deploy/index.d.ts +0 -6
- package/dist/deploy/index.d.ts.map +0 -1
- package/dist/deploy/index.js +0 -6
- package/dist/deploy/index.js.map +0 -1
- package/dist/dispatch/resumable.d.ts +0 -102
- package/dist/dispatch/resumable.d.ts.map +0 -1
- package/dist/dispatch/resumable.js +0 -64
- package/dist/dispatch/resumable.js.map +0 -1
- package/dist/dispatch/workflow.d.ts +0 -6
- package/dist/dispatch/workflow.d.ts.map +0 -1
- package/dist/dispatch/workflow.js +0 -37
- package/dist/dispatch/workflow.js.map +0 -1
- package/dist/domain/artifacts.d.ts +0 -12
- package/dist/domain/artifacts.d.ts.map +0 -1
- package/dist/domain/artifacts.js +0 -14
- package/dist/domain/artifacts.js.map +0 -1
- package/dist/domain/jobs.d.ts +0 -12
- package/dist/domain/jobs.d.ts.map +0 -1
- package/dist/domain/jobs.js +0 -20
- package/dist/domain/jobs.js.map +0 -1
- package/dist/domain/providers.d.ts +0 -10
- package/dist/domain/providers.d.ts.map +0 -1
- package/dist/domain/providers.js +0 -8
- package/dist/domain/providers.js.map +0 -1
- package/dist/domain/runtime.d.ts +0 -14
- package/dist/domain/runtime.d.ts.map +0 -1
- package/dist/domain/runtime.js +0 -10
- package/dist/domain/runtime.js.map +0 -1
- package/dist/domain/sessions.d.ts +0 -12
- package/dist/domain/sessions.d.ts.map +0 -1
- package/dist/domain/sessions.js +0 -57
- package/dist/domain/sessions.js.map +0 -1
- package/dist/examples/localjob.d.ts +0 -2
- package/dist/examples/localjob.d.ts.map +0 -1
- package/dist/examples/localjob.js.map +0 -1
- package/dist/examples/publicapi.d.ts +0 -2
- package/dist/examples/publicapi.d.ts.map +0 -1
- package/dist/examples/publicapi.js.map +0 -1
- package/dist/extension/build.d.ts +0 -9
- package/dist/extension/build.d.ts.map +0 -1
- package/dist/extension/build.js +0 -85
- package/dist/extension/build.js.map +0 -1
- package/dist/extension/content.d.ts +0 -6
- package/dist/extension/content.d.ts.map +0 -1
- package/dist/extension/content.js +0 -135
- package/dist/extension/content.js.map +0 -1
- package/dist/extension/index.d.ts +0 -7
- package/dist/extension/index.d.ts.map +0 -1
- package/dist/extension/index.js +0 -7
- package/dist/extension/index.js.map +0 -1
- package/dist/extension/pagebridge.d.ts +0 -6
- package/dist/extension/pagebridge.d.ts.map +0 -1
- package/dist/extension/pagebridge.js +0 -42
- package/dist/extension/pagebridge.js.map +0 -1
- package/dist/extension/permissions.d.ts +0 -26
- package/dist/extension/permissions.d.ts.map +0 -1
- package/dist/extension/permissions.js +0 -28
- package/dist/extension/permissions.js.map +0 -1
- package/dist/extension/popup.d.ts +0 -5
- package/dist/extension/popup.d.ts.map +0 -1
- package/dist/extension/popup.js +0 -26
- package/dist/extension/popup.js.map +0 -1
- package/dist/extension/protocol.d.ts +0 -58
- package/dist/extension/protocol.d.ts.map +0 -1
- package/dist/extension/protocol.js +0 -101
- package/dist/extension/protocol.js.map +0 -1
- package/dist/extension/serviceworker.d.ts +0 -25
- package/dist/extension/serviceworker.d.ts.map +0 -1
- package/dist/extension/serviceworker.js +0 -102
- package/dist/extension/serviceworker.js.map +0 -1
- package/dist/extension/worker.d.ts +0 -28
- package/dist/extension/worker.d.ts.map +0 -1
- package/dist/extension/worker.js +0 -22
- package/dist/extension/worker.js.map +0 -1
- package/dist/format/check.d.ts +0 -8
- package/dist/format/check.d.ts.map +0 -1
- package/dist/format/check.js +0 -38
- package/dist/format/check.js.map +0 -1
- package/dist/library/public.d.ts.map +0 -1
- package/dist/library/public.js.map +0 -1
- package/dist/mcp/browser.d.ts +0 -9
- package/dist/mcp/browser.d.ts.map +0 -1
- package/dist/mcp/browser.js +0 -13
- package/dist/mcp/browser.js.map +0 -1
- package/dist/mcp/server.d.ts +0 -35
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -40
- package/dist/mcp/server.js.map +0 -1
- package/dist/mcp/transport.d.ts +0 -7
- package/dist/mcp/transport.d.ts.map +0 -1
- package/dist/mcp/transport.js +0 -17
- package/dist/mcp/transport.js.map +0 -1
- package/dist/memory/bridge.d.ts +0 -14
- package/dist/memory/bridge.d.ts.map +0 -1
- package/dist/memory/bridge.js +0 -16
- package/dist/memory/bridge.js.map +0 -1
- package/dist/memory/engine.d.ts +0 -38
- package/dist/memory/engine.d.ts.map +0 -1
- package/dist/memory/engine.js +0 -109
- package/dist/memory/engine.js.map +0 -1
- package/dist/memory/modes.d.ts +0 -43
- package/dist/memory/modes.d.ts.map +0 -1
- package/dist/memory/modes.js +0 -66
- package/dist/memory/modes.js.map +0 -1
- package/dist/memory/objects.d.ts +0 -21
- package/dist/memory/objects.d.ts.map +0 -1
- package/dist/memory/objects.js +0 -20
- package/dist/memory/objects.js.map +0 -1
- package/dist/memory/planner.d.ts +0 -66
- package/dist/memory/planner.d.ts.map +0 -1
- package/dist/memory/planner.js +0 -108
- package/dist/memory/planner.js.map +0 -1
- package/dist/memory/targets.d.ts +0 -78
- package/dist/memory/targets.d.ts.map +0 -1
- package/dist/memory/targets.js +0 -29
- package/dist/memory/targets.js.map +0 -1
- package/dist/memory/transforms.d.ts +0 -19
- package/dist/memory/transforms.d.ts.map +0 -1
- package/dist/memory/transforms.js +0 -14
- package/dist/memory/transforms.js.map +0 -1
- package/dist/modes/matrix.d.ts +0 -23
- package/dist/modes/matrix.d.ts.map +0 -1
- package/dist/modes/matrix.js +0 -18
- package/dist/modes/matrix.js.map +0 -1
- package/dist/modes/modes.d.ts +0 -33
- package/dist/modes/modes.d.ts.map +0 -1
- package/dist/modes/modes.js +0 -15
- package/dist/modes/modes.js.map +0 -1
- package/dist/modes/resolve.d.ts +0 -96
- package/dist/modes/resolve.d.ts.map +0 -1
- package/dist/modes/resolve.js +0 -48
- package/dist/modes/resolve.js.map +0 -1
- package/dist/observability/metrics.d.ts +0 -14
- package/dist/observability/metrics.d.ts.map +0 -1
- package/dist/observability/metrics.js +0 -18
- package/dist/observability/metrics.js.map +0 -1
- package/dist/packager/manifest.d.ts +0 -66
- package/dist/packager/manifest.d.ts.map +0 -1
- package/dist/packager/manifest.js +0 -94
- package/dist/packager/manifest.js.map +0 -1
- package/dist/packager/publish.d.ts +0 -51
- package/dist/packager/publish.d.ts.map +0 -1
- package/dist/packager/publish.js +0 -18
- package/dist/packager/publish.js.map +0 -1
- package/dist/packager/targetcli.d.ts +0 -7
- package/dist/packager/targetcli.d.ts.map +0 -1
- package/dist/packager/targetcli.js +0 -22
- package/dist/packager/targetcli.js.map +0 -1
- package/dist/persistence/adapter.d.ts +0 -5
- package/dist/persistence/adapter.d.ts.map +0 -1
- package/dist/persistence/adapter.js +0 -11
- package/dist/persistence/adapter.js.map +0 -1
- package/dist/persistence/drizzle.d.ts +0 -2
- package/dist/persistence/drizzle.d.ts.map +0 -1
- package/dist/persistence/drizzle.js +0 -12
- package/dist/persistence/drizzle.js.map +0 -1
- package/dist/persistence/memory.d.ts +0 -2
- package/dist/persistence/memory.d.ts.map +0 -1
- package/dist/persistence/memory.js +0 -27
- package/dist/persistence/memory.js.map +0 -1
- package/dist/persistence/migrations.d.ts +0 -19
- package/dist/persistence/migrations.d.ts.map +0 -1
- package/dist/persistence/migrations.js +0 -13
- package/dist/persistence/migrations.js.map +0 -1
- package/dist/persistence/prisma.d.ts +0 -2
- package/dist/persistence/prisma.d.ts.map +0 -1
- package/dist/persistence/prisma.js +0 -25
- package/dist/persistence/prisma.js.map +0 -1
- package/dist/persistence/schema.d.ts +0 -63
- package/dist/persistence/schema.d.ts.map +0 -1
- package/dist/persistence/schema.js +0 -28
- package/dist/persistence/schema.js.map +0 -1
- package/dist/persistence/sql.d.ts +0 -3
- package/dist/persistence/sql.d.ts.map +0 -1
- package/dist/persistence/sql.js +0 -37
- package/dist/persistence/sql.js.map +0 -1
- package/dist/protocol/blocks.d.ts +0 -10
- package/dist/protocol/blocks.d.ts.map +0 -1
- package/dist/protocol/blocks.js +0 -26
- package/dist/protocol/blocks.js.map +0 -1
- package/dist/protocol/json.d.ts +0 -6
- package/dist/protocol/json.d.ts.map +0 -1
- package/dist/protocol/json.js +0 -6
- package/dist/protocol/json.js.map +0 -1
- package/dist/protocol/ndjson.d.ts +0 -3
- package/dist/protocol/ndjson.d.ts.map +0 -1
- package/dist/protocol/ndjson.js +0 -20
- package/dist/protocol/ndjson.js.map +0 -1
- package/dist/protocol/sse.d.ts +0 -7
- package/dist/protocol/sse.d.ts.map +0 -1
- package/dist/protocol/sse.js +0 -25
- package/dist/protocol/sse.js.map +0 -1
- package/dist/proxy/pool.d.ts +0 -9
- package/dist/proxy/pool.d.ts.map +0 -1
- package/dist/proxy/pool.js +0 -27
- package/dist/proxy/pool.js.map +0 -1
- package/dist/queue/idempotency.d.ts +0 -10
- package/dist/queue/idempotency.d.ts.map +0 -1
- package/dist/queue/idempotency.js +0 -13
- package/dist/queue/idempotency.js.map +0 -1
- package/dist/queue/persistent.d.ts +0 -14
- package/dist/queue/persistent.d.ts.map +0 -1
- package/dist/queue/persistent.js +0 -66
- package/dist/queue/persistent.js.map +0 -1
- package/dist/queue/queue.d.ts +0 -7
- package/dist/queue/queue.d.ts.map +0 -1
- package/dist/queue/queue.js +0 -55
- package/dist/queue/queue.js.map +0 -1
- package/dist/queue/saga.d.ts +0 -5
- package/dist/queue/saga.d.ts.map +0 -1
- package/dist/queue/saga.js +0 -20
- package/dist/queue/saga.js.map +0 -1
- package/dist/release/assets.d.ts +0 -144
- package/dist/release/assets.d.ts.map +0 -1
- package/dist/release/assets.js +0 -157
- package/dist/release/assets.js.map +0 -1
- package/dist/release/evidence.d.ts +0 -92
- package/dist/release/evidence.d.ts.map +0 -1
- package/dist/release/evidence.js +0 -148
- package/dist/release/evidence.js.map +0 -1
- package/dist/release/verify.d.ts +0 -12
- package/dist/release/verify.d.ts.map +0 -1
- package/dist/release/verify.js +0 -112
- package/dist/release/verify.js.map +0 -1
- package/dist/runners/chain.d.ts +0 -125
- package/dist/runners/chain.d.ts.map +0 -1
- package/dist/runners/chain.js +0 -95
- package/dist/runners/chain.js.map +0 -1
- package/dist/runners/health.d.ts +0 -36
- package/dist/runners/health.d.ts.map +0 -1
- package/dist/runners/health.js +0 -26
- package/dist/runners/health.js.map +0 -1
- package/dist/runners/heartbeat.d.ts +0 -23
- package/dist/runners/heartbeat.d.ts.map +0 -1
- package/dist/runners/heartbeat.js +0 -29
- package/dist/runners/heartbeat.js.map +0 -1
- package/dist/runners/inprocess.d.ts +0 -17
- package/dist/runners/inprocess.d.ts.map +0 -1
- package/dist/runners/inprocess.js +0 -20
- package/dist/runners/inprocess.js.map +0 -1
- package/dist/runners/scheduler.d.ts +0 -5
- package/dist/runners/scheduler.d.ts.map +0 -1
- package/dist/runners/scheduler.js +0 -19
- package/dist/runners/scheduler.js.map +0 -1
- package/dist/runtime/abort.d.ts +0 -8
- package/dist/runtime/abort.d.ts.map +0 -1
- package/dist/runtime/abort.js +0 -13
- package/dist/runtime/abort.js.map +0 -1
- package/dist/runtime/compatibility.d.ts +0 -37
- package/dist/runtime/compatibility.d.ts.map +0 -1
- package/dist/runtime/compatibility.js +0 -10
- package/dist/runtime/compatibility.js.map +0 -1
- package/dist/runtime/detect.d.ts +0 -13
- package/dist/runtime/detect.d.ts.map +0 -1
- package/dist/runtime/detect.js +0 -18
- package/dist/runtime/detect.js.map +0 -1
- package/dist/runtime/engine.d.ts +0 -20
- package/dist/runtime/engine.d.ts.map +0 -1
- package/dist/runtime/engine.js +0 -61
- package/dist/runtime/engine.js.map +0 -1
- package/dist/runtime/retry.d.ts +0 -19
- package/dist/runtime/retry.d.ts.map +0 -1
- package/dist/runtime/retry.js +0 -54
- package/dist/runtime/retry.js.map +0 -1
- package/dist/runtime/worker.d.ts +0 -10
- package/dist/runtime/worker.d.ts.map +0 -1
- package/dist/runtime/worker.js +0 -25
- package/dist/runtime/worker.js.map +0 -1
- package/dist/scrape/cache.d.ts +0 -45
- package/dist/scrape/cache.d.ts.map +0 -1
- package/dist/scrape/cache.js +0 -45
- package/dist/scrape/cache.js.map +0 -1
- package/dist/scrape/crawl.d.ts +0 -47
- package/dist/scrape/crawl.d.ts.map +0 -1
- package/dist/scrape/crawl.js +0 -114
- package/dist/scrape/crawl.js.map +0 -1
- package/dist/scrape/extract.d.ts +0 -63
- package/dist/scrape/extract.d.ts.map +0 -1
- package/dist/scrape/extract.js +0 -47
- package/dist/scrape/extract.js.map +0 -1
- package/dist/scrape/normalize.d.ts +0 -52
- package/dist/scrape/normalize.d.ts.map +0 -1
- package/dist/scrape/normalize.js +0 -102
- package/dist/scrape/normalize.js.map +0 -1
- package/dist/scrape/robots.d.ts +0 -37
- package/dist/scrape/robots.d.ts.map +0 -1
- package/dist/scrape/robots.js +0 -71
- package/dist/scrape/robots.js.map +0 -1
- package/dist/scrape/schema.d.ts +0 -23
- package/dist/scrape/schema.d.ts.map +0 -1
- package/dist/scrape/schema.js +0 -92
- package/dist/scrape/schema.js.map +0 -1
- package/dist/scrape/scraper.d.ts +0 -6
- package/dist/scrape/scraper.d.ts.map +0 -1
- package/dist/scrape/scraper.js +0 -46
- package/dist/scrape/scraper.js.map +0 -1
- package/dist/scrape/semantic.d.ts +0 -25
- package/dist/scrape/semantic.d.ts.map +0 -1
- package/dist/scrape/semantic.js +0 -27
- package/dist/scrape/semantic.js.map +0 -1
- package/dist/server/node.d.ts +0 -7
- package/dist/server/node.d.ts.map +0 -1
- package/dist/server/node.js.map +0 -1
- package/dist/sessions/file.d.ts +0 -25
- package/dist/sessions/file.d.ts.map +0 -1
- package/dist/sessions/file.js +0 -13
- package/dist/sessions/file.js.map +0 -1
- package/dist/sessions/replay.d.ts +0 -9
- package/dist/sessions/replay.d.ts.map +0 -1
- package/dist/sessions/replay.js +0 -70
- package/dist/sessions/replay.js.map +0 -1
- package/dist/sessions/store.d.ts +0 -25
- package/dist/sessions/store.d.ts.map +0 -1
- package/dist/sessions/store.js +0 -13
- package/dist/sessions/store.js.map +0 -1
- package/dist/storage/adapter.d.ts +0 -5
- package/dist/storage/adapter.d.ts.map +0 -1
- package/dist/storage/adapter.js +0 -11
- package/dist/storage/adapter.js.map +0 -1
- package/dist/storage/cache.d.ts +0 -23
- package/dist/storage/cache.d.ts.map +0 -1
- package/dist/storage/cache.js +0 -79
- package/dist/storage/cache.js.map +0 -1
- package/dist/storage/checksum.d.ts +0 -3
- package/dist/storage/checksum.d.ts.map +0 -1
- package/dist/storage/checksum.js +0 -19
- package/dist/storage/checksum.js.map +0 -1
- package/dist/storage/chunked.d.ts +0 -21
- package/dist/storage/chunked.d.ts.map +0 -1
- package/dist/storage/chunked.js +0 -67
- package/dist/storage/chunked.js.map +0 -1
- package/dist/storage/content.d.ts +0 -24
- package/dist/storage/content.d.ts.map +0 -1
- package/dist/storage/content.js +0 -44
- package/dist/storage/content.js.map +0 -1
- package/dist/storage/filehosting.d.ts +0 -2
- package/dist/storage/filehosting.d.ts.map +0 -1
- package/dist/storage/filehosting.js +0 -24
- package/dist/storage/filehosting.js.map +0 -1
- package/dist/storage/githubcontents.d.ts +0 -2
- package/dist/storage/githubcontents.d.ts.map +0 -1
- package/dist/storage/githubcontents.js +0 -29
- package/dist/storage/githubcontents.js.map +0 -1
- package/dist/storage/index.d.ts +0 -12
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js +0 -12
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/local.d.ts +0 -2
- package/dist/storage/local.d.ts.map +0 -1
- package/dist/storage/local.js +0 -72
- package/dist/storage/local.js.map +0 -1
- package/dist/storage/memory.d.ts +0 -6
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js +0 -36
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/pool.d.ts +0 -95
- package/dist/storage/pool.d.ts.map +0 -1
- package/dist/storage/pool.js +0 -202
- package/dist/storage/pool.js.map +0 -1
- package/dist/storage/s3compatible.d.ts +0 -3
- package/dist/storage/s3compatible.d.ts.map +0 -1
- package/dist/storage/s3compatible.js +0 -70
- package/dist/storage/s3compatible.js.map +0 -1
- package/dist/storage/sync.d.ts +0 -47
- package/dist/storage/sync.d.ts.map +0 -1
- package/dist/storage/sync.js +0 -75
- package/dist/storage/sync.js.map +0 -1
- package/dist/surfaces/adapters.d.ts +0 -97
- package/dist/surfaces/adapters.d.ts.map +0 -1
- package/dist/surfaces/adapters.js +0 -50
- package/dist/surfaces/adapters.js.map +0 -1
- package/dist/surfaces/controls.d.ts +0 -18
- package/dist/surfaces/controls.d.ts.map +0 -1
- package/dist/surfaces/controls.js +0 -45
- package/dist/surfaces/controls.js.map +0 -1
- package/dist/surfaces/manifest.d.ts +0 -68
- package/dist/surfaces/manifest.d.ts.map +0 -1
- package/dist/surfaces/manifest.js +0 -42
- package/dist/surfaces/manifest.js.map +0 -1
- package/dist/surfaces/n8n.d.ts +0 -25
- package/dist/surfaces/n8n.d.ts.map +0 -1
- package/dist/surfaces/n8n.js +0 -31
- package/dist/surfaces/n8n.js.map +0 -1
- package/dist/surfaces/operations.d.ts +0 -32
- package/dist/surfaces/operations.d.ts.map +0 -1
- package/dist/surfaces/operations.js +0 -53
- package/dist/surfaces/operations.js.map +0 -1
- package/dist/surfaces/requirements.d.ts +0 -29
- package/dist/surfaces/requirements.d.ts.map +0 -1
- package/dist/surfaces/requirements.js +0 -50
- package/dist/surfaces/requirements.js.map +0 -1
- package/dist/surfaces/targets.d.ts +0 -196
- package/dist/surfaces/targets.d.ts.map +0 -1
- package/dist/surfaces/targets.js +0 -27
- package/dist/surfaces/targets.js.map +0 -1
- package/dist/webhook/delivery.d.ts +0 -18
- package/dist/webhook/delivery.d.ts.map +0 -1
- package/dist/webhook/delivery.js +0 -36
- package/dist/webhook/delivery.js.map +0 -1
- package/dist/webhook/receiver.d.ts +0 -26
- package/dist/webhook/receiver.d.ts.map +0 -1
- package/dist/webhook/receiver.js +0 -24
- package/dist/webhook/receiver.js.map +0 -1
- package/dist/webhook/signature.d.ts +0 -3
- package/dist/webhook/signature.d.ts.map +0 -1
- package/dist/webhook/signature.js +0 -7
- package/dist/webhook/signature.js.map +0 -1
- package/dist/workflow/manifest.d.ts +0 -22
- package/dist/workflow/manifest.d.ts.map +0 -1
- package/dist/workflow/manifest.js +0 -21
- package/dist/workflow/manifest.js.map +0 -1
- package/dist/workflow/registry.d.ts +0 -7
- package/dist/workflow/registry.d.ts.map +0 -1
- package/dist/workflow/registry.js +0 -18
- package/dist/workflow/registry.js.map +0 -1
- package/dist/workflow/templates.d.ts +0 -6
- package/dist/workflow/templates.d.ts.map +0 -1
- package/dist/workflow/templates.js +0 -18
- package/dist/workflow/templates.js.map +0 -1
- package/dist/workflow/triggers.d.ts +0 -64
- package/dist/workflow/triggers.d.ts.map +0 -1
- package/dist/workflow/triggers.js +0 -99
- package/dist/workflow/triggers.js.map +0 -1
- package/docs/logs/.gitkeep +0 -0
- package/docs/plans/00.index.md +0 -50
- package/docs/plans/01.architecture.md +0 -86
- package/docs/plans/02.research.computer.use.md +0 -58
- package/docs/plans/03.research.captcha.bypass.md +0 -68
- package/docs/plans/04.research.sandbox.ai.md +0 -52
- package/docs/plans/05.capture.platform.md +0 -57
- package/docs/plans/06.dependencies.md +0 -97
- package/docs/plans/07.captcha.test.page.md +0 -41
- package/docs/plans/08.production.infra.md +0 -70
- package/docs/plans/09.database.schema.md +0 -121
- package/docs/plans/10.cloudinary.storage.md +0 -57
- package/docs/plans/11.movement.logs.json.md +0 -72
- package/docs/plans/12.research.atlas.agent.browser.md +0 -79
- package/docs/plans/13.research.anti.detection.md +0 -898
- package/docs/plans/14.research.proxy.md +0 -1495
- package/docs/plans/15.research.retry.rate.limit.md +0 -1958
- package/docs/plans/16.research.crawling.md +0 -1417
- package/docs/plans/17.research.caching.md +0 -1610
- package/docs/plans/18.research.content.extraction.md +0 -1952
- package/docs/plans/19.research.errors.events.md +0 -1523
- package/docs/plans/20.research.zod.validation.md +0 -1350
- package/docs/plans/21.research.batch.concurrency.md +0 -1888
- package/docs/plans/22.research.universal.runtime.md +0 -944
- package/docs/plans/23.research.ai.integration.md +0 -1465
- package/docs/plans/24.research.memory.persistence.md +0 -1979
- package/docs/plans/25.research.server.api.md +0 -342
- package/docs/plans/26.research.compilation.md +0 -249
- package/docs/plans/27.research.html.parsing.md +0 -251
- package/docs/plans/28.action.plan.md +0 -50
- package/docs/plans/29.api.reference.md +0 -174
- package/docs/plans/30.architecture.plan.md +0 -94
- package/docs/plans/31.auditoria.dados.md +0 -163
- package/docs/plans/32.bots.automacao.computacional.md +0 -214
- package/docs/plans/33.bots.codigo.revisao.md +0 -220
- package/docs/plans/34.bots.seguranca.cicd.md +0 -366
- package/docs/plans/35.comparativo.concorrencia.md +0 -464
- package/docs/plans/36.computational.memory.md +0 -340
- package/docs/plans/37.deploystrategy.md +0 -394
- package/docs/plans/38.flow.md +0 -155
- package/docs/plans/39.multi.platform.bot.md +0 -252
- package/docs/plans/40.npm.publish.md +0 -250
- package/docs/plans/41.o.que.falta.md +0 -407
- package/docs/plans/42.pesquisa.concorrencia.md +0 -721
- package/docs/plans/43.plan.universal.architecture.md +0 -496
- package/docs/plans/44.reference.md +0 -100
- package/docs/plans/45.robotarchitecture.md +0 -237
- package/docs/plans/46.scdnintegration.md +0 -284
- package/docs/plans/47.multiforge.readme.md +0 -129
- package/docs/plans/48.theory.v4.repo.os.md +0 -152
- package/docs/plans/49.third.party.infra.md +0 -12
- package/docs/plans/50.file.as.compute.md +0 -39
- package/docs/plans/51.architecture.virtual.processor.md +0 -80
- package/docs/plans/52.manifesto.v8.md +0 -11
- package/docs/plans/58.cdn.list.md +0 -23
- package/docs/plans/59.sql.frameworks.md +0 -33
- package/docs/plans/60.sql.thirdparty.md +0 -26
- package/docs/plans/61.objective.multiforge.md +0 -63
- package/docs/plans/62.huggingface.upload.md +0 -26
- package/docs/plans/63.kaggle.upload.md +0 -24
- package/docs/plans/64.npm.storage.md +0 -30
- package/docs/plans/65.rclone.terabox.md +0 -32
- package/docs/plans/66.buckets.and.models.todo.md +0 -14
- package/docs/plans/67.database.todo.md +0 -13
- package/docs/plans/68.deploy.packages.todo.md +0 -12
- package/docs/plans/69.report.human.operator.md +0 -133
- package/docs/plans/70.report.brain2qwerty.ems.md +0 -135
- package/docs/plans/71.report.hd.infinito.vram.md +0 -155
- package/docs/plans/72.plan.hd.infinito.node.md +0 -146
- package/docs/plans/73.plan.scifi.repos.md +0 -125
- package/docs/plans/74.000.manifesto.v8.flat.2..md +0 -11
- package/docs/plans/README.md +0 -489
- package/docs/plans/aggregate_platforms.mjs +0 -146
- package/docs/plans/examplesession.json +0 -36
- package/docs/plans/missing-facts.md +0 -192
- package/docs/plans/models.md +0 -64
- package/docs/plans/organize.cjs +0 -270
- package/docs/plans/platforms.md +0 -2887
- package/docs/plans/sites.md +0 -31322
- package/docs/sources/farm.py +0 -117
- package/docs/sources/html/saddle1.html +0 -132
- package/docs/sources/html/saddle2.html +0 -157
- package/docs/sources/html/saddle3.html +0 -119
- package/docs/sources/html/saddle4.html +0 -144
- package/docs/sources/html/saddle5.html +0 -72
- package/docs/sources/html/saddle6.html +0 -171
- package/docs/sources/html/saddle7.html +0 -236
- package/docs/sources/saddle.ts +0 -74
- package/docs/sources/schema.prisma +0 -88
- package/docs/sources/script.sh +0 -64
- package/docs/sources/workflows.yml +0 -458
- package/docs/talks1/_body.txt +0 -14
- package/docs/talks1/_index.md +0 -15
- package/docs/talks1/_screenshot.png +0 -0
- package/docs/talks1/assistant-01.md +0 -5
- package/docs/talks1/assistant-02.md +0 -5
- package/docs/talks1/assistant-03.md +0 -531
- package/docs/talks1/assistant-04.md +0 -26
- package/docs/talks1/assistant-05.md +0 -774
- package/docs/talks1/assistant-06.md +0 -1718
- package/docs/talks1/scrape-share.cjs +0 -185
- package/docs/talks1/scrape-share.ts +0 -183
- package/docs/talks1/user-01.md +0 -3
- package/docs/talks1/user-02.md +0 -3
- package/docs/talks1/user-03.md +0 -88
- package/docs/talks1/user-04.md +0 -3
- package/docs/talks1/user-05.md +0 -3
- package/docs/talks1/user-06.md +0 -88
- package/docs/talks1/user-07.md +0 -88
- package/docs/talks2/_body.txt +0 -14
- package/docs/talks2/_index.md +0 -16
- package/docs/talks2/_screenshot.png +0 -0
- package/docs/talks2/assistant-01.md +0 -5
- package/docs/talks2/assistant-02.md +0 -5
- package/docs/talks2/assistant-03.md +0 -424
- package/docs/talks2/assistant-04.md +0 -598
- package/docs/talks2/assistant-05.md +0 -1280
- package/docs/talks2/assistant-06.md +0 -1227
- package/docs/talks2/assistant-07.md +0 -1252
- package/docs/talks2/user-01.md +0 -3
- package/docs/talks2/user-02.md +0 -3
- package/docs/talks2/user-03.md +0 -88
- package/docs/talks2/user-04.md +0 -88
- package/docs/talks2/user-05.md +0 -88
- package/docs/talks2/user-06.md +0 -88
- package/docs/talks2/user-07.md +0 -3
- package/docs/talks3/_body.txt +0 -467
- package/docs/talks3/_index.md +0 -10
- package/docs/talks3/_screenshot.png +0 -0
- package/docs/talks3/assistant-01.md +0 -417
- package/docs/talks3/assistant-02.md +0 -417
- package/docs/talks3/assistant-03.md +0 -29
- package/docs/talks3/assistant-04.md +0 -727
- package/docs/talks3/user-01.md +0 -88
- package/docs/talks3/user-02.md +0 -88
- package/docs/talks3/user-03.md +0 -3
- package/docs/talks3/user-04.md +0 -3
- package/docs/talks4/_body.txt +0 -14
- package/docs/talks4/_index.md +0 -12
- package/docs/talks4/_screenshot.png +0 -0
- package/docs/talks4/assistant-01.md +0 -5
- package/docs/talks4/assistant-02.md +0 -5
- package/docs/talks4/assistant-03.md +0 -35
- package/docs/talks4/assistant-04.md +0 -512
- package/docs/talks4/assistant-05.md +0 -599
- package/docs/talks4/user-01.md +0 -3
- package/docs/talks4/user-02.md +0 -3
- package/docs/talks4/user-03.md +0 -88
- package/docs/talks4/user-04.md +0 -88
- package/docs/talks4/user-05.md +0 -7
- package/docs/talks5/_body.txt +0 -14
- package/docs/talks5/_index.md +0 -13
- package/docs/talks5/_screenshot.png +0 -0
- package/docs/talks5/assistant-01.md +0 -5
- package/docs/talks5/assistant-02.md +0 -5
- package/docs/talks5/assistant-03.md +0 -690
- package/docs/talks5/assistant-04.md +0 -758
- package/docs/talks5/assistant-05.md +0 -974
- package/docs/talks5/user-01.md +0 -3
- package/docs/talks5/user-02.md +0 -3
- package/docs/talks5/user-03.md +0 -105
- package/docs/talks5/user-04.md +0 -105
- package/docs/talks5/user-05.md +0 -63
- package/docs/talks5/user-06.md +0 -105
- package/docs/talks6/_body.txt +0 -14
- package/docs/talks6/_index.md +0 -9
- package/docs/talks6/_screenshot.png +0 -0
- package/docs/talks6/assistant-01.md +0 -5
- package/docs/talks6/assistant-02.md +0 -5
- package/docs/talks6/assistant-03.md +0 -1499
- package/docs/talks6/user-01.md +0 -3
- package/docs/talks6/user-02.md +0 -3
- package/docs/talks6/user-03.md +0 -88
- package/docs/talks6/user-04.md +0 -88
- package/docs/talks7/_body.txt +0 -14
- package/docs/talks7/_index.md +0 -10
- package/docs/talks7/_screenshot.png +0 -0
- package/docs/talks7/assistant-01.md +0 -5
- package/docs/talks7/assistant-02.md +0 -5
- package/docs/talks7/assistant-03.md +0 -523
- package/docs/talks7/assistant-04.md +0 -617
- package/docs/talks7/user-01.md +0 -3
- package/docs/talks7/user-02.md +0 -3
- package/docs/talks7/user-03.md +0 -105
- package/docs/talks7/user-04.md +0 -67
- package/docs/talks8/conversa1.txt +0 -1322
- package/docs/talks8/conversa2.txt +0 -237
- package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +0 -174
- package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +0 -59
- package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +0 -91
- package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +0 -116
- package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +0 -128
- package/docs/talks9/README (2).md +0 -489
- package/docs/talks9/README.md +0 -198
- package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +0 -80
- package/docs/talks9/conversa.txt +0 -544
- package/docs/talks9/other (2).md +0 -39
- package/docs/talks9/other.md +0 -57
- package/docs/talks9/outro.txt +0 -24
- /package/{extension/README.md → docs/extension.md} +0 -0
|
@@ -0,0 +1,2697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webscrape.ts — the legacy WebScrape toolkit generation.
|
|
3
|
+
*
|
|
4
|
+
* This file carries the complete legacy scrape sub-project of the former
|
|
5
|
+
* repository (thirty sections: types, errors, utils, port, tokens,
|
|
6
|
+
* chunking, formats, serialize, headers, session, pool, proxy, proxypool,
|
|
7
|
+
* rate-limiter, retry, middleware, events, renderer, sitemap, agent,
|
|
8
|
+
* llms-txt, fetch, batch, browser, scrape, crawler, server, cli and
|
|
9
|
+
* dev-server) consolidated into the single root-level file of the grand
|
|
10
|
+
* merge. The optional npm runtimes of the toolkit (emittery, p-limit,
|
|
11
|
+
* p-retry, cheerio, playwright, hono, commander) load lazily so nothing is
|
|
12
|
+
* required at module load that the package does not ship; the CLI,
|
|
13
|
+
* dev-server and hono API bootstraps are exported factories.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { JSDOM } from "jsdom";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { createServer as createnodeserver } from "node:http";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
22
|
+
/* Section 12: scrape/types.ts */
|
|
23
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
24
|
+
export const ScrapeFormatSchema = z.enum(['markdown', 'html', 'text', 'json', 'xml', 'redis']);
|
|
25
|
+
export const SerializeTargetSchema = z.enum(['markdown', 'xml', 'json', 'redis', 'text']);
|
|
26
|
+
export const ScrapeOptionsSchema = z.strictObject({
|
|
27
|
+
url: z.string().url().optional(),
|
|
28
|
+
html: z.string().optional(),
|
|
29
|
+
mode: z.enum(['auto', 'fetch', 'browser']).prefault('auto').optional(),
|
|
30
|
+
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle', 'commit']).prefault('networkidle').optional(),
|
|
31
|
+
timeout: z.number().positive().prefault(30000).optional(),
|
|
32
|
+
scroll: z.boolean().prefault(false).optional(),
|
|
33
|
+
scrollDelay: z.number().nonnegative().prefault(300).optional(),
|
|
34
|
+
maxScrolls: z.number().positive().prefault(50).optional(),
|
|
35
|
+
removeSelectors: z.array(z.string()).prefault([]).optional(),
|
|
36
|
+
extractImages: z.boolean().prefault(true).optional(),
|
|
37
|
+
extractLinks: z.boolean().prefault(true).optional(),
|
|
38
|
+
extractTables: z.boolean().prefault(true).optional(),
|
|
39
|
+
maxContentLength: z.number().positive().optional(),
|
|
40
|
+
format: ScrapeFormatSchema.prefault('markdown').optional(),
|
|
41
|
+
language: z.string().optional(),
|
|
42
|
+
proxy: z.string().optional(),
|
|
43
|
+
retries: z.number().nonnegative().prefault(3).optional(),
|
|
44
|
+
retryDelay: z.number().positive().prefault(1000).optional(),
|
|
45
|
+
userAgent: z.string().optional(),
|
|
46
|
+
headers: z.record(z.string(), z.string()).prefault({}).optional(),
|
|
47
|
+
cache: z.boolean().prefault(false).optional(),
|
|
48
|
+
cacheTtlMs: z.number().positive().prefault(300000).optional(),
|
|
49
|
+
readable: z.boolean().optional(),
|
|
50
|
+
});
|
|
51
|
+
export const BrowserAgentConfigSchema = z.strictObject({
|
|
52
|
+
headless: z.boolean().prefault(true).optional(),
|
|
53
|
+
userAgent: z.string().optional(),
|
|
54
|
+
viewport: z
|
|
55
|
+
.strictObject({
|
|
56
|
+
width: z.number().positive().prefault(1280),
|
|
57
|
+
height: z.number().positive().prefault(720),
|
|
58
|
+
})
|
|
59
|
+
.prefault({ width: 1280, height: 720 })
|
|
60
|
+
.optional(),
|
|
61
|
+
locale: z.string().prefault('en-US').optional(),
|
|
62
|
+
geolocation: z
|
|
63
|
+
.strictObject({
|
|
64
|
+
latitude: z.number(),
|
|
65
|
+
longitude: z.number(),
|
|
66
|
+
})
|
|
67
|
+
.optional(),
|
|
68
|
+
timeout: z.number().positive().prefault(30000).optional(),
|
|
69
|
+
recordVideo: z.boolean().prefault(false).optional(),
|
|
70
|
+
proxy: z.string().prefault('').optional(),
|
|
71
|
+
storageState: z.string().prefault('').optional(),
|
|
72
|
+
blockAds: z.boolean().prefault(true).optional(),
|
|
73
|
+
stealth: z.boolean().prefault(true).optional(),
|
|
74
|
+
});
|
|
75
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
76
|
+
/* Section 13: scrape/errors.ts */
|
|
77
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
78
|
+
export const ErrorCode = {
|
|
79
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
80
|
+
INVALID_URL: 'INVALID_URL',
|
|
81
|
+
TIMEOUT: 'TIMEOUT',
|
|
82
|
+
BLOCKED: 'BLOCKED',
|
|
83
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
84
|
+
PROXY_ERROR: 'PROXY_ERROR',
|
|
85
|
+
PARSE_ERROR: 'PARSE_ERROR',
|
|
86
|
+
AUTH_REQUIRED: 'AUTH_REQUIRED',
|
|
87
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
88
|
+
BROWSER_NOT_AVAILABLE: 'BROWSER_NOT_AVAILABLE',
|
|
89
|
+
CRAWL_DEPTH_EXCEEDED: 'CRAWL_DEPTH_EXCEEDED',
|
|
90
|
+
MAX_RETRIES_EXCEEDED: 'MAX_RETRIES_EXCEEDED',
|
|
91
|
+
};
|
|
92
|
+
export class WebScrapeError extends Error {
|
|
93
|
+
code;
|
|
94
|
+
statusCode;
|
|
95
|
+
isRetryable;
|
|
96
|
+
timestamp;
|
|
97
|
+
details;
|
|
98
|
+
cause;
|
|
99
|
+
constructor(message, code, statusCode, isRetryable, details, cause) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = 'WebScrapeError';
|
|
102
|
+
this.code = code;
|
|
103
|
+
this.statusCode = statusCode;
|
|
104
|
+
this.isRetryable = isRetryable;
|
|
105
|
+
this.timestamp = new Date().toISOString();
|
|
106
|
+
this.details = details;
|
|
107
|
+
this.cause = cause;
|
|
108
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
109
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
110
|
+
}
|
|
111
|
+
toJSON() {
|
|
112
|
+
return {
|
|
113
|
+
error: {
|
|
114
|
+
name: this.name,
|
|
115
|
+
code: this.code,
|
|
116
|
+
message: this.message,
|
|
117
|
+
statusCode: this.statusCode,
|
|
118
|
+
isRetryable: this.isRetryable,
|
|
119
|
+
timestamp: this.timestamp,
|
|
120
|
+
details: this.details,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export class ValidationError extends WebScrapeError {
|
|
126
|
+
constructor(message, details, cause) {
|
|
127
|
+
super(message, ErrorCode.VALIDATION_FAILED, 400, false, details, cause);
|
|
128
|
+
this.name = 'ValidationError';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export class TimeoutError extends WebScrapeError {
|
|
132
|
+
constructor(message, details, cause) {
|
|
133
|
+
super(message, ErrorCode.TIMEOUT, 504, true, details, cause);
|
|
134
|
+
this.name = 'TimeoutError';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export class BlockedError extends WebScrapeError {
|
|
138
|
+
constructor(message, details, cause) {
|
|
139
|
+
super(message, ErrorCode.BLOCKED, 403, false, details, cause);
|
|
140
|
+
this.name = 'BlockedError';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export class RateLimitError extends WebScrapeError {
|
|
144
|
+
retryAfterMs;
|
|
145
|
+
constructor(message, retryAfterMs, details, cause) {
|
|
146
|
+
super(message, ErrorCode.RATE_LIMITED, 429, true, { ...details, retryAfterMs }, cause);
|
|
147
|
+
this.name = 'RateLimitError';
|
|
148
|
+
this.retryAfterMs = retryAfterMs;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export class ProxyError extends WebScrapeError {
|
|
152
|
+
constructor(message, details, cause) {
|
|
153
|
+
super(message, ErrorCode.PROXY_ERROR, 502, true, details, cause);
|
|
154
|
+
this.name = 'ProxyError';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export class ParseError extends WebScrapeError {
|
|
158
|
+
constructor(message, details, cause) {
|
|
159
|
+
super(message, ErrorCode.PARSE_ERROR, 422, false, details, cause);
|
|
160
|
+
this.name = 'ParseError';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export class AuthError extends WebScrapeError {
|
|
164
|
+
constructor(message, details, cause) {
|
|
165
|
+
super(message, ErrorCode.AUTH_REQUIRED, 401, false, details, cause);
|
|
166
|
+
this.name = 'AuthError';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export class NetworkError extends WebScrapeError {
|
|
170
|
+
constructor(message, details, cause) {
|
|
171
|
+
super(message, ErrorCode.NETWORK_ERROR, 503, true, details, cause);
|
|
172
|
+
this.name = 'NetworkError';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export class BrowserNotAvailableError extends WebScrapeError {
|
|
176
|
+
constructor(message = 'Playwright is not installed. Install it with: npm install playwright', details) {
|
|
177
|
+
super(message, ErrorCode.BROWSER_NOT_AVAILABLE, 500, false, details);
|
|
178
|
+
this.name = 'BrowserNotAvailableError';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
182
|
+
/* Section 14: scrape/utils.ts */
|
|
183
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
184
|
+
export function ensureDir(path) {
|
|
185
|
+
const dir = dirname(path);
|
|
186
|
+
if (!existsSync(dir)) {
|
|
187
|
+
mkdirSync(dir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function writeOutput(path, content) {
|
|
191
|
+
ensureDir(path);
|
|
192
|
+
writeFileSync(path, content, 'utf-8');
|
|
193
|
+
}
|
|
194
|
+
export function slugify(text) {
|
|
195
|
+
return text
|
|
196
|
+
.toLowerCase()
|
|
197
|
+
.replace(/[^\w\s-]/g, '')
|
|
198
|
+
.replace(/[\s_]+/g, '-')
|
|
199
|
+
.replace(/^-+|-+$/g, '');
|
|
200
|
+
}
|
|
201
|
+
export function truncate(text, max, suffix = '...') {
|
|
202
|
+
if (text.length <= max)
|
|
203
|
+
return text;
|
|
204
|
+
return text.slice(0, max - suffix.length) + suffix;
|
|
205
|
+
}
|
|
206
|
+
export function legacyestimatetokens(text) {
|
|
207
|
+
return Math.ceil(text.length / 4);
|
|
208
|
+
}
|
|
209
|
+
export function legacychunktext(text, chunkSize) {
|
|
210
|
+
const chunks = [];
|
|
211
|
+
let i = 0;
|
|
212
|
+
while (i < text.length) {
|
|
213
|
+
let end = i + chunkSize;
|
|
214
|
+
if (end < text.length) {
|
|
215
|
+
const breakIdx = text.lastIndexOf('\n', end);
|
|
216
|
+
if (breakIdx > i)
|
|
217
|
+
end = breakIdx + 1;
|
|
218
|
+
}
|
|
219
|
+
chunks.push(text.slice(i, end).trim());
|
|
220
|
+
i = end;
|
|
221
|
+
}
|
|
222
|
+
return chunks;
|
|
223
|
+
}
|
|
224
|
+
export function delay(ms) {
|
|
225
|
+
return new Promise(r => setTimeout(r, ms));
|
|
226
|
+
}
|
|
227
|
+
export function isValidUrl(url) {
|
|
228
|
+
try {
|
|
229
|
+
new URL(url);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export function normalizeUrl(url, base) {
|
|
237
|
+
try {
|
|
238
|
+
return new URL(url, base).href;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return url;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function isInternalUrl(href, base) {
|
|
245
|
+
try {
|
|
246
|
+
const baseUrl = new URL(base);
|
|
247
|
+
const target = new URL(href, base);
|
|
248
|
+
return target.hostname === baseUrl.hostname;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
255
|
+
/* Section 15: scrape/port.ts — Generate a random port number in the ephemeral port range (1024-65535). */
|
|
256
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
257
|
+
/**
|
|
258
|
+
* Generate a random port number in the ephemeral port range (1024-65535).
|
|
259
|
+
* The port is calculated once and then fixed for the lifetime of the process.
|
|
260
|
+
*/
|
|
261
|
+
let _cachedPort = null;
|
|
262
|
+
export function randomPort() {
|
|
263
|
+
if (_cachedPort !== null)
|
|
264
|
+
return _cachedPort;
|
|
265
|
+
_cachedPort = Math.floor(Math.random() * 64511) + 1024;
|
|
266
|
+
return _cachedPort;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Reset the cached port (for testing).
|
|
270
|
+
*/
|
|
271
|
+
export function resetPort() {
|
|
272
|
+
_cachedPort = null;
|
|
273
|
+
}
|
|
274
|
+
const MODEL_TOKEN_RATIOS = {
|
|
275
|
+
'gpt-4o': 3.5,
|
|
276
|
+
'gpt-4': 3.5,
|
|
277
|
+
'gpt-3.5-turbo': 4,
|
|
278
|
+
'claude': 3.2,
|
|
279
|
+
'gemini': 3.5,
|
|
280
|
+
'default': 4,
|
|
281
|
+
};
|
|
282
|
+
export function estimateTokens(text, model = 'default') {
|
|
283
|
+
const ratio = MODEL_TOKEN_RATIOS[model] || 4;
|
|
284
|
+
return Math.ceil(text.length / ratio);
|
|
285
|
+
}
|
|
286
|
+
export function countTokens(text, model = 'default') {
|
|
287
|
+
return estimateTokens(text, model);
|
|
288
|
+
}
|
|
289
|
+
export function fitsInContext(text, maxTokens, model = 'default') {
|
|
290
|
+
return estimateTokens(text, model) <= maxTokens;
|
|
291
|
+
}
|
|
292
|
+
export function truncateToTokens(text, maxTokens, model = 'default') {
|
|
293
|
+
const estimatedTokens = estimateTokens(text, model);
|
|
294
|
+
if (estimatedTokens <= maxTokens)
|
|
295
|
+
return text;
|
|
296
|
+
const charLimit = Math.floor(maxTokens * (MODEL_TOKEN_RATIOS[model] || 4));
|
|
297
|
+
return text.slice(0, charLimit) + '...';
|
|
298
|
+
}
|
|
299
|
+
export function tokenCost(text, model, pricePer1kTokens) {
|
|
300
|
+
const tokens = estimateTokens(text, model);
|
|
301
|
+
return (tokens / 1000) * pricePer1kTokens;
|
|
302
|
+
}
|
|
303
|
+
export const MODEL_PRICES = {
|
|
304
|
+
'gpt-4o': { input: 0.0025, output: 0.01 },
|
|
305
|
+
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
|
|
306
|
+
'gpt-4-turbo': { input: 0.01, output: 0.03 },
|
|
307
|
+
'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
|
|
308
|
+
'claude-3-haiku': { input: 0.00025, output: 0.00125 },
|
|
309
|
+
'gemini-1.5-pro': { input: 0.00125, output: 0.005 },
|
|
310
|
+
'gemini-1.5-flash': { input: 0.000075, output: 0.0003 },
|
|
311
|
+
};
|
|
312
|
+
const DEFAULT_CHUNK_OPTIONS = {
|
|
313
|
+
maxTokens: 512,
|
|
314
|
+
overlapTokens: 50,
|
|
315
|
+
preserveCodeBlocks: true,
|
|
316
|
+
includeHeadingPath: true,
|
|
317
|
+
};
|
|
318
|
+
function splitByHeaders(markdown) {
|
|
319
|
+
const sections = [];
|
|
320
|
+
const lines = markdown.split('\n');
|
|
321
|
+
let currentHeading = '';
|
|
322
|
+
let currentLevel = 0;
|
|
323
|
+
let currentContent = [];
|
|
324
|
+
for (const line of lines) {
|
|
325
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
326
|
+
if (headerMatch) {
|
|
327
|
+
if (currentContent.length > 0 || currentHeading) {
|
|
328
|
+
sections.push({
|
|
329
|
+
heading: currentHeading,
|
|
330
|
+
level: currentLevel,
|
|
331
|
+
content: currentContent.join('\n').trim(),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
currentLevel = headerMatch[1].length;
|
|
335
|
+
currentHeading = headerMatch[2];
|
|
336
|
+
currentContent = [];
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
currentContent.push(line);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (currentContent.length > 0 || currentHeading) {
|
|
343
|
+
sections.push({
|
|
344
|
+
heading: currentHeading,
|
|
345
|
+
level: currentLevel,
|
|
346
|
+
content: currentContent.join('\n').trim(),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return sections;
|
|
350
|
+
}
|
|
351
|
+
function splitByParagraphs(text) {
|
|
352
|
+
return text.split(/\n\n+/).filter(p => p.trim().length > 0);
|
|
353
|
+
}
|
|
354
|
+
function splitBySentences(text) {
|
|
355
|
+
return text.match(/[^.!?]+[.!?]+/g) || [text];
|
|
356
|
+
}
|
|
357
|
+
export function chunkMarkdown(markdown, options = {}) {
|
|
358
|
+
const opts = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
359
|
+
const chunks = [];
|
|
360
|
+
const sections = splitByHeaders(markdown);
|
|
361
|
+
const headingPath = [];
|
|
362
|
+
let headingDepth = 0;
|
|
363
|
+
for (const section of sections) {
|
|
364
|
+
while (headingDepth > 0 && headingDepth >= section.level) {
|
|
365
|
+
headingDepth--;
|
|
366
|
+
headingPath.pop();
|
|
367
|
+
}
|
|
368
|
+
if (section.heading) {
|
|
369
|
+
headingDepth = section.level;
|
|
370
|
+
headingPath.push(section.heading);
|
|
371
|
+
}
|
|
372
|
+
const sectionText = section.heading ? `#${'#'.repeat(section.level - 1)} ${section.heading}\n\n${section.content}` : section.content;
|
|
373
|
+
if (estimateTokens(sectionText) <= opts.maxTokens) {
|
|
374
|
+
chunks.push({
|
|
375
|
+
content: sectionText,
|
|
376
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
377
|
+
tokenCount: estimateTokens(sectionText),
|
|
378
|
+
index: chunks.length,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const paragraphs = splitByParagraphs(sectionText);
|
|
383
|
+
let currentChunk = '';
|
|
384
|
+
for (const paragraph of paragraphs) {
|
|
385
|
+
const testChunk = currentChunk ? `${currentChunk}\n\n${paragraph}` : paragraph;
|
|
386
|
+
if (estimateTokens(testChunk) <= opts.maxTokens) {
|
|
387
|
+
currentChunk = testChunk;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
if (currentChunk) {
|
|
391
|
+
chunks.push({
|
|
392
|
+
content: currentChunk,
|
|
393
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
394
|
+
tokenCount: estimateTokens(currentChunk),
|
|
395
|
+
index: chunks.length,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (estimateTokens(paragraph) > opts.maxTokens) {
|
|
399
|
+
const sentences = splitBySentences(paragraph);
|
|
400
|
+
let sentenceChunk = '';
|
|
401
|
+
for (const sentence of sentences) {
|
|
402
|
+
const testSentence = sentenceChunk ? `${sentenceChunk} ${sentence}` : sentence;
|
|
403
|
+
if (estimateTokens(testSentence) <= opts.maxTokens) {
|
|
404
|
+
sentenceChunk = testSentence;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
if (sentenceChunk) {
|
|
408
|
+
chunks.push({
|
|
409
|
+
content: sentenceChunk,
|
|
410
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
411
|
+
tokenCount: estimateTokens(sentenceChunk),
|
|
412
|
+
index: chunks.length,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
sentenceChunk = sentence;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
currentChunk = sentenceChunk;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
currentChunk = paragraph;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (currentChunk) {
|
|
426
|
+
chunks.push({
|
|
427
|
+
content: currentChunk,
|
|
428
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
429
|
+
tokenCount: estimateTokens(currentChunk),
|
|
430
|
+
index: chunks.length,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return chunks;
|
|
436
|
+
}
|
|
437
|
+
export function chunkText(text, options = {}) {
|
|
438
|
+
const opts = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
439
|
+
const chunks = [];
|
|
440
|
+
const paragraphs = splitByParagraphs(text);
|
|
441
|
+
let currentChunk = '';
|
|
442
|
+
for (const paragraph of paragraphs) {
|
|
443
|
+
const testChunk = currentChunk ? `${currentChunk}\n\n${paragraph}` : paragraph;
|
|
444
|
+
if (estimateTokens(testChunk) <= opts.maxTokens) {
|
|
445
|
+
currentChunk = testChunk;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
if (currentChunk) {
|
|
449
|
+
chunks.push({
|
|
450
|
+
content: currentChunk,
|
|
451
|
+
headingPath: [],
|
|
452
|
+
tokenCount: estimateTokens(currentChunk),
|
|
453
|
+
index: chunks.length,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
currentChunk = paragraph;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (currentChunk) {
|
|
460
|
+
chunks.push({
|
|
461
|
+
content: currentChunk,
|
|
462
|
+
headingPath: [],
|
|
463
|
+
tokenCount: estimateTokens(currentChunk),
|
|
464
|
+
index: chunks.length,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return chunks;
|
|
468
|
+
}
|
|
469
|
+
export function formatChunksForRAG(chunks) {
|
|
470
|
+
return chunks.map(chunk => {
|
|
471
|
+
const heading = chunk.headingPath.length > 0 ? `## ${chunk.headingPath.join(' > ')}\n\n` : '';
|
|
472
|
+
return `${heading}${chunk.content}`;
|
|
473
|
+
}).join('\n\n---\n\n');
|
|
474
|
+
}
|
|
475
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
476
|
+
/* Section 18: scrape/formats.ts */
|
|
477
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
478
|
+
const FORMAT_ALIASES = {
|
|
479
|
+
md: 'markdown',
|
|
480
|
+
txt: 'text',
|
|
481
|
+
plain: 'text',
|
|
482
|
+
xml: 'xml',
|
|
483
|
+
json: 'json',
|
|
484
|
+
redis: 'redis',
|
|
485
|
+
html: 'html',
|
|
486
|
+
h: 'html',
|
|
487
|
+
};
|
|
488
|
+
const FORMAT_EXTENSIONS = {
|
|
489
|
+
'.md': 'markdown',
|
|
490
|
+
'.markdown': 'markdown',
|
|
491
|
+
'.html': 'html',
|
|
492
|
+
'.htm': 'html',
|
|
493
|
+
'.txt': 'text',
|
|
494
|
+
'.json': 'json',
|
|
495
|
+
'.xml': 'xml',
|
|
496
|
+
'.redis': 'redis',
|
|
497
|
+
};
|
|
498
|
+
export function resolveFormat(format) {
|
|
499
|
+
const lower = format.toLowerCase().trim();
|
|
500
|
+
return FORMAT_ALIASES[lower] || lower;
|
|
501
|
+
}
|
|
502
|
+
export function formatFromExtension(ext) {
|
|
503
|
+
return FORMAT_EXTENSIONS[ext.toLowerCase()] ?? null;
|
|
504
|
+
}
|
|
505
|
+
export function extensionForFormat(format) {
|
|
506
|
+
const map = {
|
|
507
|
+
markdown: '.md',
|
|
508
|
+
html: '.html',
|
|
509
|
+
text: '.txt',
|
|
510
|
+
json: '.json',
|
|
511
|
+
xml: '.xml',
|
|
512
|
+
redis: '.redis',
|
|
513
|
+
};
|
|
514
|
+
return map[format] || '.md';
|
|
515
|
+
}
|
|
516
|
+
export function detectContentType(html) {
|
|
517
|
+
if (html.includes('<article') || html.includes('entry-content') || html.includes('class="post-content"') || html.includes('class="article-content"') || html.includes('role="main"')) {
|
|
518
|
+
return 'article';
|
|
519
|
+
}
|
|
520
|
+
const listCount = (html.match(/<li>/gi) || []).length;
|
|
521
|
+
if (listCount > 20)
|
|
522
|
+
return 'list';
|
|
523
|
+
if (html.includes('<article') || html.includes('entry-content'))
|
|
524
|
+
return 'article';
|
|
525
|
+
return 'page';
|
|
526
|
+
}
|
|
527
|
+
export function buildSerializeOptions(format, pretty = true) {
|
|
528
|
+
const targetMap = {
|
|
529
|
+
markdown: 'markdown',
|
|
530
|
+
html: 'markdown',
|
|
531
|
+
text: 'text',
|
|
532
|
+
json: 'json',
|
|
533
|
+
xml: 'xml',
|
|
534
|
+
redis: 'redis',
|
|
535
|
+
};
|
|
536
|
+
return {
|
|
537
|
+
format: targetMap[format] || 'markdown',
|
|
538
|
+
pretty,
|
|
539
|
+
includeMetadata: true,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
export async function convertResult(result, targetFormat) {
|
|
543
|
+
if (result.format === targetFormat)
|
|
544
|
+
return result;
|
|
545
|
+
const opts = buildSerializeOptions(targetFormat);
|
|
546
|
+
const serialized = sr(result, opts);
|
|
547
|
+
return {
|
|
548
|
+
...result,
|
|
549
|
+
content: serialized.content,
|
|
550
|
+
format: targetFormat,
|
|
551
|
+
size: serialized.size,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
555
|
+
/* Section 19: scrape/serialize.ts */
|
|
556
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
557
|
+
function htmlToMarkdown(html) {
|
|
558
|
+
const dom = new JSDOM(html);
|
|
559
|
+
const doc = dom.window.document;
|
|
560
|
+
let md = '';
|
|
561
|
+
function processNode(node, depth = 0) {
|
|
562
|
+
if (node.nodeType === 3) {
|
|
563
|
+
const text = node.textContent?.trim();
|
|
564
|
+
if (text)
|
|
565
|
+
md += text + ' ';
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (node.nodeType !== 1)
|
|
569
|
+
return;
|
|
570
|
+
const el = node;
|
|
571
|
+
const tag = el.tagName.toLowerCase();
|
|
572
|
+
switch (tag) {
|
|
573
|
+
case 'h1':
|
|
574
|
+
case 'h2':
|
|
575
|
+
case 'h3':
|
|
576
|
+
case 'h4':
|
|
577
|
+
case 'h5':
|
|
578
|
+
case 'h6': {
|
|
579
|
+
const level = parseInt(tag[1]);
|
|
580
|
+
md += '\n' + '#'.repeat(level) + ' ' + el.textContent?.trim() + '\n\n';
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
case 'p':
|
|
584
|
+
md += '\n' + el.textContent?.trim() + '\n\n';
|
|
585
|
+
break;
|
|
586
|
+
case 'br':
|
|
587
|
+
md += '\n';
|
|
588
|
+
break;
|
|
589
|
+
case 'hr':
|
|
590
|
+
md += '\n---\n\n';
|
|
591
|
+
break;
|
|
592
|
+
case 'ul':
|
|
593
|
+
case 'ol': {
|
|
594
|
+
el.childNodes.forEach(li => {
|
|
595
|
+
if (li.nodeType === 1) {
|
|
596
|
+
const liEl = li;
|
|
597
|
+
if (liEl.tagName.toLowerCase() === 'li') {
|
|
598
|
+
md += ' '.repeat(depth) + '- ' + liEl.textContent?.trim() + '\n';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
md += '\n';
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case 'a': {
|
|
606
|
+
const href = el.getAttribute('href') || '';
|
|
607
|
+
const text = el.textContent?.trim() || href;
|
|
608
|
+
if (href && href !== '#')
|
|
609
|
+
md += `[${text}](${href}) `;
|
|
610
|
+
else
|
|
611
|
+
md += text + ' ';
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
case 'img': {
|
|
615
|
+
const src = el.getAttribute('src') || '';
|
|
616
|
+
const alt = el.getAttribute('alt') || '';
|
|
617
|
+
if (src)
|
|
618
|
+
md += ` `;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case 'strong':
|
|
622
|
+
case 'b':
|
|
623
|
+
md += '**' + el.textContent?.trim() + '** ';
|
|
624
|
+
break;
|
|
625
|
+
case 'em':
|
|
626
|
+
case 'i':
|
|
627
|
+
md += '*' + el.textContent?.trim() + '* ';
|
|
628
|
+
break;
|
|
629
|
+
case 'code':
|
|
630
|
+
md += '`' + el.textContent?.trim() + '` ';
|
|
631
|
+
break;
|
|
632
|
+
case 'pre': {
|
|
633
|
+
const code = el.querySelector('code');
|
|
634
|
+
const lang = code?.getAttribute('class')?.replace(/^language-/, '') || '';
|
|
635
|
+
md += '\n```' + lang + '\n' + (code || el).textContent + '\n```\n\n';
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case 'blockquote':
|
|
639
|
+
md += '\n> ' + el.textContent?.trim().replace(/\n/g, '\n> ') + '\n\n';
|
|
640
|
+
break;
|
|
641
|
+
case 'table': {
|
|
642
|
+
const rows = el.querySelectorAll('tr');
|
|
643
|
+
rows.forEach((row, i) => {
|
|
644
|
+
const cells = row.querySelectorAll('td, th');
|
|
645
|
+
md += '| ' + Array.from(cells).map(c => c.textContent?.trim()).join(' | ') + ' |\n';
|
|
646
|
+
if (i === 0 && row.parentElement?.tagName === 'THEAD') {
|
|
647
|
+
md += '|' + Array.from(cells).map(() => ' --- ').join('|') + '|\n';
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
md += '\n';
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
default:
|
|
654
|
+
el.childNodes.forEach(child => processNode(child, depth));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
doc.body?.childNodes.forEach((child) => processNode(child));
|
|
658
|
+
return md.replace(/\n{3,}/g, '\n\n').trim();
|
|
659
|
+
}
|
|
660
|
+
function htmlToXml(html, root = 'document') {
|
|
661
|
+
const dom = new JSDOM(html);
|
|
662
|
+
const doc = dom.window.document;
|
|
663
|
+
function serialize(el, depth = 0) {
|
|
664
|
+
const indent = ' '.repeat(depth);
|
|
665
|
+
const tag = el.tagName.toLowerCase();
|
|
666
|
+
if (el.childNodes.length === 0) {
|
|
667
|
+
return `${indent}<${tag} />\n`;
|
|
668
|
+
}
|
|
669
|
+
const text = el.textContent?.trim();
|
|
670
|
+
if (el.childNodes.length === 1 && el.firstChild?.nodeType === 3 && text) {
|
|
671
|
+
return `${indent}<${tag}>${escapeXml(text)}</${tag}>\n`;
|
|
672
|
+
}
|
|
673
|
+
let xml = `${indent}<${tag}>\n`;
|
|
674
|
+
el.childNodes.forEach(child => {
|
|
675
|
+
if (child.nodeType === 1)
|
|
676
|
+
xml += serialize(child, depth + 1);
|
|
677
|
+
else if (child.nodeType === 3 && child.textContent?.trim()) {
|
|
678
|
+
xml += `${indent} ${escapeXml(child.textContent.trim())}\n`;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
xml += `${indent}</${tag}>\n`;
|
|
682
|
+
return xml;
|
|
683
|
+
}
|
|
684
|
+
function escapeXml(s) {
|
|
685
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
686
|
+
}
|
|
687
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${root}>\n`;
|
|
688
|
+
doc.body?.childNodes.forEach((child) => {
|
|
689
|
+
if (child.nodeType === 1)
|
|
690
|
+
xml += serialize(child, 1);
|
|
691
|
+
});
|
|
692
|
+
xml += `</${root}>\n`;
|
|
693
|
+
return xml;
|
|
694
|
+
}
|
|
695
|
+
function htmlToRedisJson(html, key = 'page') {
|
|
696
|
+
const dom = new JSDOM(html);
|
|
697
|
+
const doc = dom.window.document;
|
|
698
|
+
function extract(el) {
|
|
699
|
+
const tag = el.tagName.toLowerCase();
|
|
700
|
+
const children = Array.from(el.children);
|
|
701
|
+
if (children.length === 0)
|
|
702
|
+
return { _tag: tag, _text: el.textContent?.trim() || '' };
|
|
703
|
+
const result = { _tag: tag };
|
|
704
|
+
if (el.getAttribute('href'))
|
|
705
|
+
result.href = el.getAttribute('href');
|
|
706
|
+
if (el.getAttribute('src'))
|
|
707
|
+
result.src = el.getAttribute('src');
|
|
708
|
+
if (el.getAttribute('alt'))
|
|
709
|
+
result.alt = el.getAttribute('alt');
|
|
710
|
+
if (el.getAttribute('class'))
|
|
711
|
+
result.class = el.getAttribute('class');
|
|
712
|
+
children.forEach(child => {
|
|
713
|
+
const childTag = child.tagName.toLowerCase();
|
|
714
|
+
if (result[childTag]) {
|
|
715
|
+
if (!Array.isArray(result[childTag]))
|
|
716
|
+
result[childTag] = [result[childTag]];
|
|
717
|
+
result[childTag].push(extract(child));
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
result[childTag] = extract(child);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
const title = doc.title;
|
|
726
|
+
const bodyArr = [];
|
|
727
|
+
doc.body?.childNodes.forEach((child) => {
|
|
728
|
+
if (child.nodeType === 1)
|
|
729
|
+
bodyArr.push(extract(child));
|
|
730
|
+
});
|
|
731
|
+
const json = {
|
|
732
|
+
title,
|
|
733
|
+
body: bodyArr,
|
|
734
|
+
links: Array.from(doc.querySelectorAll('a[href]')).map((a) => ({
|
|
735
|
+
href: a.getAttribute('href'),
|
|
736
|
+
text: a.textContent?.trim(),
|
|
737
|
+
})),
|
|
738
|
+
images: Array.from(doc.querySelectorAll('img[src]')).map((img) => ({
|
|
739
|
+
src: img.getAttribute('src'),
|
|
740
|
+
alt: img.getAttribute('alt'),
|
|
741
|
+
})),
|
|
742
|
+
};
|
|
743
|
+
return `JSON.SET ${key} $ ${JSON.stringify(json, null, 2)}`;
|
|
744
|
+
}
|
|
745
|
+
/** Single-pass HTML entity decode map: the replace callback resolves each entity exactly once so a double-escaped `&lt;` decodes to the literal text `<` and never again to `<`. */
|
|
746
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" };
|
|
747
|
+
/** Linear single-pass script/style block removal: case-insensitive, tolerates `</script >` closing whitespace, and drops any unterminated block so no `<script` fragment can survive. One scan, no regex backtracking. */
|
|
748
|
+
function stripblocks(html, ...tags) {
|
|
749
|
+
const open = new RegExp(`<(${tags.join('|')})\\b[^<>]*>`, 'i');
|
|
750
|
+
let out = '';
|
|
751
|
+
let cursor = 0;
|
|
752
|
+
let match;
|
|
753
|
+
while ((match = open.exec(html.slice(cursor))) !== null) {
|
|
754
|
+
const start = cursor + match.index;
|
|
755
|
+
out += html.slice(cursor, start);
|
|
756
|
+
const closeTag = `</${match[1].toLowerCase()}`;
|
|
757
|
+
const close = html.toLowerCase().indexOf(closeTag, start + match[0].length);
|
|
758
|
+
if (close < 0)
|
|
759
|
+
return out; /* unterminated block: the rest is dropped */
|
|
760
|
+
const gt = html.indexOf('>', close);
|
|
761
|
+
cursor = gt < 0 ? html.length : gt + 1;
|
|
762
|
+
}
|
|
763
|
+
return out + html.slice(cursor);
|
|
764
|
+
}
|
|
765
|
+
/** Converts sanitized HTML to plain text: script/style blocks drop in one linear pass, remaining tags strip once and entities decode once. */
|
|
766
|
+
function plainText(html) {
|
|
767
|
+
return stripblocks(html, 'script', 'style')
|
|
768
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
769
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
770
|
+
.replace(/<\/h[1-6]>/gi, '\n')
|
|
771
|
+
.replace(/<\/li>/gi, '\n')
|
|
772
|
+
.replace(/<\/tr>/gi, '\n')
|
|
773
|
+
.replace(/<\/td>/gi, ' | ')
|
|
774
|
+
.replace(/<\/th>/gi, ' | ')
|
|
775
|
+
.replace(/<[^<>]+>/g, '')
|
|
776
|
+
.replace(/<\/?(?:script|style)\b[^<>]*$/i, '')
|
|
777
|
+
.replace(/ /g, ' ')
|
|
778
|
+
.replace(/&(amp|lt|gt|quot|#39);/g, (whole, entity) => ENTITIES[entity] ?? whole)
|
|
779
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
780
|
+
.trim();
|
|
781
|
+
}
|
|
782
|
+
function tableToMarkdown(headers, rows, caption) {
|
|
783
|
+
let md = caption ? `### ${caption}\n\n` : '';
|
|
784
|
+
md += '| ' + headers.join(' | ') + ' |\n';
|
|
785
|
+
md += '|' + headers.map(() => ' --- ').join('|') + '|\n';
|
|
786
|
+
for (const row of rows) {
|
|
787
|
+
md += '| ' + row.join(' | ') + ' |\n';
|
|
788
|
+
}
|
|
789
|
+
return md + '\n';
|
|
790
|
+
}
|
|
791
|
+
export function serializeResult(result, options) {
|
|
792
|
+
let content = '';
|
|
793
|
+
if (options.includeMetadata !== false) {
|
|
794
|
+
content += `# ${result.title}\n\n`;
|
|
795
|
+
content += `- **URL:** ${result.url}\n`;
|
|
796
|
+
content += `- **Extracted:** ${result.extractedAt}\n`;
|
|
797
|
+
if (result.metadata.author)
|
|
798
|
+
content += `- **Author:** ${result.metadata.author}\n`;
|
|
799
|
+
if (result.metadata.publishedDate)
|
|
800
|
+
content += `- **Published:** ${result.metadata.publishedDate}\n`;
|
|
801
|
+
content += `- **Size:** ${result.size} bytes\n\n---\n\n`;
|
|
802
|
+
}
|
|
803
|
+
switch (options.format) {
|
|
804
|
+
case 'markdown':
|
|
805
|
+
content += result.content;
|
|
806
|
+
if (result.tables.length) {
|
|
807
|
+
content += '\n\n## Tables\n\n';
|
|
808
|
+
for (const t of result.tables) {
|
|
809
|
+
content += tableToMarkdown(t.headers, t.rows, t.caption);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
case 'text':
|
|
814
|
+
content += result.text;
|
|
815
|
+
break;
|
|
816
|
+
case 'xml':
|
|
817
|
+
content = htmlToXml(`<html><head><title>${result.title}</title></head><body>${result.content}</body></html>`, options.xmlRoot || 'document');
|
|
818
|
+
break;
|
|
819
|
+
case 'json':
|
|
820
|
+
content = JSON.stringify({
|
|
821
|
+
title: result.title,
|
|
822
|
+
url: result.url,
|
|
823
|
+
metadata: result.metadata,
|
|
824
|
+
content: result.content,
|
|
825
|
+
links: result.links,
|
|
826
|
+
images: result.images,
|
|
827
|
+
tables: result.tables,
|
|
828
|
+
extractedAt: result.extractedAt,
|
|
829
|
+
duration: result.duration,
|
|
830
|
+
}, null, options.pretty ? 2 : undefined);
|
|
831
|
+
break;
|
|
832
|
+
case 'redis':
|
|
833
|
+
content = htmlToRedisJson(`<html><head><title>${result.title}</title></head><body>${result.content}</body></html>`, options.redisKey || `page:${encodeURIComponent(result.url)}`);
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
const chunks = options.maxChunkSize ? legacychunktext(content, options.maxChunkSize) : [];
|
|
837
|
+
const output = {
|
|
838
|
+
format: options.format,
|
|
839
|
+
content,
|
|
840
|
+
chunks: chunks.length ? chunks : undefined,
|
|
841
|
+
size: content.length,
|
|
842
|
+
};
|
|
843
|
+
if (options.includeMetadata) {
|
|
844
|
+
output.metadata = {
|
|
845
|
+
title: result.title,
|
|
846
|
+
url: result.url,
|
|
847
|
+
format: result.format,
|
|
848
|
+
extractedAt: result.extractedAt,
|
|
849
|
+
duration: result.duration,
|
|
850
|
+
linkCount: result.links.length,
|
|
851
|
+
imageCount: result.images.length,
|
|
852
|
+
tableCount: result.tables.length,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
return output;
|
|
856
|
+
}
|
|
857
|
+
export function serializeHtml(html, options) {
|
|
858
|
+
const content = (() => {
|
|
859
|
+
switch (options.format) {
|
|
860
|
+
case 'markdown': return htmlToMarkdown(html);
|
|
861
|
+
case 'xml': return htmlToXml(html, options.xmlRoot || 'document');
|
|
862
|
+
case 'json': return JSON.stringify({ html }, null, options.pretty ? 2 : undefined);
|
|
863
|
+
case 'redis': return htmlToRedisJson(html, options.redisKey || 'page:1');
|
|
864
|
+
case 'text': return plainText(html);
|
|
865
|
+
default: return plainText(html);
|
|
866
|
+
}
|
|
867
|
+
})();
|
|
868
|
+
const chunks = options.maxChunkSize ? legacychunktext(content, options.maxChunkSize) : [];
|
|
869
|
+
return {
|
|
870
|
+
format: options.format,
|
|
871
|
+
content,
|
|
872
|
+
chunks: chunks.length ? chunks : undefined,
|
|
873
|
+
size: content.length,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
const CHROME_PROFILES = [
|
|
877
|
+
{
|
|
878
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
879
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
880
|
+
acceptLanguage: 'en-US,en;q=0.9',
|
|
881
|
+
secChUa: '"Chromium";v="131", "Not_A Brand";v="24"',
|
|
882
|
+
secChUaPlatform: '"Windows"',
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
886
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
887
|
+
acceptLanguage: 'en-US,en;q=0.9',
|
|
888
|
+
secChUa: '"Chromium";v="131", "Not_A Brand";v="24"',
|
|
889
|
+
secChUaPlatform: '"macOS"',
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0',
|
|
893
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
|
894
|
+
acceptLanguage: 'en-US,en;q=0.5',
|
|
895
|
+
secChUa: '',
|
|
896
|
+
secChUaPlatform: '"Windows"',
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15',
|
|
900
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
901
|
+
acceptLanguage: 'en-US,en;q=0.9',
|
|
902
|
+
secChUa: '',
|
|
903
|
+
secChUaPlatform: '"macOS"',
|
|
904
|
+
},
|
|
905
|
+
];
|
|
906
|
+
export function getRandomProfile() {
|
|
907
|
+
return CHROME_PROFILES[Math.floor(Math.random() * CHROME_PROFILES.length)];
|
|
908
|
+
}
|
|
909
|
+
export function getHeaders(profile) {
|
|
910
|
+
const p = profile || getRandomProfile();
|
|
911
|
+
const headers = {
|
|
912
|
+
'User-Agent': p.userAgent,
|
|
913
|
+
'Accept': p.accept,
|
|
914
|
+
'Accept-Language': p.acceptLanguage,
|
|
915
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
916
|
+
'Connection': 'keep-alive',
|
|
917
|
+
'Upgrade-Insecure-Requests': '1',
|
|
918
|
+
'Sec-Fetch-Dest': 'document',
|
|
919
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
920
|
+
'Sec-Fetch-Site': 'none',
|
|
921
|
+
'Sec-Fetch-User': '?1',
|
|
922
|
+
};
|
|
923
|
+
if (p.secChUa) {
|
|
924
|
+
headers['sec-ch-ua'] = p.secChUa;
|
|
925
|
+
headers['sec-ch-ua-mobile'] = '?0';
|
|
926
|
+
headers['sec-ch-ua-platform'] = p.secChUaPlatform;
|
|
927
|
+
}
|
|
928
|
+
return headers;
|
|
929
|
+
}
|
|
930
|
+
export function mergeHeaders(base, override) {
|
|
931
|
+
return { ...base, ...override };
|
|
932
|
+
}
|
|
933
|
+
export class CookieJar {
|
|
934
|
+
cookies = new Map();
|
|
935
|
+
set(cookie) {
|
|
936
|
+
const key = `${cookie.domain || ''}:${cookie.path || '/'}:${cookie.name}`;
|
|
937
|
+
this.cookies.set(key, cookie);
|
|
938
|
+
}
|
|
939
|
+
get(name, domain, _path) {
|
|
940
|
+
for (const [key, cookie] of this.cookies) {
|
|
941
|
+
if (cookie.name !== name)
|
|
942
|
+
continue;
|
|
943
|
+
if (domain && cookie.domain && !domain.includes(cookie.domain))
|
|
944
|
+
continue;
|
|
945
|
+
if (cookie.expires && cookie.expires < Date.now()) {
|
|
946
|
+
this.cookies.delete(key);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
return cookie;
|
|
950
|
+
}
|
|
951
|
+
return undefined;
|
|
952
|
+
}
|
|
953
|
+
getAll(domain) {
|
|
954
|
+
const result = [];
|
|
955
|
+
const now = Date.now();
|
|
956
|
+
for (const [key, cookie] of this.cookies) {
|
|
957
|
+
if (cookie.expires && cookie.expires < now) {
|
|
958
|
+
this.cookies.delete(key);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (domain && cookie.domain && !domain.includes(cookie.domain))
|
|
962
|
+
continue;
|
|
963
|
+
result.push(cookie);
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
toString(domain) {
|
|
968
|
+
return this.getAll(domain)
|
|
969
|
+
.map(c => `${c.name}=${c.value}`)
|
|
970
|
+
.join('; ');
|
|
971
|
+
}
|
|
972
|
+
parseSetCookie(header, defaultDomain) {
|
|
973
|
+
const parts = header.split(';').map(s => s.trim());
|
|
974
|
+
const [nameValue, ...attrs] = parts;
|
|
975
|
+
const [name, ...valueParts] = nameValue.split('=');
|
|
976
|
+
const value = valueParts.join('=');
|
|
977
|
+
const cookie = {
|
|
978
|
+
name: name.trim(),
|
|
979
|
+
value: value.trim(),
|
|
980
|
+
domain: defaultDomain,
|
|
981
|
+
path: '/',
|
|
982
|
+
};
|
|
983
|
+
for (const attr of attrs) {
|
|
984
|
+
const [key, val] = attr.split('=').map(s => s.trim());
|
|
985
|
+
const lower = key.toLowerCase();
|
|
986
|
+
if (lower === 'domain')
|
|
987
|
+
cookie.domain = val;
|
|
988
|
+
else if (lower === 'path')
|
|
989
|
+
cookie.path = val;
|
|
990
|
+
else if (lower === 'expires')
|
|
991
|
+
cookie.expires = new Date(val).getTime();
|
|
992
|
+
else if (lower === 'httponly')
|
|
993
|
+
cookie.httpOnly = true;
|
|
994
|
+
else if (lower === 'secure')
|
|
995
|
+
cookie.secure = true;
|
|
996
|
+
}
|
|
997
|
+
this.set(cookie);
|
|
998
|
+
}
|
|
999
|
+
clear() {
|
|
1000
|
+
this.cookies.clear();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
export class ScrapingSession {
|
|
1004
|
+
cookieJar;
|
|
1005
|
+
id;
|
|
1006
|
+
data = new Map();
|
|
1007
|
+
constructor(id) {
|
|
1008
|
+
this.id = id || crypto.randomUUID();
|
|
1009
|
+
this.cookieJar = new CookieJar();
|
|
1010
|
+
}
|
|
1011
|
+
get(key) {
|
|
1012
|
+
return this.data.get(key);
|
|
1013
|
+
}
|
|
1014
|
+
set(key, value) {
|
|
1015
|
+
this.data.set(key, value);
|
|
1016
|
+
}
|
|
1017
|
+
clear() {
|
|
1018
|
+
this.cookieJar.clear();
|
|
1019
|
+
this.data.clear();
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
export function createSession(id) {
|
|
1023
|
+
return new ScrapingSession(id);
|
|
1024
|
+
}
|
|
1025
|
+
export class BrowserPool {
|
|
1026
|
+
pool = [];
|
|
1027
|
+
waitQueue = [];
|
|
1028
|
+
config;
|
|
1029
|
+
browserConfig;
|
|
1030
|
+
constructor(config = {}, browserConfig = {}) {
|
|
1031
|
+
this.config = {
|
|
1032
|
+
maxSize: config.maxSize || 3,
|
|
1033
|
+
acquireTimeoutMs: config.acquireTimeoutMs || 30000,
|
|
1034
|
+
};
|
|
1035
|
+
this.browserConfig = browserConfig;
|
|
1036
|
+
}
|
|
1037
|
+
async acquire() {
|
|
1038
|
+
const idle = this.pool.find(e => !e.inUse && e.browser.isConnected());
|
|
1039
|
+
if (idle) {
|
|
1040
|
+
idle.inUse = true;
|
|
1041
|
+
return idle.browser;
|
|
1042
|
+
}
|
|
1043
|
+
if (this.pool.length < this.config.maxSize) {
|
|
1044
|
+
const browser = new AgentBrowser(this.browserConfig);
|
|
1045
|
+
await browser.launch();
|
|
1046
|
+
const entry = { browser, inUse: true, createdAt: Date.now() };
|
|
1047
|
+
this.pool.push(entry);
|
|
1048
|
+
return browser;
|
|
1049
|
+
}
|
|
1050
|
+
return new Promise((resolve, reject) => {
|
|
1051
|
+
const timeout = setTimeout(() => {
|
|
1052
|
+
const idx = this.waitQueue.indexOf(waiter);
|
|
1053
|
+
if (idx !== -1)
|
|
1054
|
+
this.waitQueue.splice(idx, 1);
|
|
1055
|
+
reject(new Error('Timeout waiting for browser'));
|
|
1056
|
+
}, this.config.acquireTimeoutMs);
|
|
1057
|
+
const waiter = (browser) => {
|
|
1058
|
+
clearTimeout(timeout);
|
|
1059
|
+
resolve(browser);
|
|
1060
|
+
};
|
|
1061
|
+
this.waitQueue.push(waiter);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
release(browser) {
|
|
1065
|
+
const entry = this.pool.find(e => e.browser === browser);
|
|
1066
|
+
if (entry) {
|
|
1067
|
+
entry.inUse = false;
|
|
1068
|
+
if (this.waitQueue.length > 0) {
|
|
1069
|
+
const waiter = this.waitQueue.shift();
|
|
1070
|
+
entry.inUse = true;
|
|
1071
|
+
waiter(browser);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
async destroy() {
|
|
1076
|
+
for (const entry of this.pool) {
|
|
1077
|
+
await entry.browser.close().catch(() => { });
|
|
1078
|
+
}
|
|
1079
|
+
this.pool = [];
|
|
1080
|
+
for (const waiter of this.waitQueue) {
|
|
1081
|
+
waiter(new AgentBrowser());
|
|
1082
|
+
}
|
|
1083
|
+
this.waitQueue = [];
|
|
1084
|
+
}
|
|
1085
|
+
getStatus() {
|
|
1086
|
+
return {
|
|
1087
|
+
total: this.pool.length,
|
|
1088
|
+
idle: this.pool.filter(e => !e.inUse).length,
|
|
1089
|
+
inUse: this.pool.filter(e => e.inUse).length,
|
|
1090
|
+
waiting: this.waitQueue.length,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
export function createPool(config, browserConfig) {
|
|
1095
|
+
return new BrowserPool(config, browserConfig);
|
|
1096
|
+
}
|
|
1097
|
+
const DEFAULT_POOL_CONFIG = {
|
|
1098
|
+
maxFailures: 3,
|
|
1099
|
+
reviveAfterMs: 30 * 60 * 1000,
|
|
1100
|
+
strategy: 'round-robin',
|
|
1101
|
+
};
|
|
1102
|
+
export class ProxyPool {
|
|
1103
|
+
proxies;
|
|
1104
|
+
stats = new Map();
|
|
1105
|
+
currentIndex = 0;
|
|
1106
|
+
config;
|
|
1107
|
+
constructor(proxies, config = {}) {
|
|
1108
|
+
this.proxies = proxies;
|
|
1109
|
+
this.config = { ...DEFAULT_POOL_CONFIG, ...config };
|
|
1110
|
+
for (const p of proxies) {
|
|
1111
|
+
this.stats.set(p.url, { failures: 0, successes: 0, consecutiveFailures: 0, disabled: false });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
getActiveProxies() {
|
|
1115
|
+
const now = Date.now();
|
|
1116
|
+
return this.proxies.filter(p => {
|
|
1117
|
+
const s = this.stats.get(p.url);
|
|
1118
|
+
if (!s)
|
|
1119
|
+
return true;
|
|
1120
|
+
if (s.disabled && s.disabledAt && (now - s.disabledAt) >= this.config.reviveAfterMs) {
|
|
1121
|
+
s.disabled = false;
|
|
1122
|
+
s.failures = 0;
|
|
1123
|
+
s.consecutiveFailures = 0;
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
return !s.disabled;
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
next() {
|
|
1130
|
+
const active = this.getActiveProxies();
|
|
1131
|
+
if (active.length === 0)
|
|
1132
|
+
return null;
|
|
1133
|
+
if (this.config.strategy === 'random') {
|
|
1134
|
+
return active[Math.floor(Math.random() * active.length)];
|
|
1135
|
+
}
|
|
1136
|
+
const proxy = active[this.currentIndex % active.length];
|
|
1137
|
+
this.currentIndex = (this.currentIndex + 1) % active.length;
|
|
1138
|
+
return proxy;
|
|
1139
|
+
}
|
|
1140
|
+
reportSuccess(proxy) {
|
|
1141
|
+
const s = this.stats.get(proxy.url);
|
|
1142
|
+
if (s) {
|
|
1143
|
+
s.successes++;
|
|
1144
|
+
s.consecutiveFailures = 0;
|
|
1145
|
+
s.lastSuccess = Date.now();
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
reportFailure(proxy) {
|
|
1149
|
+
const s = this.stats.get(proxy.url);
|
|
1150
|
+
if (s) {
|
|
1151
|
+
s.failures++;
|
|
1152
|
+
s.consecutiveFailures++;
|
|
1153
|
+
s.lastFailure = Date.now();
|
|
1154
|
+
if (s.consecutiveFailures >= this.config.maxFailures) {
|
|
1155
|
+
s.disabled = true;
|
|
1156
|
+
s.disabledAt = Date.now();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
getStatus() {
|
|
1161
|
+
const active = this.getActiveProxies();
|
|
1162
|
+
return {
|
|
1163
|
+
total: this.proxies.length,
|
|
1164
|
+
active: active.length,
|
|
1165
|
+
disabled: this.proxies.length - active.length,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
export function createProxyPool(proxies, config) {
|
|
1170
|
+
return new ProxyPool(proxies, config);
|
|
1171
|
+
}
|
|
1172
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
1173
|
+
/* Section 24: scrape/proxypool.ts — proxy pool chooses healthy least used entries and never rotates blindly. */
|
|
1174
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
1175
|
+
/**
|
|
1176
|
+
* proxy pool chooses healthy least used entries and never rotates blindly.
|
|
1177
|
+
*/
|
|
1178
|
+
export function proxypool(options = {}) {
|
|
1179
|
+
const entries = (options.proxies ?? []).map((proxy) => ({ ...proxy, failures: 0, uses: 0, status: "active", lastused: 0 }));
|
|
1180
|
+
const threshold = options.failurethreshold ?? 3;
|
|
1181
|
+
const recovery = options.recoverytime ?? 300000;
|
|
1182
|
+
function revive() { const now = Date.now(); for (const entry of entries)
|
|
1183
|
+
if (entry.status === "graveyard" && now - entry.failedat >= recovery) {
|
|
1184
|
+
entry.status = "active";
|
|
1185
|
+
entry.failures = 0;
|
|
1186
|
+
} }
|
|
1187
|
+
function choose() { revive(); const available = entries.filter((entry) => entry.status === "active"); if (!available.length)
|
|
1188
|
+
throw new Error("proxy pool has no healthy entries"); const selected = [...available].sort((left, right) => left.uses - right.uses || left.lastused - right.lastused)[0]; selected.uses += 1; selected.lastused = Date.now(); return { ...selected }; }
|
|
1189
|
+
function report(id, result = {}) { const entry = entries.find((value) => value.id === id); if (!entry)
|
|
1190
|
+
return null; if (result.ok) {
|
|
1191
|
+
entry.failures = 0;
|
|
1192
|
+
entry.status = "active";
|
|
1193
|
+
}
|
|
1194
|
+
else {
|
|
1195
|
+
entry.failures += 1;
|
|
1196
|
+
entry.status = entry.failures >= threshold ? "graveyard" : "active";
|
|
1197
|
+
entry.failedat = Date.now();
|
|
1198
|
+
} return { ...entry }; }
|
|
1199
|
+
return { choose, report, list() { revive(); return entries.map((entry) => ({ ...entry })); } };
|
|
1200
|
+
}
|
|
1201
|
+
const DEFAULT_CONFIG = {
|
|
1202
|
+
maxTokensPerInterval: 10,
|
|
1203
|
+
intervalMs: 1000,
|
|
1204
|
+
maxConcurrent: 5,
|
|
1205
|
+
};
|
|
1206
|
+
export class RateLimiter {
|
|
1207
|
+
buckets = new Map();
|
|
1208
|
+
active = new Map();
|
|
1209
|
+
config;
|
|
1210
|
+
constructor(config = {}) {
|
|
1211
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1212
|
+
}
|
|
1213
|
+
getBucket(key) {
|
|
1214
|
+
if (!this.buckets.has(key)) {
|
|
1215
|
+
this.buckets.set(key, {
|
|
1216
|
+
tokens: this.config.maxTokensPerInterval,
|
|
1217
|
+
lastRefill: Date.now(),
|
|
1218
|
+
maxTokens: this.config.maxTokensPerInterval,
|
|
1219
|
+
refillRate: this.config.maxTokensPerInterval / (this.config.intervalMs / 1000),
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
return this.buckets.get(key);
|
|
1223
|
+
}
|
|
1224
|
+
refill(bucket) {
|
|
1225
|
+
const now = Date.now();
|
|
1226
|
+
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
1227
|
+
bucket.tokens = Math.min(bucket.maxTokens, bucket.tokens + elapsed * bucket.refillRate);
|
|
1228
|
+
bucket.lastRefill = now;
|
|
1229
|
+
}
|
|
1230
|
+
async acquire(key) {
|
|
1231
|
+
const bucket = this.getBucket(key);
|
|
1232
|
+
const currentActive = this.active.get(key) || 0;
|
|
1233
|
+
if (currentActive >= this.config.maxConcurrent) {
|
|
1234
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1235
|
+
return this.acquire(key);
|
|
1236
|
+
}
|
|
1237
|
+
this.refill(bucket);
|
|
1238
|
+
if (bucket.tokens < 1) {
|
|
1239
|
+
const waitMs = ((1 - bucket.tokens) / bucket.refillRate) * 1000;
|
|
1240
|
+
await new Promise((resolve) => setTimeout(resolve, Math.ceil(waitMs)));
|
|
1241
|
+
this.refill(bucket);
|
|
1242
|
+
}
|
|
1243
|
+
bucket.tokens -= 1;
|
|
1244
|
+
this.active.set(key, currentActive + 1);
|
|
1245
|
+
}
|
|
1246
|
+
release(key) {
|
|
1247
|
+
const current = this.active.get(key) || 0;
|
|
1248
|
+
if (current <= 1) {
|
|
1249
|
+
this.active.delete(key);
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
this.active.set(key, current - 1);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
getStats(key) {
|
|
1256
|
+
const bucket = this.buckets.get(key);
|
|
1257
|
+
if (bucket)
|
|
1258
|
+
this.refill(bucket);
|
|
1259
|
+
return {
|
|
1260
|
+
tokens: bucket?.tokens ?? this.config.maxTokensPerInterval,
|
|
1261
|
+
active: this.active.get(key) || 0,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
clear() {
|
|
1265
|
+
this.buckets.clear();
|
|
1266
|
+
this.active.clear();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
export function createRateLimiter(config) {
|
|
1270
|
+
return new RateLimiter(config);
|
|
1271
|
+
}
|
|
1272
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
1273
|
+
/* Section 26: scrape/retry.ts */
|
|
1274
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
1275
|
+
export const DEFAULT_RETRY_CONFIG = {
|
|
1276
|
+
retries: 3,
|
|
1277
|
+
delay: 1000,
|
|
1278
|
+
backoff: "exponential",
|
|
1279
|
+
maxDelay: 30000,
|
|
1280
|
+
statusCodes: [408, 429, 500, 502, 503, 504],
|
|
1281
|
+
};
|
|
1282
|
+
const RETRYABLE_NETWORK_CODES = ["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "EPIPE", "ENOTFOUND"];
|
|
1283
|
+
export function isRetryableError(error) {
|
|
1284
|
+
const code = error.code;
|
|
1285
|
+
if (code && RETRYABLE_NETWORK_CODES.includes(code))
|
|
1286
|
+
return true;
|
|
1287
|
+
const status = error.statusCode || error.status;
|
|
1288
|
+
if (status && [408, 429, 500, 502, 503, 504].includes(status))
|
|
1289
|
+
return true;
|
|
1290
|
+
if (error.name === "AbortError")
|
|
1291
|
+
return false;
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
function calculateDelay(attempt, config) {
|
|
1295
|
+
switch (config.backoff) {
|
|
1296
|
+
case "exponential":
|
|
1297
|
+
return Math.min(config.delay * 2 ** attempt, config.maxDelay);
|
|
1298
|
+
case "linear":
|
|
1299
|
+
return Math.min(config.delay * (attempt + 1), config.maxDelay);
|
|
1300
|
+
case "constant":
|
|
1301
|
+
return config.delay;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
export async function withRetry(fn, config = {}, onRetry) {
|
|
1305
|
+
const cfg = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
1306
|
+
const { default: pRetry } = await import("p-retry");
|
|
1307
|
+
return pRetry(async (attemptNumber) => {
|
|
1308
|
+
try {
|
|
1309
|
+
return await fn(attemptNumber);
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
if (!isRetryableError(error)) {
|
|
1313
|
+
throw new AbortError(error.message);
|
|
1314
|
+
}
|
|
1315
|
+
throw error;
|
|
1316
|
+
}
|
|
1317
|
+
}, {
|
|
1318
|
+
retries: cfg.retries,
|
|
1319
|
+
minTimeout: calculateDelay(0, cfg),
|
|
1320
|
+
maxTimeout: cfg.maxDelay,
|
|
1321
|
+
randomize: cfg.backoff === "exponential",
|
|
1322
|
+
onFailedAttempt: (context) => {
|
|
1323
|
+
onRetry?.(context.error, context.attemptNumber);
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Loads the p-retry AbortError class lazily (the class was a value re-export
|
|
1329
|
+
* of the former scrape/retry.ts module; the grand merge keeps it reachable
|
|
1330
|
+
* without requiring p-retry at module load time).
|
|
1331
|
+
*/
|
|
1332
|
+
export async function loadaborterror() {
|
|
1333
|
+
const { AbortError } = await import("p-retry");
|
|
1334
|
+
return AbortError;
|
|
1335
|
+
}
|
|
1336
|
+
export class MiddlewarePipeline {
|
|
1337
|
+
middlewares = [];
|
|
1338
|
+
use(middleware) {
|
|
1339
|
+
this.middlewares.push(middleware);
|
|
1340
|
+
return this;
|
|
1341
|
+
}
|
|
1342
|
+
async execute(initialData) {
|
|
1343
|
+
const ctx = {
|
|
1344
|
+
data: initialData,
|
|
1345
|
+
state: {},
|
|
1346
|
+
startTime: Date.now(),
|
|
1347
|
+
aborted: false,
|
|
1348
|
+
abort() { this.aborted = true; },
|
|
1349
|
+
};
|
|
1350
|
+
let index = 0;
|
|
1351
|
+
const next = async () => {
|
|
1352
|
+
if (ctx.aborted)
|
|
1353
|
+
return;
|
|
1354
|
+
if (index < this.middlewares.length) {
|
|
1355
|
+
const middleware = this.middlewares[index++];
|
|
1356
|
+
await middleware(ctx, next);
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
await next();
|
|
1360
|
+
return ctx;
|
|
1361
|
+
}
|
|
1362
|
+
clear() {
|
|
1363
|
+
this.middlewares = [];
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
export function loggingMiddleware(logger) {
|
|
1367
|
+
return async (_ctx, next) => {
|
|
1368
|
+
const start = Date.now();
|
|
1369
|
+
await next();
|
|
1370
|
+
const duration = Date.now() - start;
|
|
1371
|
+
logger?.info(`Request completed in ${duration}ms`);
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
export function timeoutMiddleware(timeoutMs) {
|
|
1375
|
+
return async (ctx, next) => {
|
|
1376
|
+
const timer = setTimeout(() => ctx.abort(), timeoutMs);
|
|
1377
|
+
try {
|
|
1378
|
+
await next();
|
|
1379
|
+
}
|
|
1380
|
+
finally {
|
|
1381
|
+
clearTimeout(timer);
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
export function retryMiddleware(maxRetries, delayMs) {
|
|
1386
|
+
return async (ctx, next) => {
|
|
1387
|
+
let attempt = 0;
|
|
1388
|
+
while (attempt <= maxRetries) {
|
|
1389
|
+
ctx.state.attempt = attempt;
|
|
1390
|
+
try {
|
|
1391
|
+
await next();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
if (attempt === maxRetries)
|
|
1396
|
+
throw error;
|
|
1397
|
+
await new Promise(r => setTimeout(r, delayMs * Math.pow(2, attempt)));
|
|
1398
|
+
attempt++;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
export function createPipeline() {
|
|
1404
|
+
return new MiddlewarePipeline();
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Creates a fresh emittery event bus. Emittery joined the lazy-loading
|
|
1408
|
+
* contract of the grand merge: the optional runtime dependency is imported
|
|
1409
|
+
* on first use, so loading this module never requires it to be installed.
|
|
1410
|
+
*/
|
|
1411
|
+
export async function createEventEmitter() {
|
|
1412
|
+
const { default: EmitteryCtor } = await import("emittery");
|
|
1413
|
+
return new EmitteryCtor();
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Resolves the process-wide emitter on first call. The former eager
|
|
1417
|
+
* `globalEmitter` value of scrape/events.ts became this lazy memoized
|
|
1418
|
+
* factory in the grand merge: emittery is an optional runtime and nothing
|
|
1419
|
+
* may request it as an import side effect of loading the module.
|
|
1420
|
+
*/
|
|
1421
|
+
let globalemitterpromise = null;
|
|
1422
|
+
export function globalEmitter() {
|
|
1423
|
+
globalemitterpromise ??= createEventEmitter();
|
|
1424
|
+
return globalemitterpromise;
|
|
1425
|
+
}
|
|
1426
|
+
const DEFAULT_PYGAME_CONFIG = {
|
|
1427
|
+
pythonPath: 'python',
|
|
1428
|
+
width: 1280,
|
|
1429
|
+
height: 720,
|
|
1430
|
+
fps: 30,
|
|
1431
|
+
fullscreen: false,
|
|
1432
|
+
};
|
|
1433
|
+
function generatePygameScript(imagePath, config, interactive = false) {
|
|
1434
|
+
const lines = [
|
|
1435
|
+
'import sys, json',
|
|
1436
|
+
'import pygame',
|
|
1437
|
+
'pygame.init()',
|
|
1438
|
+
`screen = pygame.display.set_mode((${config.width}, ${config.height})${config.fullscreen ? ', pygame.FULLSCREEN' : ''})`,
|
|
1439
|
+
'pygame.display.set_caption("DevThink WebScrape Renderer")',
|
|
1440
|
+
`clock = pygame.time.Clock()`,
|
|
1441
|
+
'',
|
|
1442
|
+
'try:',
|
|
1443
|
+
` img = pygame.image.load(${JSON.stringify(imagePath)})`,
|
|
1444
|
+
' img_rect = img.get_rect(center=(screen.get_width()//2, screen.get_height()//2))',
|
|
1445
|
+
'except:',
|
|
1446
|
+
' img = None',
|
|
1447
|
+
'',
|
|
1448
|
+
'font = pygame.font.Font(None, 36)',
|
|
1449
|
+
'',
|
|
1450
|
+
'running = True',
|
|
1451
|
+
'while running:',
|
|
1452
|
+
' for event in pygame.event.get():',
|
|
1453
|
+
' if event.type == pygame.QUIT:',
|
|
1454
|
+
' running = False',
|
|
1455
|
+
' elif event.type == pygame.KEYDOWN:',
|
|
1456
|
+
' if event.key == pygame.K_ESCAPE:',
|
|
1457
|
+
' running = False',
|
|
1458
|
+
...(interactive ? [
|
|
1459
|
+
' elif event.key == pygame.K_SPACE:',
|
|
1460
|
+
' print(json.dumps({"action": "next"}))',
|
|
1461
|
+
' sys.stdout.flush()',
|
|
1462
|
+
] : []),
|
|
1463
|
+
'',
|
|
1464
|
+
' screen.fill((30, 30, 40))',
|
|
1465
|
+
' if img:',
|
|
1466
|
+
' screen.blit(img, img_rect)',
|
|
1467
|
+
' else:',
|
|
1468
|
+
' text = font.render("No screenshot available", True, (200, 200, 200))',
|
|
1469
|
+
' screen.blit(text, (config.width//2 - text.get_width()//2, config.height//2))',
|
|
1470
|
+
'',
|
|
1471
|
+
' pygame.display.flip()',
|
|
1472
|
+
' clock.tick(30)',
|
|
1473
|
+
'',
|
|
1474
|
+
'pygame.quit()',
|
|
1475
|
+
];
|
|
1476
|
+
return lines.join('\n');
|
|
1477
|
+
}
|
|
1478
|
+
function generateDoomScript(config) {
|
|
1479
|
+
const lines = [
|
|
1480
|
+
'import sys, json, asyncio, subprocess',
|
|
1481
|
+
'import pygame',
|
|
1482
|
+
'pygame.init()',
|
|
1483
|
+
`screen = pygame.display.set_mode((${config.width}, ${config.height})${config.fullscreen ? ', pygame.FULLSCREEN' : ''})`,
|
|
1484
|
+
'pygame.display.set_caption("DevThink WebScrape - DOOM")',
|
|
1485
|
+
'',
|
|
1486
|
+
'font = pygame.font.Font(None, 24)',
|
|
1487
|
+
'small_font = pygame.font.Font(None, 18)',
|
|
1488
|
+
'',
|
|
1489
|
+
'frames = []',
|
|
1490
|
+
'current_frame = 0',
|
|
1491
|
+
'running = True',
|
|
1492
|
+
'',
|
|
1493
|
+
'def load_frames(path):',
|
|
1494
|
+
' import os',
|
|
1495
|
+
' frames_dir = os.path.expanduser(path)',
|
|
1496
|
+
' if os.path.isdir(frames_dir):',
|
|
1497
|
+
' files = sorted(os.listdir(frames_dir))',
|
|
1498
|
+
' for f in files:',
|
|
1499
|
+
' if f.endswith((".png", ".jpg", ".jpeg")):',
|
|
1500
|
+
' try:',
|
|
1501
|
+
' img = pygame.image.load(os.path.join(frames_dir, f))',
|
|
1502
|
+
' frames.append(img)',
|
|
1503
|
+
' except: pass',
|
|
1504
|
+
'',
|
|
1505
|
+
'load_frames("~/devthink/webscrape/doom_frames")',
|
|
1506
|
+
'',
|
|
1507
|
+
'while running:',
|
|
1508
|
+
' for event in pygame.event.get():',
|
|
1509
|
+
' if event.type == pygame.QUIT:',
|
|
1510
|
+
' running = False',
|
|
1511
|
+
' elif event.type == pygame.KEYDOWN:',
|
|
1512
|
+
' if event.key == pygame.K_ESCAPE: running = False',
|
|
1513
|
+
' elif event.key == pygame.K_LEFT: current_frame = max(0, current_frame - 1)',
|
|
1514
|
+
' elif event.key == pygame.K_RIGHT: current_frame = min(len(frames)-1, current_frame + 1)',
|
|
1515
|
+
' elif event.key == pygame.K_SPACE:',
|
|
1516
|
+
' out = {"action": "frame", "index": current_frame, "total": len(frames)}',
|
|
1517
|
+
' print(json.dumps(out))',
|
|
1518
|
+
' sys.stdout.flush()',
|
|
1519
|
+
'',
|
|
1520
|
+
' screen.fill((20, 20, 30))',
|
|
1521
|
+
' if frames and current_frame < len(frames):',
|
|
1522
|
+
' img = pygame.transform.scale(frames[current_frame], (config.width, config.height - 60))',
|
|
1523
|
+
' screen.blit(img, (0, 0))',
|
|
1524
|
+
' else:',
|
|
1525
|
+
' text = font.render("No DOOM frames loaded", True, (200, 80, 80))',
|
|
1526
|
+
' screen.blit(text, (config.width//2 - 150, config.height//2))',
|
|
1527
|
+
'',
|
|
1528
|
+
' info = f"Frame {current_frame+1}/{len(frames)} | ESC: quit | <- ->: navigate | SPACE: capture"',
|
|
1529
|
+
' info_surf = small_font.render(info, True, (180, 180, 180))',
|
|
1530
|
+
' screen.blit(info_surf, (10, config.height - 40))',
|
|
1531
|
+
'',
|
|
1532
|
+
' pygame.display.flip()',
|
|
1533
|
+
' clock.tick(30)',
|
|
1534
|
+
'',
|
|
1535
|
+
'pygame.quit()',
|
|
1536
|
+
];
|
|
1537
|
+
return lines.join('\n');
|
|
1538
|
+
}
|
|
1539
|
+
export class PygameRenderer {
|
|
1540
|
+
config;
|
|
1541
|
+
process = null;
|
|
1542
|
+
tempDir;
|
|
1543
|
+
screenDir;
|
|
1544
|
+
constructor(config = {}) {
|
|
1545
|
+
this.config = { ...DEFAULT_PYGAME_CONFIG, ...config };
|
|
1546
|
+
this.tempDir = join(process.cwd(), '.webscrape');
|
|
1547
|
+
this.screenDir = join(this.tempDir, 'screenshots');
|
|
1548
|
+
if (!existsSync(this.tempDir))
|
|
1549
|
+
mkdirSync(this.tempDir, { recursive: true });
|
|
1550
|
+
if (!existsSync(this.screenDir))
|
|
1551
|
+
mkdirSync(this.screenDir, { recursive: true });
|
|
1552
|
+
}
|
|
1553
|
+
async renderScreenshot(imageBuffer) {
|
|
1554
|
+
const imagePath = join(this.screenDir, 'current.png');
|
|
1555
|
+
writeFileSync(imagePath, imageBuffer);
|
|
1556
|
+
const script = generatePygameScript(imagePath, this.config);
|
|
1557
|
+
const scriptPath = join(this.tempDir, 'render_screenshot.py');
|
|
1558
|
+
writeFileSync(scriptPath, script);
|
|
1559
|
+
await this.runScript(scriptPath);
|
|
1560
|
+
}
|
|
1561
|
+
async renderDoom() {
|
|
1562
|
+
const script = generateDoomScript(this.config);
|
|
1563
|
+
const scriptPath = join(this.tempDir, 'render_doom.py');
|
|
1564
|
+
writeFileSync(scriptPath, script);
|
|
1565
|
+
await this.runScript(scriptPath);
|
|
1566
|
+
}
|
|
1567
|
+
runScript(scriptPath) {
|
|
1568
|
+
return new Promise((resolve, reject) => {
|
|
1569
|
+
this.process = spawn(this.config.pythonPath, [scriptPath], {
|
|
1570
|
+
stdio: ['inherit', 'inherit', 'pipe'],
|
|
1571
|
+
});
|
|
1572
|
+
this.process.on('close', code => {
|
|
1573
|
+
this.process = null;
|
|
1574
|
+
if (code === 0)
|
|
1575
|
+
resolve();
|
|
1576
|
+
else
|
|
1577
|
+
reject(new Error(`Pygame exited with code ${code}`));
|
|
1578
|
+
});
|
|
1579
|
+
this.process.on('error', err => reject(err));
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
close() {
|
|
1583
|
+
if (this.process && !this.process.killed) {
|
|
1584
|
+
this.process.kill();
|
|
1585
|
+
this.process = null;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
export function createRenderer(config) {
|
|
1590
|
+
return new PygameRenderer(config);
|
|
1591
|
+
}
|
|
1592
|
+
function parseSitemapXml(xml) {
|
|
1593
|
+
const urls = [];
|
|
1594
|
+
const urlMatches = xml.match(/<url[^>]*>([\s\S]*?)<\/url>/gi) || [];
|
|
1595
|
+
for (const urlBlock of urlMatches) {
|
|
1596
|
+
const loc = urlBlock.match(/<loc>([\s\S]*?)<\/loc>/i)?.[1]?.trim();
|
|
1597
|
+
if (!loc)
|
|
1598
|
+
continue;
|
|
1599
|
+
const lastmod = urlBlock.match(/<lastmod>([\s\S]*?)<\/lastmod>/i)?.[1]?.trim();
|
|
1600
|
+
const changefreq = urlBlock.match(/<changefreq>([\s\S]*?)<\/changefreq>/i)?.[1]?.trim();
|
|
1601
|
+
const priorityStr = urlBlock.match(/<priority>([\s\S]*?)<\/priority>/i)?.[1]?.trim();
|
|
1602
|
+
const priority = priorityStr ? parseFloat(priorityStr) : undefined;
|
|
1603
|
+
urls.push({ loc, lastmod, changefreq, priority });
|
|
1604
|
+
}
|
|
1605
|
+
return urls;
|
|
1606
|
+
}
|
|
1607
|
+
function extractSitemapUrls(xml) {
|
|
1608
|
+
const urls = [];
|
|
1609
|
+
const sitemapMatches = xml.match(/<sitemap[^>]*>[\s\S]*?<loc>([\s\S]*?)<\/loc>[\s\S]*?<\/sitemap>/gi) || [];
|
|
1610
|
+
for (const match of sitemapMatches) {
|
|
1611
|
+
const loc = match.match(/<loc>([\s\S]*?)<\/loc>/i)?.[1]?.trim();
|
|
1612
|
+
if (loc)
|
|
1613
|
+
urls.push(loc);
|
|
1614
|
+
}
|
|
1615
|
+
return urls;
|
|
1616
|
+
}
|
|
1617
|
+
export async function fetchSitemap(url, options = {}) {
|
|
1618
|
+
const startTime = Date.now();
|
|
1619
|
+
const fetcher = options.fetcher ?? fetch;
|
|
1620
|
+
try {
|
|
1621
|
+
const response = await fetcher(url, {
|
|
1622
|
+
headers: {
|
|
1623
|
+
'User-Agent': 'DevThink-WebScrape/2.0 (sitemap parser)',
|
|
1624
|
+
...options.headers,
|
|
1625
|
+
},
|
|
1626
|
+
signal: AbortSignal.timeout(options.timeout || 10000),
|
|
1627
|
+
});
|
|
1628
|
+
if (!response.ok) {
|
|
1629
|
+
throw new WebScrapeError(`Failed to fetch sitemap: ${response.status}`, ErrorCode.NETWORK_ERROR, response.status, false, { url });
|
|
1630
|
+
}
|
|
1631
|
+
const text = await response.text();
|
|
1632
|
+
const isIndex = text.includes('<sitemapindex');
|
|
1633
|
+
const urls = isIndex ? [] : parseSitemapXml(text);
|
|
1634
|
+
const sitemaps = isIndex ? extractSitemapUrls(text) : [];
|
|
1635
|
+
return {
|
|
1636
|
+
urls,
|
|
1637
|
+
sitemaps,
|
|
1638
|
+
duration: Date.now() - startTime,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
catch (error) {
|
|
1642
|
+
if (error instanceof WebScrapeError)
|
|
1643
|
+
throw error;
|
|
1644
|
+
throw new WebScrapeError(`Failed to parse sitemap: ${error.message}`, ErrorCode.PARSE_ERROR, 422, false, { url }, error);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
export async function discoverSitemaps(siteUrl) {
|
|
1648
|
+
const candidates = [
|
|
1649
|
+
'/sitemap.xml',
|
|
1650
|
+
'/sitemap_index.xml',
|
|
1651
|
+
'/sitemap-index.xml',
|
|
1652
|
+
'/sitemaps.xml',
|
|
1653
|
+
'/robots.txt',
|
|
1654
|
+
];
|
|
1655
|
+
const found = [];
|
|
1656
|
+
const base = siteUrl.replace(/\/$/, '');
|
|
1657
|
+
for (const path of candidates) {
|
|
1658
|
+
try {
|
|
1659
|
+
const url = path === '/robots.txt' ? `${base}/robots.txt` : `${base}${path}`;
|
|
1660
|
+
const response = await fetch(url, {
|
|
1661
|
+
headers: { 'User-Agent': 'DevThink-WebScrape/2.0' },
|
|
1662
|
+
signal: AbortSignal.timeout(5000),
|
|
1663
|
+
});
|
|
1664
|
+
if (response.ok) {
|
|
1665
|
+
const text = await response.text();
|
|
1666
|
+
if (path === '/robots.txt') {
|
|
1667
|
+
const sitemapUrls = text.match(/Sitemap:\s*(https?:\/\/[^\s]+)/gi);
|
|
1668
|
+
if (sitemapUrls) {
|
|
1669
|
+
for (const s of sitemapUrls) {
|
|
1670
|
+
found.push(s.replace(/Sitemap:\s*/i, '').trim());
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
found.push(url);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
catch { }
|
|
1680
|
+
}
|
|
1681
|
+
return [...new Set(found)];
|
|
1682
|
+
}
|
|
1683
|
+
export async function parseSitemap(url, options = {}) {
|
|
1684
|
+
const maxUrls = Math.max(0, Number(options.maxUrls ?? 10000));
|
|
1685
|
+
const maxDepth = Math.max(0, Number(options.maxDepth ?? 8));
|
|
1686
|
+
const allUrls = [];
|
|
1687
|
+
const seenSitemaps = new Set();
|
|
1688
|
+
const seenUrls = new Set();
|
|
1689
|
+
async function visit(current, depth) {
|
|
1690
|
+
const identity = sitemapidentity(current);
|
|
1691
|
+
if (seenSitemaps.has(identity) || depth > maxDepth || allUrls.length >= maxUrls)
|
|
1692
|
+
return;
|
|
1693
|
+
seenSitemaps.add(identity);
|
|
1694
|
+
const result = await fetchSitemap(current, options);
|
|
1695
|
+
for (const item of result.urls) {
|
|
1696
|
+
const itemidentity = sitemapidentity(item.loc);
|
|
1697
|
+
if (!seenUrls.has(itemidentity)) {
|
|
1698
|
+
seenUrls.add(itemidentity);
|
|
1699
|
+
allUrls.push(item);
|
|
1700
|
+
if (allUrls.length >= maxUrls)
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (options.followIndexes === false || depth >= maxDepth)
|
|
1705
|
+
return;
|
|
1706
|
+
for (const child of result.sitemaps) {
|
|
1707
|
+
if (allUrls.length >= maxUrls)
|
|
1708
|
+
return;
|
|
1709
|
+
await visit(child, depth + 1);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
await visit(url, 0);
|
|
1713
|
+
return allUrls;
|
|
1714
|
+
}
|
|
1715
|
+
function sitemapidentity(value) { try {
|
|
1716
|
+
const parsed = new URL(value);
|
|
1717
|
+
parsed.hash = "";
|
|
1718
|
+
return parsed.href;
|
|
1719
|
+
}
|
|
1720
|
+
catch {
|
|
1721
|
+
return String(value).trim();
|
|
1722
|
+
} }
|
|
1723
|
+
const DEFAULT_OPTIONS = {
|
|
1724
|
+
includeSummary: true,
|
|
1725
|
+
includeKeyPoints: true,
|
|
1726
|
+
includeUrls: true,
|
|
1727
|
+
includeRaw: true,
|
|
1728
|
+
maxTokens: 8000,
|
|
1729
|
+
chunkSize: 512,
|
|
1730
|
+
model: 'default',
|
|
1731
|
+
};
|
|
1732
|
+
function generateSummary(result) {
|
|
1733
|
+
const wordCount = result.text.split(/\s+/).length;
|
|
1734
|
+
return `This page titled "${result.title}" contains approximately ${wordCount} words with ${result.links.length} links and ${result.images.length} images.`;
|
|
1735
|
+
}
|
|
1736
|
+
function generateKeyPoints(result) {
|
|
1737
|
+
const points = [];
|
|
1738
|
+
const text = result.text.slice(0, 5000);
|
|
1739
|
+
if (result.metadata.author)
|
|
1740
|
+
points.push(`Author: ${result.metadata.author}`);
|
|
1741
|
+
if (result.metadata.publishedDate)
|
|
1742
|
+
points.push(`Published: ${result.metadata.publishedDate}`);
|
|
1743
|
+
if (result.metadata.description)
|
|
1744
|
+
points.push(`Description: ${result.metadata.description.slice(0, 200)}`);
|
|
1745
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
|
|
1746
|
+
for (const s of sentences.slice(0, 5)) {
|
|
1747
|
+
const trimmed = s.trim();
|
|
1748
|
+
if (trimmed.length > 30)
|
|
1749
|
+
points.push(trimmed);
|
|
1750
|
+
}
|
|
1751
|
+
return points.slice(0, 8);
|
|
1752
|
+
}
|
|
1753
|
+
function buildSystemPrompt(result) {
|
|
1754
|
+
let prompt = `# Web Page Content\n\n`;
|
|
1755
|
+
prompt += `## Title\n${result.title}\n\n`;
|
|
1756
|
+
prompt += `## URL\n${result.url}\n\n`;
|
|
1757
|
+
if (result.metadata.description) {
|
|
1758
|
+
prompt += `## Description\n${result.metadata.description}\n\n`;
|
|
1759
|
+
}
|
|
1760
|
+
if (result.tables.length > 0) {
|
|
1761
|
+
prompt += `## Tables (${result.tables.length} found)\n`;
|
|
1762
|
+
for (const t of result.tables.slice(0, 3)) {
|
|
1763
|
+
prompt += `- ${t.caption || 'Unnamed table'}: ${t.headers.join(', ')}\n`;
|
|
1764
|
+
}
|
|
1765
|
+
prompt += '\n';
|
|
1766
|
+
}
|
|
1767
|
+
prompt += `## Content\n${result.content}\n\n`;
|
|
1768
|
+
return prompt;
|
|
1769
|
+
}
|
|
1770
|
+
export function formatForAgent(result, options) {
|
|
1771
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1772
|
+
let content = '';
|
|
1773
|
+
if (opts.includeSummary) {
|
|
1774
|
+
content += `## Summary\n${generateSummary(result)}\n\n`;
|
|
1775
|
+
}
|
|
1776
|
+
if (opts.includeKeyPoints) {
|
|
1777
|
+
const points = generateKeyPoints(result);
|
|
1778
|
+
content += `## Key Points\n${points.map(p => `- ${p}`).join('\n')}\n\n`;
|
|
1779
|
+
}
|
|
1780
|
+
if (opts.includeUrls) {
|
|
1781
|
+
const externalUrls = result.links.filter(l => l.isExternal).slice(0, 15);
|
|
1782
|
+
if (externalUrls.length) {
|
|
1783
|
+
content += `## External Links\n${externalUrls.map(l => `- [${l.text || l.href}](${l.href})`).join('\n')}\n\n`;
|
|
1784
|
+
}
|
|
1785
|
+
const internalUrls = result.links.filter(l => l.isInternal).slice(0, 10);
|
|
1786
|
+
if (internalUrls.length) {
|
|
1787
|
+
content += `## Internal Links\n${internalUrls.map(l => `- [${l.text || l.href}](${l.href})`).join('\n')}\n\n`;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (opts.includeRaw) {
|
|
1791
|
+
content += `## Full Content\n${result.content}\n\n`;
|
|
1792
|
+
}
|
|
1793
|
+
const totalTokens = estimateTokens(content, opts.model);
|
|
1794
|
+
if (totalTokens > opts.maxTokens) {
|
|
1795
|
+
const charLimit = Math.floor(opts.maxTokens * 4);
|
|
1796
|
+
content = content.slice(0, charLimit) + '\n\n... (truncated)';
|
|
1797
|
+
}
|
|
1798
|
+
const chunks = chunkMarkdown(content, { maxTokens: opts.chunkSize });
|
|
1799
|
+
const output = {
|
|
1800
|
+
summary: generateSummary(result),
|
|
1801
|
+
content,
|
|
1802
|
+
keyPoints: generateKeyPoints(result),
|
|
1803
|
+
relevantUrls: result.links.filter(l => l.isExternal).map(l => l.href).slice(0, 10),
|
|
1804
|
+
tokens: estimateTokens(content, opts.model),
|
|
1805
|
+
};
|
|
1806
|
+
if (chunks.length > 1) {
|
|
1807
|
+
output.chunks = chunks.map(c => c.content);
|
|
1808
|
+
}
|
|
1809
|
+
return output;
|
|
1810
|
+
}
|
|
1811
|
+
export function buildContext(result) {
|
|
1812
|
+
return buildSystemPrompt(result);
|
|
1813
|
+
}
|
|
1814
|
+
export function generateLlmsTxt(results, options = {}) {
|
|
1815
|
+
const siteName = options.siteName || results[0]?.metadata?.title || 'Site';
|
|
1816
|
+
const description = options.description || results[0]?.metadata?.description || '';
|
|
1817
|
+
const lines = [];
|
|
1818
|
+
lines.push(`# ${siteName}`);
|
|
1819
|
+
lines.push('');
|
|
1820
|
+
if (description) {
|
|
1821
|
+
lines.push(`> ${description}`);
|
|
1822
|
+
lines.push('');
|
|
1823
|
+
}
|
|
1824
|
+
const pages = [];
|
|
1825
|
+
const docs = [];
|
|
1826
|
+
for (const result of results) {
|
|
1827
|
+
const url = result.url.toLowerCase();
|
|
1828
|
+
if (url.includes('doc') || url.includes('guide') || url.includes('api')) {
|
|
1829
|
+
docs.push(result);
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
pages.push(result);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (pages.length > 0) {
|
|
1836
|
+
lines.push('## Pages');
|
|
1837
|
+
lines.push('');
|
|
1838
|
+
for (const page of pages.slice(0, 20)) {
|
|
1839
|
+
const title = page.metadata.title || page.title;
|
|
1840
|
+
const desc = page.metadata.description || '';
|
|
1841
|
+
lines.push(`- ${title}: ${desc || page.url}`);
|
|
1842
|
+
}
|
|
1843
|
+
lines.push('');
|
|
1844
|
+
}
|
|
1845
|
+
if (docs.length > 0) {
|
|
1846
|
+
lines.push('## Documentation');
|
|
1847
|
+
lines.push('');
|
|
1848
|
+
for (const doc of docs.slice(0, 20)) {
|
|
1849
|
+
const title = doc.metadata.title || doc.title;
|
|
1850
|
+
const desc = doc.metadata.description || '';
|
|
1851
|
+
lines.push(`- ${title}: ${desc || doc.url}`);
|
|
1852
|
+
}
|
|
1853
|
+
lines.push('');
|
|
1854
|
+
}
|
|
1855
|
+
if (options.includeOptional && results.length > 5) {
|
|
1856
|
+
lines.push('## Optional');
|
|
1857
|
+
lines.push('');
|
|
1858
|
+
lines.push(`- Full content: ${results.length} pages scraped`);
|
|
1859
|
+
lines.push(`- Last updated: ${new Date().toISOString().split('T')[0]}`);
|
|
1860
|
+
lines.push('');
|
|
1861
|
+
}
|
|
1862
|
+
return lines.join('\n');
|
|
1863
|
+
}
|
|
1864
|
+
export function generateLlmsFullTxt(results) {
|
|
1865
|
+
const parts = [];
|
|
1866
|
+
for (const result of results) {
|
|
1867
|
+
parts.push(`# ${result.title || result.metadata.title || 'Untitled'}`);
|
|
1868
|
+
parts.push('');
|
|
1869
|
+
parts.push(`Source: ${result.url}`);
|
|
1870
|
+
parts.push('');
|
|
1871
|
+
parts.push(result.content);
|
|
1872
|
+
parts.push('');
|
|
1873
|
+
parts.push('---');
|
|
1874
|
+
parts.push('');
|
|
1875
|
+
}
|
|
1876
|
+
return parts.join('\n');
|
|
1877
|
+
}
|
|
1878
|
+
export async function fetchHtml(url, options = {}) {
|
|
1879
|
+
const startTime = Date.now();
|
|
1880
|
+
const profile = getRandomProfile();
|
|
1881
|
+
const baseHeaders = getHeaders(profile);
|
|
1882
|
+
const headers = { ...baseHeaders, ...options.headers };
|
|
1883
|
+
if (options.userAgent)
|
|
1884
|
+
headers['User-Agent'] = options.userAgent;
|
|
1885
|
+
const controller = new AbortController();
|
|
1886
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout || 30000);
|
|
1887
|
+
const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
1888
|
+
try {
|
|
1889
|
+
const response = await fetch(url, {
|
|
1890
|
+
method: 'GET',
|
|
1891
|
+
headers,
|
|
1892
|
+
signal,
|
|
1893
|
+
redirect: 'follow',
|
|
1894
|
+
});
|
|
1895
|
+
clearTimeout(timeoutId);
|
|
1896
|
+
if (!response.ok) {
|
|
1897
|
+
throw new WebScrapeError(`HTTP ${response.status}: ${response.statusText}`, response.status === 403 ? ErrorCode.BLOCKED :
|
|
1898
|
+
response.status === 429 ? ErrorCode.RATE_LIMITED :
|
|
1899
|
+
response.status >= 500 ? ErrorCode.NETWORK_ERROR :
|
|
1900
|
+
ErrorCode.NETWORK_ERROR, response.status, [408, 429, 500, 502, 503, 504].includes(response.status), { url, status: response.status });
|
|
1901
|
+
}
|
|
1902
|
+
const html = await response.text();
|
|
1903
|
+
const responseHeaders = {};
|
|
1904
|
+
response.headers.forEach((value, key) => { responseHeaders[key] = value; });
|
|
1905
|
+
return {
|
|
1906
|
+
html,
|
|
1907
|
+
url: response.url || url,
|
|
1908
|
+
status: response.status,
|
|
1909
|
+
headers: responseHeaders,
|
|
1910
|
+
duration: Date.now() - startTime,
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
catch (error) {
|
|
1914
|
+
clearTimeout(timeoutId);
|
|
1915
|
+
if (error instanceof WebScrapeError)
|
|
1916
|
+
throw error;
|
|
1917
|
+
if (error.name === 'AbortError') {
|
|
1918
|
+
throw new WebScrapeError(`Request timeout after ${options.timeout || 30000}ms`, ErrorCode.TIMEOUT, 504, true, { url, timeout: options.timeout || 30000 });
|
|
1919
|
+
}
|
|
1920
|
+
throw new WebScrapeError(`Network error: ${error.message}`, ErrorCode.NETWORK_ERROR, 503, true, { url }, error);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
export async function fetchAndParse(url, options = {}) {
|
|
1924
|
+
return fetchHtml(url, {
|
|
1925
|
+
timeout: options.timeout,
|
|
1926
|
+
headers: options.headers,
|
|
1927
|
+
userAgent: options.userAgent,
|
|
1928
|
+
proxy: options.proxy,
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
export async function detectRenderingMode(html) {
|
|
1932
|
+
if (/<div id=["'](?:root|app|__next|__nuxt)["']>\s*<\/div>/.test(html))
|
|
1933
|
+
return 'spa';
|
|
1934
|
+
if (html.includes('__NEXT_DATA__') || html.includes('__NUXT__') || html.includes('__VUE_SSR_DATA__'))
|
|
1935
|
+
return 'hydrated';
|
|
1936
|
+
const cheeriomodule = await import("cheerio");
|
|
1937
|
+
const $ = cheeriomodule.load(html);
|
|
1938
|
+
if ($('script[type="application/ld+json"]').length > 0)
|
|
1939
|
+
return 'static';
|
|
1940
|
+
const textContent = $.text().replace(/\s+/g, ' ').trim();
|
|
1941
|
+
if (textContent.length > 500)
|
|
1942
|
+
return 'static';
|
|
1943
|
+
return 'spa';
|
|
1944
|
+
}
|
|
1945
|
+
export async function batchScrape(options, scrapeOpts = {}) {
|
|
1946
|
+
const startTime = Date.now();
|
|
1947
|
+
const { default: pLimit } = await import("p-limit");
|
|
1948
|
+
const limit = pLimit(options.concurrency || 5);
|
|
1949
|
+
const results = [];
|
|
1950
|
+
const errors = [];
|
|
1951
|
+
const tasks = options.urls.map((url) => limit(async () => {
|
|
1952
|
+
try {
|
|
1953
|
+
const result = await scrapeUrl(url, { ...scrapeOpts, mode: scrapeOpts.mode || 'auto' });
|
|
1954
|
+
results.push(result);
|
|
1955
|
+
options.onProgress?.(results.length + errors.length, options.urls.length, url);
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
errors.push({ url, error: error });
|
|
1959
|
+
options.onError?.(url, error);
|
|
1960
|
+
options.onProgress?.(results.length + errors.length, options.urls.length, url);
|
|
1961
|
+
}
|
|
1962
|
+
}));
|
|
1963
|
+
await Promise.allSettled(tasks);
|
|
1964
|
+
return {
|
|
1965
|
+
results,
|
|
1966
|
+
errors,
|
|
1967
|
+
successful: results.length,
|
|
1968
|
+
failed: errors.length,
|
|
1969
|
+
duration: Date.now() - startTime,
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
export async function batchScrapeSequential(urls, options = {}, callbacks) {
|
|
1973
|
+
const startTime = Date.now();
|
|
1974
|
+
const results = [];
|
|
1975
|
+
const errors = [];
|
|
1976
|
+
for (let i = 0; i < urls.length; i++) {
|
|
1977
|
+
try {
|
|
1978
|
+
const result = await scrapeUrl(urls[i], options);
|
|
1979
|
+
results.push(result);
|
|
1980
|
+
callbacks?.onResult?.(result, i);
|
|
1981
|
+
}
|
|
1982
|
+
catch (error) {
|
|
1983
|
+
errors.push({ url: urls[i], error: error });
|
|
1984
|
+
callbacks?.onError?.(urls[i], error, i);
|
|
1985
|
+
}
|
|
1986
|
+
if (callbacks?.delayMs && i < urls.length - 1) {
|
|
1987
|
+
await new Promise(r => setTimeout(r, callbacks.delayMs));
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1991
|
+
results,
|
|
1992
|
+
errors,
|
|
1993
|
+
successful: results.length,
|
|
1994
|
+
failed: errors.length,
|
|
1995
|
+
duration: Date.now() - startTime,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
1999
|
+
/* Section 35: scrape/browser.ts */
|
|
2000
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2001
|
+
export class AgentBrowser {
|
|
2002
|
+
browser = null;
|
|
2003
|
+
context = null;
|
|
2004
|
+
page = null;
|
|
2005
|
+
config;
|
|
2006
|
+
static DEFAULTS = {
|
|
2007
|
+
headless: true,
|
|
2008
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
2009
|
+
viewport: { width: 1280, height: 720 },
|
|
2010
|
+
locale: 'en-US',
|
|
2011
|
+
timeout: 30000,
|
|
2012
|
+
recordVideo: false,
|
|
2013
|
+
proxy: '',
|
|
2014
|
+
storageState: '',
|
|
2015
|
+
blockAds: true,
|
|
2016
|
+
stealth: true,
|
|
2017
|
+
};
|
|
2018
|
+
constructor(config = {}) {
|
|
2019
|
+
this.config = { ...AgentBrowser.DEFAULTS, ...config };
|
|
2020
|
+
}
|
|
2021
|
+
async launch(browserType = 'chromium') {
|
|
2022
|
+
try {
|
|
2023
|
+
const playwrightmodule = await import("playwright");
|
|
2024
|
+
const launcher = { chromium: playwrightmodule.chromium, firefox: playwrightmodule.firefox, webkit: playwrightmodule.webkit }[browserType];
|
|
2025
|
+
const launchOpts = {
|
|
2026
|
+
headless: this.config.headless,
|
|
2027
|
+
args: [
|
|
2028
|
+
'--no-sandbox',
|
|
2029
|
+
'--disable-setuid-sandbox',
|
|
2030
|
+
'--disable-dev-shm-usage',
|
|
2031
|
+
'--disable-web-security',
|
|
2032
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
2033
|
+
],
|
|
2034
|
+
};
|
|
2035
|
+
if (this.config.proxy) {
|
|
2036
|
+
launchOpts.proxy = { server: this.config.proxy };
|
|
2037
|
+
}
|
|
2038
|
+
this.browser = await launcher.launch(launchOpts);
|
|
2039
|
+
const ctxOpts = {
|
|
2040
|
+
viewport: this.config.viewport,
|
|
2041
|
+
locale: this.config.locale,
|
|
2042
|
+
userAgent: this.config.userAgent,
|
|
2043
|
+
};
|
|
2044
|
+
if (this.config.recordVideo) {
|
|
2045
|
+
ctxOpts.recordVideo = { dir: './videos' };
|
|
2046
|
+
}
|
|
2047
|
+
if (this.config.geolocation) {
|
|
2048
|
+
ctxOpts.geolocation = this.config.geolocation;
|
|
2049
|
+
ctxOpts.permissions = ['geolocation'];
|
|
2050
|
+
}
|
|
2051
|
+
if (this.config.storageState) {
|
|
2052
|
+
ctxOpts.storageState = this.config.storageState;
|
|
2053
|
+
}
|
|
2054
|
+
this.context = await this.browser.newContext(ctxOpts);
|
|
2055
|
+
if (this.config.blockAds) {
|
|
2056
|
+
await this.context.route('**/*.{png,jpg,jpeg,gif,svg,ico,css,woff,woff2,mp4,webm}', r => r.abort());
|
|
2057
|
+
}
|
|
2058
|
+
this.page = await this.context.newPage();
|
|
2059
|
+
this.page.setDefaultTimeout(this.config.timeout);
|
|
2060
|
+
}
|
|
2061
|
+
catch (error) {
|
|
2062
|
+
if (error.message?.includes('playwright') || error.message?.includes('chromium')) {
|
|
2063
|
+
throw new BrowserNotAvailableError();
|
|
2064
|
+
}
|
|
2065
|
+
throw error;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
async navigate(url, waitUntil = 'networkidle') {
|
|
2069
|
+
if (!this.page)
|
|
2070
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2071
|
+
await this.page.goto(url, { waitUntil, timeout: this.config.timeout });
|
|
2072
|
+
}
|
|
2073
|
+
async screenshot(opts = {}) {
|
|
2074
|
+
if (!this.page)
|
|
2075
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2076
|
+
return this.page.screenshot({
|
|
2077
|
+
fullPage: opts.fullPage ?? false,
|
|
2078
|
+
type: opts.type ?? 'png',
|
|
2079
|
+
quality: opts.quality ?? 80,
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
async evaluate(fn, args) {
|
|
2083
|
+
if (!this.page)
|
|
2084
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2085
|
+
if (typeof fn === 'string') {
|
|
2086
|
+
return this.page.evaluate(new Function(fn));
|
|
2087
|
+
}
|
|
2088
|
+
return this.page.evaluate(fn, args);
|
|
2089
|
+
}
|
|
2090
|
+
async click(selector) {
|
|
2091
|
+
if (!this.page)
|
|
2092
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2093
|
+
await this.page.click(selector);
|
|
2094
|
+
}
|
|
2095
|
+
async type(selector, text, delayMs = 0) {
|
|
2096
|
+
if (!this.page)
|
|
2097
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2098
|
+
await this.page.fill(selector, '');
|
|
2099
|
+
await this.page.type(selector, text, { delay: delayMs });
|
|
2100
|
+
}
|
|
2101
|
+
async scrollToBottom(step = 800, delayMs = 300, maxScrolls = 50) {
|
|
2102
|
+
if (!this.page)
|
|
2103
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2104
|
+
let scrolls = 0;
|
|
2105
|
+
let prevHeight = 0;
|
|
2106
|
+
while (scrolls < maxScrolls) {
|
|
2107
|
+
const height = await this.page.evaluate(() => document.body.scrollHeight);
|
|
2108
|
+
if (height === prevHeight && scrolls > 2)
|
|
2109
|
+
break;
|
|
2110
|
+
prevHeight = height;
|
|
2111
|
+
await this.page.evaluate(y => window.scrollBy(0, y), step);
|
|
2112
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
2113
|
+
scrolls++;
|
|
2114
|
+
}
|
|
2115
|
+
return scrolls;
|
|
2116
|
+
}
|
|
2117
|
+
async scrollTo(x, y) {
|
|
2118
|
+
if (!this.page)
|
|
2119
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2120
|
+
await this.page.evaluate(([px, py]) => window.scrollTo(px, py), [x, y]);
|
|
2121
|
+
}
|
|
2122
|
+
async waitFor(selector, timeout) {
|
|
2123
|
+
if (!this.page)
|
|
2124
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2125
|
+
await this.page.waitForSelector(selector, { timeout: timeout ?? this.config.timeout });
|
|
2126
|
+
}
|
|
2127
|
+
async waitForLoad() {
|
|
2128
|
+
if (!this.page)
|
|
2129
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2130
|
+
await this.page.waitForLoadState('networkidle');
|
|
2131
|
+
}
|
|
2132
|
+
async html() {
|
|
2133
|
+
if (!this.page)
|
|
2134
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2135
|
+
return this.page.content();
|
|
2136
|
+
}
|
|
2137
|
+
async text() {
|
|
2138
|
+
if (!this.page)
|
|
2139
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2140
|
+
return this.page.evaluate(() => document.body.innerText);
|
|
2141
|
+
}
|
|
2142
|
+
async title() {
|
|
2143
|
+
if (!this.page)
|
|
2144
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2145
|
+
return this.page.title();
|
|
2146
|
+
}
|
|
2147
|
+
async url() {
|
|
2148
|
+
if (!this.page)
|
|
2149
|
+
throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
2150
|
+
return this.page.url();
|
|
2151
|
+
}
|
|
2152
|
+
async executeCommands(commands) {
|
|
2153
|
+
const results = [];
|
|
2154
|
+
for (const cmd of commands) {
|
|
2155
|
+
switch (cmd.action) {
|
|
2156
|
+
case 'goto':
|
|
2157
|
+
await this.navigate(cmd.args?.url);
|
|
2158
|
+
results.push({ ok: true });
|
|
2159
|
+
break;
|
|
2160
|
+
case 'click':
|
|
2161
|
+
await this.click(cmd.args?.selector);
|
|
2162
|
+
results.push({ ok: true });
|
|
2163
|
+
break;
|
|
2164
|
+
case 'type':
|
|
2165
|
+
await this.type(cmd.args?.selector, cmd.args?.text);
|
|
2166
|
+
results.push({ ok: true });
|
|
2167
|
+
break;
|
|
2168
|
+
case 'screenshot':
|
|
2169
|
+
results.push(await this.screenshot(cmd.args));
|
|
2170
|
+
break;
|
|
2171
|
+
case 'evaluate':
|
|
2172
|
+
results.push(await this.evaluate(cmd.args?.fn));
|
|
2173
|
+
break;
|
|
2174
|
+
case 'wait':
|
|
2175
|
+
await this.waitFor(cmd.args?.selector);
|
|
2176
|
+
results.push({ ok: true });
|
|
2177
|
+
break;
|
|
2178
|
+
case 'scroll':
|
|
2179
|
+
if (cmd.args?.to) {
|
|
2180
|
+
await this.scrollTo(0, cmd.args.to);
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
await this.scrollToBottom();
|
|
2184
|
+
}
|
|
2185
|
+
results.push({ ok: true });
|
|
2186
|
+
break;
|
|
2187
|
+
case 'extract':
|
|
2188
|
+
results.push({
|
|
2189
|
+
html: await this.html(),
|
|
2190
|
+
text: await this.text(),
|
|
2191
|
+
title: await this.title(),
|
|
2192
|
+
});
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return results;
|
|
2197
|
+
}
|
|
2198
|
+
async close() {
|
|
2199
|
+
if (this.page)
|
|
2200
|
+
await this.page.close().catch(() => { });
|
|
2201
|
+
if (this.context)
|
|
2202
|
+
await this.context.close().catch(() => { });
|
|
2203
|
+
if (this.browser)
|
|
2204
|
+
await this.browser.close().catch(() => { });
|
|
2205
|
+
this.page = null;
|
|
2206
|
+
this.context = null;
|
|
2207
|
+
this.browser = null;
|
|
2208
|
+
}
|
|
2209
|
+
isConnected() {
|
|
2210
|
+
return this.browser?.isConnected() ?? false;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
export function createBrowser(config) {
|
|
2214
|
+
return new AgentBrowser(config);
|
|
2215
|
+
}
|
|
2216
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2217
|
+
/* Section 36: scrape/scrape.ts */
|
|
2218
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2219
|
+
export async function scrapeUrl(url, options = {}) {
|
|
2220
|
+
if (!url || !url.startsWith('http')) {
|
|
2221
|
+
throw new WebScrapeError(`Invalid URL: ${url}`, ErrorCode.VALIDATION_FAILED, 400, false);
|
|
2222
|
+
}
|
|
2223
|
+
const startTime = Date.now();
|
|
2224
|
+
const mode = options.mode || 'auto';
|
|
2225
|
+
if (mode === 'fetch' || mode === 'auto') {
|
|
2226
|
+
try {
|
|
2227
|
+
const fetchResult = await fetchHtml(url, {
|
|
2228
|
+
timeout: options.timeout,
|
|
2229
|
+
headers: options.headers,
|
|
2230
|
+
userAgent: options.userAgent,
|
|
2231
|
+
});
|
|
2232
|
+
if (mode === 'auto') {
|
|
2233
|
+
const renderMode = await detectRenderingMode(fetchResult.html);
|
|
2234
|
+
if (renderMode === 'static') {
|
|
2235
|
+
return buildResult(fetchResult.html, fetchResult.url, options, startTime);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
else {
|
|
2239
|
+
return buildResult(fetchResult.html, fetchResult.url, options, startTime);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
catch (error) {
|
|
2243
|
+
if (mode === 'fetch')
|
|
2244
|
+
throw error;
|
|
2245
|
+
if (!(error instanceof WebScrapeError) || error.isRetryable) {
|
|
2246
|
+
// Fall through to browser mode
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
throw error;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
const browser = new AgentBrowser({
|
|
2254
|
+
headless: true,
|
|
2255
|
+
timeout: options.timeout ?? 30000,
|
|
2256
|
+
proxy: options.proxy ?? '',
|
|
2257
|
+
userAgent: options.userAgent,
|
|
2258
|
+
});
|
|
2259
|
+
try {
|
|
2260
|
+
await browser.launch();
|
|
2261
|
+
await browser.navigate(url, options.waitUntil ?? 'networkidle');
|
|
2262
|
+
if (options.scroll) {
|
|
2263
|
+
await browser.scrollToBottom(800, options.scrollDelay ?? 300, options.maxScrolls ?? 50);
|
|
2264
|
+
}
|
|
2265
|
+
const html = await browser.html();
|
|
2266
|
+
const pageTitle = await browser.title();
|
|
2267
|
+
const pageUrl = await browser.url();
|
|
2268
|
+
return buildResult(html, pageUrl, options, startTime, pageTitle);
|
|
2269
|
+
}
|
|
2270
|
+
finally {
|
|
2271
|
+
await browser.close();
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
export async function scrapeHtml(html, options = {}) {
|
|
2275
|
+
const startTime = Date.now();
|
|
2276
|
+
return buildResult(html, 'about:blank', options, startTime);
|
|
2277
|
+
}
|
|
2278
|
+
export async function scrapeWithBrowser(browser, url, options = {}) {
|
|
2279
|
+
const startTime = Date.now();
|
|
2280
|
+
await browser.navigate(url, options.waitUntil ?? 'networkidle');
|
|
2281
|
+
if (options.scroll) {
|
|
2282
|
+
await browser.scrollToBottom(800, options.scrollDelay ?? 300, options.maxScrolls ?? 50);
|
|
2283
|
+
}
|
|
2284
|
+
const html = await browser.html();
|
|
2285
|
+
const pageTitle = await browser.title();
|
|
2286
|
+
const pageUrl = await browser.url();
|
|
2287
|
+
return buildResult(html, pageUrl, options, startTime, pageTitle);
|
|
2288
|
+
}
|
|
2289
|
+
async function buildResult(html, url, options, startTime, title) {
|
|
2290
|
+
const extracted = await extractContent(html, {
|
|
2291
|
+
readable: true,
|
|
2292
|
+
preserveLinks: options.extractLinks ?? true,
|
|
2293
|
+
preserveImages: options.extractImages ?? true,
|
|
2294
|
+
preserveTables: options.extractTables ?? true,
|
|
2295
|
+
maxLength: options.maxContentLength,
|
|
2296
|
+
removeSelectors: options.removeSelectors,
|
|
2297
|
+
});
|
|
2298
|
+
const duration = Date.now() - startTime;
|
|
2299
|
+
return {
|
|
2300
|
+
url,
|
|
2301
|
+
title: title || extracted.metadata.title || '',
|
|
2302
|
+
content: extracted.content,
|
|
2303
|
+
format: options.format ?? 'markdown',
|
|
2304
|
+
text: extracted.text,
|
|
2305
|
+
links: extracted.links,
|
|
2306
|
+
images: extracted.images,
|
|
2307
|
+
tables: extracted.tables,
|
|
2308
|
+
metadata: extracted.metadata,
|
|
2309
|
+
extractedAt: new Date().toISOString(),
|
|
2310
|
+
duration,
|
|
2311
|
+
size: extracted.content.length + extracted.text.length,
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
function isSameDomain(url1, url2) {
|
|
2315
|
+
try {
|
|
2316
|
+
return new URL(url1).hostname === new URL(url2).hostname;
|
|
2317
|
+
}
|
|
2318
|
+
catch {
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function normalizeForDedup(url) {
|
|
2323
|
+
try {
|
|
2324
|
+
const u = new URL(url);
|
|
2325
|
+
u.hash = '';
|
|
2326
|
+
u.search = '';
|
|
2327
|
+
return u.href.replace(/\/+$/, '') || u.href;
|
|
2328
|
+
}
|
|
2329
|
+
catch {
|
|
2330
|
+
return url;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
export async function legacycrawl(startUrl, options = {}) {
|
|
2334
|
+
const { maxDepth = 2, maxPages = 50, maxConcurrent = 3, sameDomain = true, delayMs = 1000, onDiscover, onResult, onError, ...scrapeOpts } = options;
|
|
2335
|
+
const startTime = Date.now();
|
|
2336
|
+
const visited = new Set();
|
|
2337
|
+
const queue = [{ url: startUrl, depth: 0 }];
|
|
2338
|
+
const results = [];
|
|
2339
|
+
let failed = 0;
|
|
2340
|
+
visited.add(normalizeForDedup(startUrl));
|
|
2341
|
+
while (queue.length > 0 && results.length < maxPages) {
|
|
2342
|
+
const batch = queue.splice(0, maxConcurrent);
|
|
2343
|
+
const promises = batch.map(async (entry) => {
|
|
2344
|
+
if (entry.depth > maxDepth)
|
|
2345
|
+
return;
|
|
2346
|
+
try {
|
|
2347
|
+
const result = await scrapeUrl(entry.url, scrapeOpts);
|
|
2348
|
+
results.push(result);
|
|
2349
|
+
onResult?.(result);
|
|
2350
|
+
// Extract links for further crawling
|
|
2351
|
+
if (entry.depth < maxDepth) {
|
|
2352
|
+
for (const link of result.links) {
|
|
2353
|
+
const normalized = normalizeForDedup(link.href);
|
|
2354
|
+
if (visited.has(normalized))
|
|
2355
|
+
continue;
|
|
2356
|
+
if (sameDomain && !isSameDomain(startUrl, link.href))
|
|
2357
|
+
continue;
|
|
2358
|
+
try {
|
|
2359
|
+
new URL(link.href);
|
|
2360
|
+
}
|
|
2361
|
+
catch {
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
visited.add(normalized);
|
|
2365
|
+
queue.push({ url: link.href, depth: entry.depth + 1 });
|
|
2366
|
+
onDiscover?.(link.href, entry.depth + 1);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
catch (error) {
|
|
2371
|
+
failed++;
|
|
2372
|
+
onError?.(entry.url, error);
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
await Promise.all(promises);
|
|
2376
|
+
if (delayMs > 0 && queue.length > 0) {
|
|
2377
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const duration = Date.now() - startTime;
|
|
2381
|
+
return {
|
|
2382
|
+
results,
|
|
2383
|
+
stats: {
|
|
2384
|
+
totalUrls: visited.size,
|
|
2385
|
+
successful: results.length,
|
|
2386
|
+
failed,
|
|
2387
|
+
skipped: visited.size - results.length - failed,
|
|
2388
|
+
duration,
|
|
2389
|
+
avgResponseTime: results.length > 0 ? results.reduce((sum, r) => sum + r.duration, 0) / results.length : 0,
|
|
2390
|
+
},
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Builds the hono application lazily — hono is an optional runtime the merged
|
|
2395
|
+
* module never requires at load time (the grand-merge load-safety adaptation
|
|
2396
|
+
* of the former scrape/server.ts).
|
|
2397
|
+
*/
|
|
2398
|
+
async function buildscrapeserver() {
|
|
2399
|
+
const { Hono } = await import("hono");
|
|
2400
|
+
const { cors } = await import("hono/cors");
|
|
2401
|
+
const app = new Hono();
|
|
2402
|
+
app.use('*', cors());
|
|
2403
|
+
const api = new Hono();
|
|
2404
|
+
api.post('/scrape', async (c) => {
|
|
2405
|
+
const body = await c.req.json();
|
|
2406
|
+
const { url, options = {} } = body;
|
|
2407
|
+
if (!url) {
|
|
2408
|
+
return c.json({ error: { code: 'MISSING_URL', message: 'URL is required' } }, 400);
|
|
2409
|
+
}
|
|
2410
|
+
try {
|
|
2411
|
+
const result = await scrapeUrl(url, options);
|
|
2412
|
+
return c.json({ status: 'ok', data: result });
|
|
2413
|
+
}
|
|
2414
|
+
catch (error) {
|
|
2415
|
+
return c.json({
|
|
2416
|
+
error: {
|
|
2417
|
+
code: error.code || 'SCRAPE_ERROR',
|
|
2418
|
+
message: error.message,
|
|
2419
|
+
}
|
|
2420
|
+
}, error.statusCode || 500);
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
api.post('/scrape/agent', async (c) => {
|
|
2424
|
+
const body = await c.req.json();
|
|
2425
|
+
const { url, options = {} } = body;
|
|
2426
|
+
if (!url) {
|
|
2427
|
+
return c.json({ error: { code: 'MISSING_URL', message: 'URL is required' } }, 400);
|
|
2428
|
+
}
|
|
2429
|
+
try {
|
|
2430
|
+
const result = await scrapeUrl(url, options);
|
|
2431
|
+
const agentOutput = formatForAgent(result);
|
|
2432
|
+
return c.json({ status: 'ok', data: agentOutput });
|
|
2433
|
+
}
|
|
2434
|
+
catch (error) {
|
|
2435
|
+
return c.json({
|
|
2436
|
+
error: {
|
|
2437
|
+
code: error.code || 'SCRAPE_ERROR',
|
|
2438
|
+
message: error.message,
|
|
2439
|
+
}
|
|
2440
|
+
}, error.statusCode || 500);
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
api.post('/batch', async (c) => {
|
|
2444
|
+
const body = await c.req.json();
|
|
2445
|
+
const { urls, options = {}, scrapeOptions = {} } = body;
|
|
2446
|
+
if (!urls || !Array.isArray(urls) || urls.length === 0) {
|
|
2447
|
+
return c.json({ error: { code: 'MISSING_URLS', message: 'urls array is required' } }, 400);
|
|
2448
|
+
}
|
|
2449
|
+
try {
|
|
2450
|
+
const result = await batchScrape({ urls, ...options }, scrapeOptions);
|
|
2451
|
+
return c.json({
|
|
2452
|
+
status: 'ok',
|
|
2453
|
+
data: {
|
|
2454
|
+
successful: result.successful,
|
|
2455
|
+
failed: result.failed,
|
|
2456
|
+
duration: result.duration,
|
|
2457
|
+
results: result.results,
|
|
2458
|
+
errors: result.errors.map(e => ({ url: e.url, error: e.error.message })),
|
|
2459
|
+
},
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
catch (error) {
|
|
2463
|
+
return c.json({
|
|
2464
|
+
error: {
|
|
2465
|
+
code: 'BATCH_ERROR',
|
|
2466
|
+
message: error.message,
|
|
2467
|
+
}
|
|
2468
|
+
}, 500);
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
api.post('/crawl', async (c) => {
|
|
2472
|
+
const body = await c.req.json();
|
|
2473
|
+
const { url, options = {} } = body;
|
|
2474
|
+
if (!url) {
|
|
2475
|
+
return c.json({ error: { code: 'MISSING_URL', message: 'URL is required' } }, 400);
|
|
2476
|
+
}
|
|
2477
|
+
try {
|
|
2478
|
+
const result = await legacycrawl(url, options);
|
|
2479
|
+
return c.json({
|
|
2480
|
+
status: 'ok',
|
|
2481
|
+
data: {
|
|
2482
|
+
stats: result.stats,
|
|
2483
|
+
results: result.results.slice(0, 100),
|
|
2484
|
+
},
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
catch (error) {
|
|
2488
|
+
return c.json({
|
|
2489
|
+
error: {
|
|
2490
|
+
code: 'CRAWL_ERROR',
|
|
2491
|
+
message: error.message,
|
|
2492
|
+
}
|
|
2493
|
+
}, 500);
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
api.get('/health', (c) => {
|
|
2497
|
+
return c.json({ status: 'ok', version: '2.0.0', uptime: process.uptime() });
|
|
2498
|
+
});
|
|
2499
|
+
app.route('/v1', api);
|
|
2500
|
+
return app;
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Creates the self-hosted scrape API surface. The factory became async with
|
|
2504
|
+
* the grand merge: the hono application is built on first call and the port
|
|
2505
|
+
* draw keeps using the process-wide random port helper.
|
|
2506
|
+
*/
|
|
2507
|
+
export async function createServer(config = {}) {
|
|
2508
|
+
const app = await buildscrapeserver();
|
|
2509
|
+
return {
|
|
2510
|
+
fetch: app.fetch,
|
|
2511
|
+
port: config.port || randomPort(),
|
|
2512
|
+
host: config.host || '0.0.0.0',
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2516
|
+
/* Section 39: scrape/cli.ts — the webscrape command-line interface (grand-merge adaptation: commander became a lazy runtime import and the top-level program construction became an exported factory). */
|
|
2517
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2518
|
+
/**
|
|
2519
|
+
* Runs the webscrape CLI. The former top-level commander construction of
|
|
2520
|
+
* scrape/cli.ts became this exported factory in the grand merge: commander
|
|
2521
|
+
* is imported on first call and loading the module never requires it.
|
|
2522
|
+
*/
|
|
2523
|
+
export async function runscrapecli() {
|
|
2524
|
+
const { Command } = await import("commander");
|
|
2525
|
+
const program = new Command();
|
|
2526
|
+
program
|
|
2527
|
+
.name('webscrape')
|
|
2528
|
+
.description('DevThink WebScrape — Universal AI-powered web scraping toolkit')
|
|
2529
|
+
.version('2.0.0');
|
|
2530
|
+
program
|
|
2531
|
+
.argument('[url]', 'URL to scrape')
|
|
2532
|
+
.option('-f, --format <type>', 'Output format: markdown, html, text, json, xml, redis', 'markdown')
|
|
2533
|
+
.option('-o, --output <file>', 'Write output to file')
|
|
2534
|
+
.option('--file <path>', 'Scrape HTML from local file')
|
|
2535
|
+
.option('--mode <mode>', 'Scraping mode: auto, fetch, browser', 'auto')
|
|
2536
|
+
.option('--no-headless', 'Show browser window')
|
|
2537
|
+
.option('--timeout <ms>', 'Navigation timeout', '30000')
|
|
2538
|
+
.option('--scroll', 'Scroll page to load dynamic content')
|
|
2539
|
+
.option('--readable', 'Extract readable content only')
|
|
2540
|
+
.option('--agent', 'Format output for AI agent consumption')
|
|
2541
|
+
.option('--pretty', 'Pretty-print JSON output')
|
|
2542
|
+
.option('--proxy <url>', 'Proxy server URL')
|
|
2543
|
+
.option('--retries <n>', 'Number of retries', '3')
|
|
2544
|
+
.option('--user-agent <ua>', 'Custom User-Agent string')
|
|
2545
|
+
.action(async (url, options) => {
|
|
2546
|
+
if (!url && !options.file) {
|
|
2547
|
+
program.help();
|
|
2548
|
+
}
|
|
2549
|
+
const format = resolveFormat(options.format);
|
|
2550
|
+
const scrapeOpts = {
|
|
2551
|
+
format,
|
|
2552
|
+
timeout: parseInt(options.timeout),
|
|
2553
|
+
scroll: options.scroll,
|
|
2554
|
+
readable: options.readable,
|
|
2555
|
+
mode: options.mode,
|
|
2556
|
+
proxy: options.proxy,
|
|
2557
|
+
retries: parseInt(options.retries),
|
|
2558
|
+
userAgent: options.userAgent,
|
|
2559
|
+
extractLinks: true,
|
|
2560
|
+
extractImages: true,
|
|
2561
|
+
extractTables: true,
|
|
2562
|
+
};
|
|
2563
|
+
if (options.readable) {
|
|
2564
|
+
scrapeOpts.removeSelectors = [
|
|
2565
|
+
'script', 'style', 'nav', 'footer', 'header',
|
|
2566
|
+
'.sidebar', '.advertisement', '.ads', '.menu',
|
|
2567
|
+
'.comments', '.comment', '#comments',
|
|
2568
|
+
];
|
|
2569
|
+
}
|
|
2570
|
+
console.error(`Scraping: ${url || options.file}`);
|
|
2571
|
+
let result;
|
|
2572
|
+
if (options.file) {
|
|
2573
|
+
const filePath = resolve(options.file);
|
|
2574
|
+
if (!existsSync(filePath)) {
|
|
2575
|
+
console.error(`File not found: ${filePath}`);
|
|
2576
|
+
process.exit(1);
|
|
2577
|
+
}
|
|
2578
|
+
const html = readFileSync(filePath, 'utf-8');
|
|
2579
|
+
result = await scrapeHtml(html, scrapeOpts);
|
|
2580
|
+
}
|
|
2581
|
+
else {
|
|
2582
|
+
result = await scrapeUrl(url, scrapeOpts);
|
|
2583
|
+
}
|
|
2584
|
+
console.error(`Done in ${result.duration}ms (${(result.size / 1024).toFixed(1)} KB)`);
|
|
2585
|
+
if (options.agent) {
|
|
2586
|
+
const agentOutput = formatForAgent(result);
|
|
2587
|
+
const output = JSON.stringify(agentOutput, null, 2);
|
|
2588
|
+
if (options.output) {
|
|
2589
|
+
writeOutput(options.output, output);
|
|
2590
|
+
console.error(`Saved to ${options.output}`);
|
|
2591
|
+
}
|
|
2592
|
+
else {
|
|
2593
|
+
console.log(output);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
else {
|
|
2597
|
+
const serializeOpts = buildSerializeOptions(format, options.pretty);
|
|
2598
|
+
const serialized = serializeResult(result, serializeOpts);
|
|
2599
|
+
if (options.output) {
|
|
2600
|
+
const ext = extensionForFormat(format);
|
|
2601
|
+
const finalPath = options.output.endsWith(ext) ? options.output : options.output + ext;
|
|
2602
|
+
writeOutput(finalPath, serialized.content);
|
|
2603
|
+
console.error(`Saved to ${finalPath}`);
|
|
2604
|
+
}
|
|
2605
|
+
else {
|
|
2606
|
+
console.log(serialized.content);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
program.parse();
|
|
2611
|
+
}
|
|
2612
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2613
|
+
/* Section 40: scrape/dev-server.ts — the zero-dependency local API server (grand-merge adaptation: the top-level listener became an exported factory so importing the module never binds a port). */
|
|
2614
|
+
/* ════════════════════════════════════════════════════════════════════ */
|
|
2615
|
+
/**
|
|
2616
|
+
* Starts the local scrape API dev server. The former top-level bootstrap
|
|
2617
|
+
* of scrape/dev-server.ts became this exported factory in the grand merge
|
|
2618
|
+
* (a merged module must never bind a port as an import side effect).
|
|
2619
|
+
*/
|
|
2620
|
+
export function startdevserver() {
|
|
2621
|
+
const PORT = randomPort();
|
|
2622
|
+
const server = createnodeserver(async (req, res) => {
|
|
2623
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
2624
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
2625
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
2626
|
+
if (req.method === 'OPTIONS') {
|
|
2627
|
+
res.writeHead(200);
|
|
2628
|
+
res.end();
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
if (req.method !== 'POST' || req.url !== '/api/scrape') {
|
|
2632
|
+
res.writeHead(404);
|
|
2633
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
let body = '';
|
|
2637
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
2638
|
+
req.on('end', async () => {
|
|
2639
|
+
try {
|
|
2640
|
+
const { url, format: fmt, options = {} } = JSON.parse(body);
|
|
2641
|
+
if (!url)
|
|
2642
|
+
throw new Error('URL is required');
|
|
2643
|
+
const format = resolveFormat(fmt || 'markdown');
|
|
2644
|
+
const result = await scrapeUrl(url, {
|
|
2645
|
+
timeout: 30000,
|
|
2646
|
+
scroll: options.scroll,
|
|
2647
|
+
extractLinks: options.extractLinks ?? true,
|
|
2648
|
+
extractImages: options.extractImages ?? true,
|
|
2649
|
+
extractTables: options.extractTables ?? true,
|
|
2650
|
+
});
|
|
2651
|
+
let response;
|
|
2652
|
+
if (options.agent) {
|
|
2653
|
+
const agentOutput = formatForAgent(result);
|
|
2654
|
+
response = {
|
|
2655
|
+
success: true,
|
|
2656
|
+
title: result.title,
|
|
2657
|
+
url: result.url,
|
|
2658
|
+
duration: result.duration,
|
|
2659
|
+
size: result.size,
|
|
2660
|
+
agentOutput,
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
else {
|
|
2664
|
+
const serializeOpts = buildSerializeOptions(format);
|
|
2665
|
+
const serialized = serializeResult(result, serializeOpts);
|
|
2666
|
+
response = {
|
|
2667
|
+
success: true,
|
|
2668
|
+
title: result.title,
|
|
2669
|
+
url: result.url,
|
|
2670
|
+
format,
|
|
2671
|
+
content: serialized.content,
|
|
2672
|
+
duration: result.duration,
|
|
2673
|
+
size: serialized.size,
|
|
2674
|
+
metadata: result.metadata,
|
|
2675
|
+
links: result.links,
|
|
2676
|
+
images: result.images,
|
|
2677
|
+
tables: result.tables,
|
|
2678
|
+
result,
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2682
|
+
res.end(JSON.stringify(response));
|
|
2683
|
+
}
|
|
2684
|
+
catch (err) {
|
|
2685
|
+
const error = err;
|
|
2686
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2687
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
});
|
|
2691
|
+
server.listen(PORT, () => {
|
|
2692
|
+
console.log(`API server running at http://localhost:${PORT}`);
|
|
2693
|
+
console.log(`POST /api/scrape with { url, format?, options? }`);
|
|
2694
|
+
});
|
|
2695
|
+
return server;
|
|
2696
|
+
}
|
|
2697
|
+
//# sourceMappingURL=webscrape.js.map
|