codeply-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +63 -63
- package/bin/codeply.js +129 -6
- package/lib/agent.mjs +2139 -2139
- package/lib/ai.mjs +6 -6
- package/lib/apply-patch.js +217 -217
- package/lib/arity-table.json +138 -138
- package/lib/arity.js +70 -70
- package/lib/auth.mjs +6 -6
- package/lib/cloud.mjs +708 -0
- package/lib/commands.js +103 -103
- package/lib/design-library/assemble-artifact.mjs +21 -21
- package/lib/design-library/cleanup.mjs +45 -45
- package/lib/design-library/index.json +22379 -22379
- package/lib/design-library/make-table-data.mjs +39 -39
- package/lib/diagnostics.js +192 -192
- package/lib/edit-engine.js +364 -364
- package/lib/github-agent.mjs +311 -290
- package/lib/history.js +75 -75
- package/lib/mcp.js +258 -258
- package/lib/native-tools.mjs +189 -189
- package/lib/oauth-connectors.js +2 -1
- package/lib/permissions.js +109 -109
- package/lib/plugins.js +353 -353
- package/lib/server.mjs +457 -457
- package/lib/session-db.js +220 -220
- package/lib/share.js +168 -168
- package/lib/skills.js +416 -416
- package/lib/snapshot.js +146 -146
- package/lib/terminal-hints.js +154 -154
- package/lib/tools.mjs +2012 -2012
- package/lib/typecheck-worker.js +147 -147
- package/lib/web-tools.js +158 -158
- package/package.json +49 -47
- package/skills/SOURCE.md +58 -58
- package/skills/accessibility/SKILL.md +147 -147
- package/skills/agent-architecture-audit/SKILL.md +257 -257
- package/skills/agent-eval/SKILL.md +147 -147
- package/skills/agent-harness-construction/SKILL.md +74 -74
- package/skills/agent-introspection-debugging/SKILL.md +154 -154
- package/skills/agent-payment-x402/SKILL.md +225 -225
- package/skills/agent-self-evaluation/SKILL.md +182 -182
- package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
- package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
- package/skills/agent-sort/SKILL.md +216 -216
- package/skills/agentic-engineering/SKILL.md +64 -64
- package/skills/agentic-os/SKILL.md +388 -388
- package/skills/ai-first-engineering/SKILL.md +52 -52
- package/skills/ai-regression-testing/SKILL.md +386 -386
- package/skills/android-clean-architecture/SKILL.md +340 -340
- package/skills/angular-developer/SKILL.md +155 -155
- package/skills/angular-developer/references/angular-animations.md +160 -160
- package/skills/angular-developer/references/angular-aria.md +410 -410
- package/skills/angular-developer/references/cli.md +86 -86
- package/skills/angular-developer/references/component-harnesses.md +59 -59
- package/skills/angular-developer/references/component-styling.md +91 -91
- package/skills/angular-developer/references/components.md +117 -117
- package/skills/angular-developer/references/creating-services.md +97 -97
- package/skills/angular-developer/references/data-resolvers.md +69 -69
- package/skills/angular-developer/references/define-routes.md +67 -67
- package/skills/angular-developer/references/defining-providers.md +72 -72
- package/skills/angular-developer/references/di-fundamentals.md +120 -120
- package/skills/angular-developer/references/e2e-testing.md +56 -56
- package/skills/angular-developer/references/effects.md +83 -83
- package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
- package/skills/angular-developer/references/host-elements.md +80 -80
- package/skills/angular-developer/references/injection-context.md +63 -63
- package/skills/angular-developer/references/inputs.md +101 -101
- package/skills/angular-developer/references/linked-signal.md +59 -59
- package/skills/angular-developer/references/loading-strategies.md +61 -61
- package/skills/angular-developer/references/mcp.md +108 -108
- package/skills/angular-developer/references/navigate-to-routes.md +69 -69
- package/skills/angular-developer/references/outputs.md +86 -86
- package/skills/angular-developer/references/reactive-forms.md +122 -122
- package/skills/angular-developer/references/rendering-strategies.md +44 -44
- package/skills/angular-developer/references/resource.md +77 -77
- package/skills/angular-developer/references/route-animations.md +56 -56
- package/skills/angular-developer/references/route-guards.md +52 -52
- package/skills/angular-developer/references/router-lifecycle.md +45 -45
- package/skills/angular-developer/references/router-testing.md +87 -87
- package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
- package/skills/angular-developer/references/signal-forms.md +795 -795
- package/skills/angular-developer/references/signals-overview.md +94 -94
- package/skills/angular-developer/references/tailwind-css.md +69 -69
- package/skills/angular-developer/references/template-driven-forms.md +114 -114
- package/skills/angular-developer/references/testing-fundamentals.md +65 -65
- package/skills/api-connector-builder/SKILL.md +121 -121
- package/skills/api-design/SKILL.md +524 -524
- package/skills/architecture-decision-records/SKILL.md +180 -180
- package/skills/article-writing/SKILL.md +80 -80
- package/skills/automation-audit-ops/SKILL.md +143 -143
- package/skills/autonomous-agent-harness/SKILL.md +274 -274
- package/skills/autonomous-loops/SKILL.md +611 -611
- package/skills/backend-patterns/SKILL.md +562 -562
- package/skills/benchmark/SKILL.md +95 -95
- package/skills/benchmark-methodology/SKILL.md +191 -191
- package/skills/benchmark-optimization-loop/SKILL.md +71 -71
- package/skills/blender-motion-state-inspection/SKILL.md +165 -165
- package/skills/blueprint/SKILL.md +106 -106
- package/skills/brand-discovery/SKILL.md +145 -145
- package/skills/brand-discovery/references/10_purpose-why.md +40 -40
- package/skills/brand-discovery/references/20_positioning.md +44 -44
- package/skills/brand-discovery/references/30_audience-niche.md +52 -52
- package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
- package/skills/brand-discovery/references/50_voice-tone.md +59 -59
- package/skills/brand-discovery/references/60_narrative-story.md +50 -50
- package/skills/brand-discovery/references/70_founder-tension.md +49 -49
- package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
- package/skills/brand-voice/SKILL.md +98 -98
- package/skills/brand-voice/references/voice-profile-schema.md +55 -55
- package/skills/browser-qa/SKILL.md +105 -105
- package/skills/bun-runtime/SKILL.md +85 -85
- package/skills/canary-watch/SKILL.md +108 -108
- package/skills/carrier-relationship-management/SKILL.md +212 -212
- package/skills/cisco-ios-patterns/SKILL.md +164 -164
- package/skills/ck/SKILL.md +148 -148
- package/skills/claude-devfleet/SKILL.md +112 -112
- package/skills/click-path-audit/SKILL.md +245 -245
- package/skills/clickhouse-io/SKILL.md +445 -445
- package/skills/code-tour/SKILL.md +254 -254
- package/skills/codebase-onboarding/SKILL.md +234 -234
- package/skills/codehealth-mcp/SKILL.md +167 -167
- package/skills/coding-standards/SKILL.md +551 -551
- package/skills/competitive-platform-analysis/SKILL.md +214 -214
- package/skills/competitive-report-structure/SKILL.md +162 -162
- package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
- package/skills/config-gc/SKILL.md +120 -120
- package/skills/configure-ecc/SKILL.md +385 -385
- package/skills/connections-optimizer/SKILL.md +190 -190
- package/skills/content-engine/SKILL.md +132 -132
- package/skills/content-hash-cache-pattern/SKILL.md +162 -162
- package/skills/context-budget/SKILL.md +136 -136
- package/skills/continuous-agent-loop/SKILL.md +46 -46
- package/skills/continuous-learning/SKILL.md +132 -132
- package/skills/continuous-learning-v2/SKILL.md +361 -361
- package/skills/contract-first/SKILL.md +287 -287
- package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
- package/skills/cost-tracking/SKILL.md +97 -97
- package/skills/council/SKILL.md +204 -204
- package/skills/cpp-coding-standards/SKILL.md +724 -724
- package/skills/cpp-testing/SKILL.md +325 -325
- package/skills/crosspost/SKILL.md +112 -112
- package/skills/csharp-testing/SKILL.md +322 -322
- package/skills/customer-billing-ops/SKILL.md +141 -141
- package/skills/customs-trade-compliance/SKILL.md +263 -263
- package/skills/dart-flutter-patterns/SKILL.md +564 -564
- package/skills/dashboard-builder/SKILL.md +109 -109
- package/skills/data-scraper-agent/SKILL.md +765 -765
- package/skills/data-throughput-accelerator/SKILL.md +74 -74
- package/skills/database-migrations/SKILL.md +430 -430
- package/skills/deep-research/SKILL.md +160 -160
- package/skills/defi-amm-security/SKILL.md +167 -167
- package/skills/delivery-gate/SKILL.md +126 -126
- package/skills/deployment-patterns/SKILL.md +428 -428
- package/skills/design-system/SKILL.md +83 -83
- package/skills/django-celery/SKILL.md +458 -458
- package/skills/django-patterns/SKILL.md +735 -735
- package/skills/django-security/SKILL.md +644 -644
- package/skills/django-tdd/SKILL.md +730 -730
- package/skills/django-verification/SKILL.md +470 -470
- package/skills/dmux-workflows/SKILL.md +192 -192
- package/skills/docker-patterns/SKILL.md +365 -365
- package/skills/documentation-lookup/SKILL.md +91 -91
- package/skills/dotnet-patterns/SKILL.md +322 -322
- package/skills/dynamic-workflow-mode/SKILL.md +124 -124
- package/skills/e2e-testing/SKILL.md +327 -327
- package/skills/ecc-guide/SKILL.md +190 -190
- package/skills/ecc-recipes/SKILL.md +149 -149
- package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
- package/skills/email-ops/SKILL.md +122 -122
- package/skills/energy-procurement/SKILL.md +228 -228
- package/skills/enterprise-agent-ops/SKILL.md +51 -51
- package/skills/error-handling/SKILL.md +377 -377
- package/skills/eval-harness/SKILL.md +271 -271
- package/skills/evm-token-decimals/SKILL.md +131 -131
- package/skills/exa-search/SKILL.md +108 -108
- package/skills/fal-ai-media/SKILL.md +289 -289
- package/skills/fastapi-patterns/SKILL.md +514 -514
- package/skills/finance-billing-ops/SKILL.md +128 -128
- package/skills/flox-environments/SKILL.md +497 -497
- package/skills/flutter-dart-code-review/SKILL.md +436 -436
- package/skills/foundation-models-on-device/SKILL.md +243 -243
- package/skills/frontend-a11y/SKILL.md +446 -446
- package/skills/frontend-design-direction/SKILL.md +93 -93
- package/skills/frontend-patterns/SKILL.md +657 -657
- package/skills/frontend-slides/SKILL.md +185 -185
- package/skills/fsharp-testing/SKILL.md +281 -281
- package/skills/gan-style-harness/SKILL.md +279 -279
- package/skills/gateguard/SKILL.md +133 -133
- package/skills/generating-python-installer/SKILL.md +820 -820
- package/skills/git-workflow/SKILL.md +716 -716
- package/skills/github-ops/SKILL.md +145 -145
- package/skills/golang-patterns/SKILL.md +676 -676
- package/skills/golang-testing/SKILL.md +721 -721
- package/skills/google-workspace-ops/SKILL.md +96 -96
- package/skills/growth-log/SKILL.md +128 -128
- package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
- package/skills/healthcare-emr-patterns/SKILL.md +160 -160
- package/skills/healthcare-eval-harness/SKILL.md +208 -208
- package/skills/healthcare-phi-compliance/SKILL.md +146 -146
- package/skills/hermes-imports/SKILL.md +89 -89
- package/skills/hexagonal-architecture/SKILL.md +277 -277
- package/skills/hipaa-compliance/SKILL.md +79 -79
- package/skills/homelab-network-readiness/SKILL.md +170 -170
- package/skills/homelab-network-setup/SKILL.md +130 -130
- package/skills/homelab-pihole-dns/SKILL.md +275 -275
- package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
- package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
- package/skills/hookify-rules/SKILL.md +128 -128
- package/skills/inherit-legacy-style/SKILL.md +157 -157
- package/skills/intent-driven-development/SKILL.md +360 -360
- package/skills/inventory-demand-planning/SKILL.md +247 -247
- package/skills/investor-materials/SKILL.md +97 -97
- package/skills/investor-outreach/SKILL.md +92 -92
- package/skills/ios-icon-gen/SKILL.md +158 -158
- package/skills/iterative-retrieval/SKILL.md +212 -212
- package/skills/ito-basket-compare/SKILL.md +64 -64
- package/skills/ito-compute/SKILL.md +129 -129
- package/skills/ito-data-atlas-agent/SKILL.md +64 -64
- package/skills/ito-market-intelligence/SKILL.md +61 -61
- package/skills/ito-trade-planner/SKILL.md +68 -68
- package/skills/java-coding-standards/SKILL.md +384 -384
- package/skills/jira-integration/SKILL.md +303 -303
- package/skills/jpa-patterns/SKILL.md +152 -152
- package/skills/knowledge-ops/SKILL.md +155 -155
- package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
- package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
- package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
- package/skills/kotlin-patterns/SKILL.md +712 -712
- package/skills/kotlin-testing/SKILL.md +825 -825
- package/skills/kubernetes-patterns/SKILL.md +756 -756
- package/skills/laravel-patterns/SKILL.md +416 -416
- package/skills/laravel-plugin-discovery/SKILL.md +230 -230
- package/skills/laravel-security/SKILL.md +948 -948
- package/skills/laravel-tdd/SKILL.md +675 -675
- package/skills/laravel-verification/SKILL.md +180 -180
- package/skills/latency-critical-systems/SKILL.md +75 -75
- package/skills/lead-intelligence/SKILL.md +322 -322
- package/skills/liquid-glass-design/SKILL.md +279 -279
- package/skills/llm-trading-agent-security/SKILL.md +147 -147
- package/skills/logistics-exception-management/SKILL.md +222 -222
- package/skills/loop-design-check/SKILL.md +143 -143
- package/skills/mailtrap-email-integration/SKILL.md +77 -77
- package/skills/make-interfaces-feel-better/SKILL.md +152 -152
- package/skills/manim-video/SKILL.md +90 -90
- package/skills/market-research/SKILL.md +76 -76
- package/skills/marketing-campaign/SKILL.md +114 -114
- package/skills/mcp-server-patterns/SKILL.md +70 -70
- package/skills/messages-ops/SKILL.md +105 -105
- package/skills/ml-adoption-playbook/SKILL.md +57 -57
- package/skills/mle-workflow/SKILL.md +348 -348
- package/skills/motion-advanced/SKILL.md +596 -596
- package/skills/motion-foundations/SKILL.md +299 -299
- package/skills/motion-patterns/SKILL.md +434 -434
- package/skills/motion-ui/SKILL.md +576 -576
- package/skills/mysql-patterns/SKILL.md +413 -413
- package/skills/nanoclaw-repl/SKILL.md +34 -34
- package/skills/nestjs-patterns/SKILL.md +231 -231
- package/skills/netmiko-ssh-automation/SKILL.md +174 -174
- package/skills/network-bgp-diagnostics/SKILL.md +168 -168
- package/skills/network-config-validation/SKILL.md +211 -211
- package/skills/network-interface-health/SKILL.md +153 -153
- package/skills/nextjs-turbopack/SKILL.md +58 -58
- package/skills/nodejs-keccak256/SKILL.md +103 -103
- package/skills/nutrient-document-processing/SKILL.md +168 -168
- package/skills/nuxt4-patterns/SKILL.md +101 -101
- package/skills/openclaw-persona-forge/SKILL.md +289 -289
- package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
- package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
- package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
- package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
- package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
- package/skills/openclaw-persona-forge/references/output-template.md +166 -166
- package/skills/opensource-pipeline/SKILL.md +256 -256
- package/skills/orch-add-feature/SKILL.md +45 -45
- package/skills/orch-build-mvp/SKILL.md +49 -49
- package/skills/orch-change-feature/SKILL.md +43 -43
- package/skills/orch-fix-defect/SKILL.md +43 -43
- package/skills/orch-pipeline/SKILL.md +121 -121
- package/skills/orch-refine-code/SKILL.md +44 -44
- package/skills/parallel-execution-optimizer/SKILL.md +74 -74
- package/skills/perl-patterns/SKILL.md +505 -505
- package/skills/perl-security/SKILL.md +504 -504
- package/skills/perl-testing/SKILL.md +476 -476
- package/skills/plan-canvas/SKILL.md +153 -153
- package/skills/plan-orchestrate/SKILL.md +263 -263
- package/skills/plankton-code-quality/SKILL.md +237 -237
- package/skills/postgres-patterns/SKILL.md +148 -148
- package/skills/prediction-market-oracle-research/SKILL.md +64 -64
- package/skills/prediction-market-risk-review/SKILL.md +61 -61
- package/skills/premium-web-design/SKILL.md +154 -154
- package/skills/prisma-patterns/SKILL.md +401 -401
- package/skills/product-capability/SKILL.md +142 -142
- package/skills/product-lens/SKILL.md +93 -93
- package/skills/production-audit/SKILL.md +207 -207
- package/skills/production-scheduling/SKILL.md +238 -238
- package/skills/project-flow-ops/SKILL.md +112 -112
- package/skills/prompt-optimizer/SKILL.md +398 -398
- package/skills/python-patterns/SKILL.md +751 -751
- package/skills/python-testing/SKILL.md +817 -817
- package/skills/pytorch-patterns/SKILL.md +397 -397
- package/skills/quality-nonconformance/SKILL.md +260 -260
- package/skills/quarkus-patterns/SKILL.md +723 -723
- package/skills/quarkus-security/SKILL.md +468 -468
- package/skills/quarkus-tdd/SKILL.md +812 -812
- package/skills/quarkus-verification/SKILL.md +480 -480
- package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
- package/skills/react-native-patterns/SKILL.md +326 -326
- package/skills/react-patterns/SKILL.md +342 -342
- package/skills/react-performance/SKILL.md +575 -575
- package/skills/react-testing/SKILL.md +424 -424
- package/skills/recsys-pipeline-architect/SKILL.md +115 -115
- package/skills/recursive-decision-ledger/SKILL.md +81 -81
- package/skills/redis-patterns/SKILL.md +404 -404
- package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
- package/skills/remotion-video-creation/SKILL.md +43 -43
- package/skills/repo-scan/SKILL.md +79 -79
- package/skills/research-ops/SKILL.md +113 -113
- package/skills/returns-reverse-logistics/SKILL.md +240 -240
- package/skills/rules-distill/SKILL.md +265 -265
- package/skills/rust-patterns/SKILL.md +500 -500
- package/skills/rust-testing/SKILL.md +501 -501
- package/skills/safety-guard/SKILL.md +76 -76
- package/skills/santa-method/SKILL.md +307 -307
- package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
- package/skills/scientific-db-uspto-database/SKILL.md +178 -178
- package/skills/scientific-pkg-gget/SKILL.md +167 -167
- package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
- package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
- package/skills/search-first/SKILL.md +183 -183
- package/skills/security-bounty-hunter/SKILL.md +100 -100
- package/skills/security-review/SKILL.md +504 -504
- package/skills/security-scan/SKILL.md +166 -166
- package/skills/seo/SKILL.md +155 -155
- package/skills/skill-comply/SKILL.md +59 -59
- package/skills/skill-scout/SKILL.md +141 -141
- package/skills/skill-stocktake/SKILL.md +195 -195
- package/skills/social-graph-ranker/SKILL.md +155 -155
- package/skills/social-publisher/SKILL.md +130 -130
- package/skills/springboot-patterns/SKILL.md +315 -315
- package/skills/springboot-security/SKILL.md +273 -273
- package/skills/springboot-tdd/SKILL.md +159 -159
- package/skills/springboot-verification/SKILL.md +232 -232
- package/skills/strategic-compact/SKILL.md +142 -142
- package/skills/swift-actor-persistence/SKILL.md +144 -144
- package/skills/swift-concurrency-6-2/SKILL.md +216 -216
- package/skills/swift-protocol-di-testing/SKILL.md +191 -191
- package/skills/swiftui-patterns/SKILL.md +259 -259
- package/skills/taste/SKILL.md +264 -264
- package/skills/taste/references/genre-taxonomy.md +87 -87
- package/skills/tdd-workflow/SKILL.md +583 -583
- package/skills/team-agent-orchestration/SKILL.md +111 -111
- package/skills/team-builder/SKILL.md +169 -169
- package/skills/terminal-ops/SKILL.md +110 -110
- package/skills/tinystruct-patterns/SKILL.md +279 -279
- package/skills/tinystruct-patterns/references/architecture.md +90 -90
- package/skills/tinystruct-patterns/references/data-handling.md +60 -60
- package/skills/tinystruct-patterns/references/database.md +99 -99
- package/skills/tinystruct-patterns/references/routing.md +64 -64
- package/skills/tinystruct-patterns/references/system-usage.md +97 -97
- package/skills/tinystruct-patterns/references/testing.md +72 -72
- package/skills/token-budget-advisor/SKILL.md +134 -134
- package/skills/ui-demo/SKILL.md +466 -466
- package/skills/ui-to-vue/SKILL.md +135 -135
- package/skills/uncloud/SKILL.md +344 -344
- package/skills/unified-memory/SKILL.md +170 -170
- package/skills/unified-notifications-ops/SKILL.md +188 -188
- package/skills/verification-loop/SKILL.md +129 -129
- package/skills/video-editing/SKILL.md +311 -311
- package/skills/videodb/SKILL.md +375 -375
- package/skills/visa-doc-translate/SKILL.md +117 -117
- package/skills/vite-patterns/SKILL.md +450 -450
- package/skills/vue-patterns/SKILL.md +471 -471
- package/skills/windows-desktop-e2e/SKILL.md +888 -888
- package/skills/workspace-surface-audit/SKILL.md +126 -126
- package/skills/x-api/SKILL.md +235 -235
package/lib/typecheck-worker.js
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type-checks single files with the project's own TypeScript, in a separate
|
|
3
|
-
* process so a slow first build never blocks the app. One worker per project
|
|
4
|
-
* root; it keeps a LanguageService warm, so after the first file each check
|
|
5
|
-
* only re-reads what changed.
|
|
6
|
-
*
|
|
7
|
-
* Messages in: { id, file } Messages out: { id, problems: string[] } | { id, error }
|
|
8
|
-
* Started with: argv[2] = project root (folder with tsconfig/jsconfig), argv[3] = typescript module path
|
|
9
|
-
*/
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
|
|
13
|
-
const root = process.argv[2];
|
|
14
|
-
const ts = require(process.argv[3]);
|
|
15
|
-
const MAX = 10;
|
|
16
|
-
|
|
17
|
-
const configPath = ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json') || ts.findConfigFile(root, ts.sys.fileExists, 'jsconfig.json');
|
|
18
|
-
let options = { allowJs: true, noEmit: true, skipLibCheck: true };
|
|
19
|
-
let fileNames = [];
|
|
20
|
-
if (configPath) {
|
|
21
|
-
const cfg = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
22
|
-
const parsed = ts.parseJsonConfigFileContent(cfg.config || {}, ts.sys, path.dirname(configPath));
|
|
23
|
-
options = { ...parsed.options, noEmit: true };
|
|
24
|
-
fileNames = parsed.fileNames;
|
|
25
|
-
}
|
|
26
|
-
const extra = new Set(); // files checked that the config doesn't list
|
|
27
|
-
|
|
28
|
-
const versions = new Map();
|
|
29
|
-
const version = (f) => {
|
|
30
|
-
try { return String(fs.statSync(f).mtimeMs); } catch { return '0'; }
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const host = {
|
|
34
|
-
getScriptFileNames: () => [...fileNames, ...extra],
|
|
35
|
-
getScriptVersion: (f) => version(f),
|
|
36
|
-
getScriptSnapshot: (f) => {
|
|
37
|
-
if (!fs.existsSync(f)) return undefined;
|
|
38
|
-
return ts.ScriptSnapshot.fromString(fs.readFileSync(f, 'utf8'));
|
|
39
|
-
},
|
|
40
|
-
getCurrentDirectory: () => root,
|
|
41
|
-
getCompilationSettings: () => options,
|
|
42
|
-
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
|
43
|
-
fileExists: ts.sys.fileExists,
|
|
44
|
-
readFile: ts.sys.readFile,
|
|
45
|
-
readDirectory: ts.sys.readDirectory,
|
|
46
|
-
directoryExists: ts.sys.directoryExists,
|
|
47
|
-
getDirectories: ts.sys.getDirectories,
|
|
48
|
-
};
|
|
49
|
-
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
|
50
|
-
|
|
51
|
-
function check(file) {
|
|
52
|
-
const abs = path.resolve(file);
|
|
53
|
-
const norm = abs.replace(/\\/g, '/');
|
|
54
|
-
if (!fileNames.some((f) => path.resolve(f) === abs)) extra.add(norm);
|
|
55
|
-
versions.set(abs, version(abs));
|
|
56
|
-
const diags = [...service.getSyntacticDiagnostics(norm), ...service.getSemanticDiagnostics(norm)]
|
|
57
|
-
.filter((d) => d.category === ts.DiagnosticCategory.Error);
|
|
58
|
-
return diags.slice(0, MAX).map((d) => {
|
|
59
|
-
const line = d.file && d.start != null ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null;
|
|
60
|
-
return `${line ? `line ${line}: ` : ''}TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`;
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── Code navigation (the lsp tool): definition, references, hover, symbols ──
|
|
65
|
-
const rel = (f) => path.relative(root, f).replace(/\\/g, '/') || f;
|
|
66
|
-
function place(fileName, start) {
|
|
67
|
-
const sf = service.getProgram().getSourceFile(fileName);
|
|
68
|
-
if (!sf) return `${rel(fileName)}`;
|
|
69
|
-
const { line, character } = sf.getLineAndCharacterOfPosition(start);
|
|
70
|
-
return `${rel(fileName)}:${line + 1}:${character + 1}`;
|
|
71
|
-
}
|
|
72
|
-
function lineText(fileName, start) {
|
|
73
|
-
const sf = service.getProgram().getSourceFile(fileName);
|
|
74
|
-
if (!sf) return '';
|
|
75
|
-
const { line } = sf.getLineAndCharacterOfPosition(start);
|
|
76
|
-
return sf.text.split('\n')[line].trim().slice(0, 160);
|
|
77
|
-
}
|
|
78
|
-
let navFile = null;
|
|
79
|
-
function flattenTree(node, out, depth = 0) {
|
|
80
|
-
for (const c of node.childItems || []) {
|
|
81
|
-
const line = c.spans[0] ? navFile.getLineAndCharacterOfPosition(c.spans[0].start).line + 1 : '?';
|
|
82
|
-
if (c.kind !== 'alias') out.push(`${' '.repeat(depth)}${c.kind} ${c.text} (line ${line})`);
|
|
83
|
-
flattenTree(c, out, depth + 1);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function navigate(msg) {
|
|
88
|
-
const abs = path.resolve(msg.file || root);
|
|
89
|
-
const norm = abs.replace(/\\/g, '/');
|
|
90
|
-
if (msg.file && fs.existsSync(abs) && !fileNames.some((f) => path.resolve(f) === abs)) extra.add(norm);
|
|
91
|
-
const op = msg.op;
|
|
92
|
-
if (op === 'workspaceSymbol') {
|
|
93
|
-
const items = (service.getNavigateToItems(msg.query || '', 200) || [])
|
|
94
|
-
.filter((i) => !/[\\/]node_modules[\\/]/.test(i.fileName) && !/[\\/]typescript[\\/]lib[\\/]/.test(i.fileName))
|
|
95
|
-
.slice(0, 40);
|
|
96
|
-
return items.map((i) => `${i.kind} ${i.name} ${place(i.fileName, i.textSpan.start)}`).join('\n') || 'No symbols match.';
|
|
97
|
-
}
|
|
98
|
-
const sf = service.getProgram().getSourceFile(norm);
|
|
99
|
-
if (!sf) return `${msg.file} is not part of a TypeScript/JavaScript project this checker can see.`;
|
|
100
|
-
if (op === 'documentSymbol') {
|
|
101
|
-
navFile = sf;
|
|
102
|
-
const out = [];
|
|
103
|
-
flattenTree(service.getNavigationTree(norm), out);
|
|
104
|
-
return out.join('\n') || 'No symbols in this file.';
|
|
105
|
-
}
|
|
106
|
-
// Position-based: 1-based line/character from the caller.
|
|
107
|
-
const lines = sf.text.split('\n');
|
|
108
|
-
const li = Math.min(Math.max((msg.line || 1) - 1, 0), lines.length - 1);
|
|
109
|
-
let col = msg.character ? msg.character - 1 : -1;
|
|
110
|
-
if (col < 0 && msg.symbol) {
|
|
111
|
-
const at = lines[li].indexOf(msg.symbol);
|
|
112
|
-
col = at >= 0 ? at : -1;
|
|
113
|
-
if (col < 0) {
|
|
114
|
-
const whole = new RegExp(`\\b${msg.symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).exec(sf.text);
|
|
115
|
-
if (whole) return navigateAt(op, norm, whole.index);
|
|
116
|
-
return `Could not find "${msg.symbol}" in ${msg.file}.`;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (col < 0) col = Math.max(0, lines[li].search(/\S/));
|
|
120
|
-
const pos = sf.getPositionOfLineAndCharacter(li, col);
|
|
121
|
-
return navigateAt(op, norm, pos);
|
|
122
|
-
}
|
|
123
|
-
function navigateAt(op, norm, pos) {
|
|
124
|
-
if (op === 'hover') {
|
|
125
|
-
const info = service.getQuickInfoAtPosition(norm, pos);
|
|
126
|
-
if (!info) return 'No type information at that position.';
|
|
127
|
-
const sig = ts.displayPartsToString(info.displayParts);
|
|
128
|
-
const doc = ts.displayPartsToString(info.documentation);
|
|
129
|
-
return doc ? `${sig}\n\n${doc}` : sig;
|
|
130
|
-
}
|
|
131
|
-
let locs;
|
|
132
|
-
if (op === 'definition') locs = service.getDefinitionAtPosition(norm, pos) || [];
|
|
133
|
-
else if (op === 'implementation') locs = service.getImplementationAtPosition(norm, pos) || [];
|
|
134
|
-
else if (op === 'references') locs = service.getReferencesAtPosition(norm, pos) || [];
|
|
135
|
-
else return `Unknown operation "${op}".`;
|
|
136
|
-
if (!locs.length) return `No ${op} found at that position.`;
|
|
137
|
-
const shown = locs.slice(0, 40).map((l) => `${place(l.fileName, l.textSpan.start)} ${lineText(l.fileName, l.textSpan.start)}`);
|
|
138
|
-
return `${locs.length} ${op === 'references' ? 'reference' : 'location'}${locs.length === 1 ? '' : 's'}${locs.length > 40 ? ' (first 40)' : ''}:\n${shown.join('\n')}`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
process.on('message', (msg) => {
|
|
142
|
-
try {
|
|
143
|
-
if (msg.op) process.send({ id: msg.id, text: navigate(msg) });
|
|
144
|
-
else process.send({ id: msg.id, problems: check(msg.file) });
|
|
145
|
-
} catch (e) { process.send({ id: msg.id, error: e.message }); }
|
|
146
|
-
});
|
|
147
|
-
process.send({ ready: true, hasConfig: !!configPath });
|
|
1
|
+
/**
|
|
2
|
+
* Type-checks single files with the project's own TypeScript, in a separate
|
|
3
|
+
* process so a slow first build never blocks the app. One worker per project
|
|
4
|
+
* root; it keeps a LanguageService warm, so after the first file each check
|
|
5
|
+
* only re-reads what changed.
|
|
6
|
+
*
|
|
7
|
+
* Messages in: { id, file } Messages out: { id, problems: string[] } | { id, error }
|
|
8
|
+
* Started with: argv[2] = project root (folder with tsconfig/jsconfig), argv[3] = typescript module path
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const root = process.argv[2];
|
|
14
|
+
const ts = require(process.argv[3]);
|
|
15
|
+
const MAX = 10;
|
|
16
|
+
|
|
17
|
+
const configPath = ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json') || ts.findConfigFile(root, ts.sys.fileExists, 'jsconfig.json');
|
|
18
|
+
let options = { allowJs: true, noEmit: true, skipLibCheck: true };
|
|
19
|
+
let fileNames = [];
|
|
20
|
+
if (configPath) {
|
|
21
|
+
const cfg = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
22
|
+
const parsed = ts.parseJsonConfigFileContent(cfg.config || {}, ts.sys, path.dirname(configPath));
|
|
23
|
+
options = { ...parsed.options, noEmit: true };
|
|
24
|
+
fileNames = parsed.fileNames;
|
|
25
|
+
}
|
|
26
|
+
const extra = new Set(); // files checked that the config doesn't list
|
|
27
|
+
|
|
28
|
+
const versions = new Map();
|
|
29
|
+
const version = (f) => {
|
|
30
|
+
try { return String(fs.statSync(f).mtimeMs); } catch { return '0'; }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const host = {
|
|
34
|
+
getScriptFileNames: () => [...fileNames, ...extra],
|
|
35
|
+
getScriptVersion: (f) => version(f),
|
|
36
|
+
getScriptSnapshot: (f) => {
|
|
37
|
+
if (!fs.existsSync(f)) return undefined;
|
|
38
|
+
return ts.ScriptSnapshot.fromString(fs.readFileSync(f, 'utf8'));
|
|
39
|
+
},
|
|
40
|
+
getCurrentDirectory: () => root,
|
|
41
|
+
getCompilationSettings: () => options,
|
|
42
|
+
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
|
43
|
+
fileExists: ts.sys.fileExists,
|
|
44
|
+
readFile: ts.sys.readFile,
|
|
45
|
+
readDirectory: ts.sys.readDirectory,
|
|
46
|
+
directoryExists: ts.sys.directoryExists,
|
|
47
|
+
getDirectories: ts.sys.getDirectories,
|
|
48
|
+
};
|
|
49
|
+
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
|
50
|
+
|
|
51
|
+
function check(file) {
|
|
52
|
+
const abs = path.resolve(file);
|
|
53
|
+
const norm = abs.replace(/\\/g, '/');
|
|
54
|
+
if (!fileNames.some((f) => path.resolve(f) === abs)) extra.add(norm);
|
|
55
|
+
versions.set(abs, version(abs));
|
|
56
|
+
const diags = [...service.getSyntacticDiagnostics(norm), ...service.getSemanticDiagnostics(norm)]
|
|
57
|
+
.filter((d) => d.category === ts.DiagnosticCategory.Error);
|
|
58
|
+
return diags.slice(0, MAX).map((d) => {
|
|
59
|
+
const line = d.file && d.start != null ? d.file.getLineAndCharacterOfPosition(d.start).line + 1 : null;
|
|
60
|
+
return `${line ? `line ${line}: ` : ''}TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, ' ')}`;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Code navigation (the lsp tool): definition, references, hover, symbols ──
|
|
65
|
+
const rel = (f) => path.relative(root, f).replace(/\\/g, '/') || f;
|
|
66
|
+
function place(fileName, start) {
|
|
67
|
+
const sf = service.getProgram().getSourceFile(fileName);
|
|
68
|
+
if (!sf) return `${rel(fileName)}`;
|
|
69
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(start);
|
|
70
|
+
return `${rel(fileName)}:${line + 1}:${character + 1}`;
|
|
71
|
+
}
|
|
72
|
+
function lineText(fileName, start) {
|
|
73
|
+
const sf = service.getProgram().getSourceFile(fileName);
|
|
74
|
+
if (!sf) return '';
|
|
75
|
+
const { line } = sf.getLineAndCharacterOfPosition(start);
|
|
76
|
+
return sf.text.split('\n')[line].trim().slice(0, 160);
|
|
77
|
+
}
|
|
78
|
+
let navFile = null;
|
|
79
|
+
function flattenTree(node, out, depth = 0) {
|
|
80
|
+
for (const c of node.childItems || []) {
|
|
81
|
+
const line = c.spans[0] ? navFile.getLineAndCharacterOfPosition(c.spans[0].start).line + 1 : '?';
|
|
82
|
+
if (c.kind !== 'alias') out.push(`${' '.repeat(depth)}${c.kind} ${c.text} (line ${line})`);
|
|
83
|
+
flattenTree(c, out, depth + 1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function navigate(msg) {
|
|
88
|
+
const abs = path.resolve(msg.file || root);
|
|
89
|
+
const norm = abs.replace(/\\/g, '/');
|
|
90
|
+
if (msg.file && fs.existsSync(abs) && !fileNames.some((f) => path.resolve(f) === abs)) extra.add(norm);
|
|
91
|
+
const op = msg.op;
|
|
92
|
+
if (op === 'workspaceSymbol') {
|
|
93
|
+
const items = (service.getNavigateToItems(msg.query || '', 200) || [])
|
|
94
|
+
.filter((i) => !/[\\/]node_modules[\\/]/.test(i.fileName) && !/[\\/]typescript[\\/]lib[\\/]/.test(i.fileName))
|
|
95
|
+
.slice(0, 40);
|
|
96
|
+
return items.map((i) => `${i.kind} ${i.name} ${place(i.fileName, i.textSpan.start)}`).join('\n') || 'No symbols match.';
|
|
97
|
+
}
|
|
98
|
+
const sf = service.getProgram().getSourceFile(norm);
|
|
99
|
+
if (!sf) return `${msg.file} is not part of a TypeScript/JavaScript project this checker can see.`;
|
|
100
|
+
if (op === 'documentSymbol') {
|
|
101
|
+
navFile = sf;
|
|
102
|
+
const out = [];
|
|
103
|
+
flattenTree(service.getNavigationTree(norm), out);
|
|
104
|
+
return out.join('\n') || 'No symbols in this file.';
|
|
105
|
+
}
|
|
106
|
+
// Position-based: 1-based line/character from the caller.
|
|
107
|
+
const lines = sf.text.split('\n');
|
|
108
|
+
const li = Math.min(Math.max((msg.line || 1) - 1, 0), lines.length - 1);
|
|
109
|
+
let col = msg.character ? msg.character - 1 : -1;
|
|
110
|
+
if (col < 0 && msg.symbol) {
|
|
111
|
+
const at = lines[li].indexOf(msg.symbol);
|
|
112
|
+
col = at >= 0 ? at : -1;
|
|
113
|
+
if (col < 0) {
|
|
114
|
+
const whole = new RegExp(`\\b${msg.symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).exec(sf.text);
|
|
115
|
+
if (whole) return navigateAt(op, norm, whole.index);
|
|
116
|
+
return `Could not find "${msg.symbol}" in ${msg.file}.`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (col < 0) col = Math.max(0, lines[li].search(/\S/));
|
|
120
|
+
const pos = sf.getPositionOfLineAndCharacter(li, col);
|
|
121
|
+
return navigateAt(op, norm, pos);
|
|
122
|
+
}
|
|
123
|
+
function navigateAt(op, norm, pos) {
|
|
124
|
+
if (op === 'hover') {
|
|
125
|
+
const info = service.getQuickInfoAtPosition(norm, pos);
|
|
126
|
+
if (!info) return 'No type information at that position.';
|
|
127
|
+
const sig = ts.displayPartsToString(info.displayParts);
|
|
128
|
+
const doc = ts.displayPartsToString(info.documentation);
|
|
129
|
+
return doc ? `${sig}\n\n${doc}` : sig;
|
|
130
|
+
}
|
|
131
|
+
let locs;
|
|
132
|
+
if (op === 'definition') locs = service.getDefinitionAtPosition(norm, pos) || [];
|
|
133
|
+
else if (op === 'implementation') locs = service.getImplementationAtPosition(norm, pos) || [];
|
|
134
|
+
else if (op === 'references') locs = service.getReferencesAtPosition(norm, pos) || [];
|
|
135
|
+
else return `Unknown operation "${op}".`;
|
|
136
|
+
if (!locs.length) return `No ${op} found at that position.`;
|
|
137
|
+
const shown = locs.slice(0, 40).map((l) => `${place(l.fileName, l.textSpan.start)} ${lineText(l.fileName, l.textSpan.start)}`);
|
|
138
|
+
return `${locs.length} ${op === 'references' ? 'reference' : 'location'}${locs.length === 1 ? '' : 's'}${locs.length > 40 ? ' (first 40)' : ''}:\n${shown.join('\n')}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
process.on('message', (msg) => {
|
|
142
|
+
try {
|
|
143
|
+
if (msg.op) process.send({ id: msg.id, text: navigate(msg) });
|
|
144
|
+
else process.send({ id: msg.id, problems: check(msg.file) });
|
|
145
|
+
} catch (e) { process.send({ id: msg.id, error: e.message }); }
|
|
146
|
+
});
|
|
147
|
+
process.send({ ready: true, hasConfig: !!configPath });
|
package/lib/web-tools.js
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web access for the agent: fetch a page as readable text, and search the web.
|
|
3
|
-
*
|
|
4
|
-
* Search uses Exa's public MCP endpoint (the same one opencode's websearch tool
|
|
5
|
-
* talks to; no key needed) and falls back to DuckDuckGo's HTML results when
|
|
6
|
-
* Exa is unreachable. Fetching converts HTML to plain markdown-ish text so the
|
|
7
|
-
* model gets the content, not the markup.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 Codeply';
|
|
11
|
-
const MAX_BYTES = 5 * 1024 * 1024;
|
|
12
|
-
const FETCH_TIMEOUT_MS = 30000;
|
|
13
|
-
const SEARCH_TIMEOUT_MS = 25000;
|
|
14
|
-
|
|
15
|
-
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', ndash: '-', mdash: '-', hellip: '...', rsquo: "'", lsquo: "'", rdquo: '"', ldquo: '"', copy: '(c)' };
|
|
16
|
-
|
|
17
|
-
function decodeEntities(s) {
|
|
18
|
-
return s
|
|
19
|
-
.replace(/&#(\d+);/g, (_, n) => { try { return String.fromCodePoint(Number(n)); } catch { return ''; } })
|
|
20
|
-
.replace(/&#x([0-9a-f]+);/gi, (_, n) => { try { return String.fromCodePoint(parseInt(n, 16)); } catch { return ''; } })
|
|
21
|
-
.replace(/&([a-z]+);/gi, (m, n) => (ENTITIES[n.toLowerCase()] ?? m));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** HTML -> readable text: headings, links, lists, code and paragraphs survive; the rest goes. */
|
|
25
|
-
function htmlToText(html, baseUrl) {
|
|
26
|
-
let s = String(html);
|
|
27
|
-
s = s.replace(/<!--[\s\S]*?-->/g, '');
|
|
28
|
-
s = s.replace(/<(script|style|noscript|template|svg|iframe|head)\b[\s\S]*?<\/\1>/gi, '');
|
|
29
|
-
s = s.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => '\n```\n' + code.replace(/<[^>]+>/g, '') + '\n```\n');
|
|
30
|
-
s = s.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_, c) => '`' + c.replace(/<[^>]+>/g, '') + '`');
|
|
31
|
-
s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_, n, t) => `\n\n${'#'.repeat(Number(n))} ${t.replace(/<[^>]+>/g, '').trim()}\n\n`);
|
|
32
|
-
s = s.replace(/<a\b[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, t) => {
|
|
33
|
-
const label = t.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
34
|
-
if (!label) return '';
|
|
35
|
-
if (!href || href.startsWith('#') || /^(javascript|mailto):/i.test(href)) return label;
|
|
36
|
-
let abs = href;
|
|
37
|
-
try { abs = new URL(href, baseUrl).href; } catch {}
|
|
38
|
-
return `[${label}](${abs})`;
|
|
39
|
-
});
|
|
40
|
-
s = s.replace(/<li\b[^>]*>/gi, '\n- ');
|
|
41
|
-
s = s.replace(/<br\s*\/?>/gi, '\n');
|
|
42
|
-
s = s.replace(/<\/(p|div|section|article|header|footer|main|nav|ul|ol|table|tr|blockquote|figure)>/gi, '\n');
|
|
43
|
-
s = s.replace(/<\/t[dh]>/gi, ' | ');
|
|
44
|
-
s = s.replace(/<[^>]+>/g, '');
|
|
45
|
-
s = decodeEntities(s);
|
|
46
|
-
s = s.replace(/[ \t\f\v]+/g, ' ').replace(/ *\n */g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
47
|
-
return s.trim();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {string} url
|
|
52
|
-
* @param {{format?:'text'|'html'|'raw', signal?:AbortSignal, maxChars?:number}} [opts]
|
|
53
|
-
* @returns {Promise<{ok:boolean, text?:string, error?:string, status?:number, contentType?:string, finalUrl?:string}>}
|
|
54
|
-
*/
|
|
55
|
-
async function fetchPage(url, opts = {}) {
|
|
56
|
-
let parsed;
|
|
57
|
-
try { parsed = new URL(url); } catch { return { ok: false, error: `Not a valid URL: ${url}` }; }
|
|
58
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return { ok: false, error: 'web_fetch only supports http:// and https:// URLs.' };
|
|
59
|
-
const signals = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
|
|
60
|
-
if (opts.signal) signals.push(opts.signal);
|
|
61
|
-
let res;
|
|
62
|
-
try {
|
|
63
|
-
res = await fetch(parsed.href, {
|
|
64
|
-
redirect: 'follow',
|
|
65
|
-
signal: AbortSignal.any(signals),
|
|
66
|
-
headers: { 'User-Agent': UA, Accept: 'text/html,application/xhtml+xml,application/json,text/plain,text/markdown;q=0.9,*/*;q=0.5', 'Accept-Language': 'en-US,en;q=0.9' },
|
|
67
|
-
});
|
|
68
|
-
} catch (e) {
|
|
69
|
-
return { ok: false, error: `Could not reach ${parsed.href}: ${e.name === 'TimeoutError' ? 'timed out' : e.message}` };
|
|
70
|
-
}
|
|
71
|
-
if (!res.ok) return { ok: false, status: res.status, error: `${parsed.href} returned HTTP ${res.status}.` };
|
|
72
|
-
const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
73
|
-
const declared = Number(res.headers.get('content-length') || 0);
|
|
74
|
-
if (declared > MAX_BYTES) return { ok: false, error: `The page is ${(declared / 1e6).toFixed(1)}MB, over the ${MAX_BYTES / 1e6}MB limit.` };
|
|
75
|
-
if (/^(image|video|audio)\//.test(contentType) || /^application\/(zip|octet-stream|pdf|gzip)/.test(contentType)) {
|
|
76
|
-
return { ok: false, contentType, error: `${parsed.href} is ${contentType}, not text. Use fetch_image for images or run curl to download a file.` };
|
|
77
|
-
}
|
|
78
|
-
let buf;
|
|
79
|
-
try { buf = Buffer.from(await res.arrayBuffer()); } catch (e) { return { ok: false, error: `Failed reading ${parsed.href}: ${e.message}` }; }
|
|
80
|
-
if (buf.length > MAX_BYTES) return { ok: false, error: `The page is over the ${MAX_BYTES / 1e6}MB limit.` };
|
|
81
|
-
const body = buf.toString('utf8');
|
|
82
|
-
const format = opts.format || 'text';
|
|
83
|
-
const isHtml = contentType.includes('html') || (!contentType && /^\s*<(!doctype|html)/i.test(body));
|
|
84
|
-
let text = body;
|
|
85
|
-
if (isHtml && format === 'text') text = htmlToText(body, res.url || parsed.href);
|
|
86
|
-
else if (contentType.includes('json') && format !== 'raw') {
|
|
87
|
-
try { text = JSON.stringify(JSON.parse(body), null, 2); } catch {}
|
|
88
|
-
}
|
|
89
|
-
return { ok: true, text, contentType, finalUrl: res.url || parsed.href, status: res.status };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
async function exaSearch(query, num, signal) {
|
|
95
|
-
const signals = [AbortSignal.timeout(SEARCH_TIMEOUT_MS)];
|
|
96
|
-
if (signal) signals.push(signal);
|
|
97
|
-
const res = await fetch('https://mcp.exa.ai/mcp', {
|
|
98
|
-
method: 'POST',
|
|
99
|
-
signal: AbortSignal.any(signals),
|
|
100
|
-
headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
|
|
101
|
-
body: JSON.stringify({
|
|
102
|
-
jsonrpc: '2.0', id: 1, method: 'tools/call',
|
|
103
|
-
params: { name: 'web_search_exa', arguments: { query, numResults: num, type: 'auto', livecrawl: 'fallback' } },
|
|
104
|
-
}),
|
|
105
|
-
});
|
|
106
|
-
if (!res.ok) throw new Error(`Exa returned HTTP ${res.status}`);
|
|
107
|
-
const raw = await res.text();
|
|
108
|
-
// The reply is either plain JSON or an SSE stream of "data: {...}" lines.
|
|
109
|
-
const payloads = [];
|
|
110
|
-
if (raw.trimStart().startsWith('{')) payloads.push(raw);
|
|
111
|
-
else for (const line of raw.split('\n')) if (line.startsWith('data:')) payloads.push(line.slice(5).trim());
|
|
112
|
-
for (const p of payloads) {
|
|
113
|
-
let j;
|
|
114
|
-
try { j = JSON.parse(p); } catch { continue; }
|
|
115
|
-
if (j.error) throw new Error(j.error.message || 'Exa error');
|
|
116
|
-
const text = (j.result?.content || []).filter((c) => c.type === 'text').map((c) => c.text).join('\n').trim();
|
|
117
|
-
if (text) return text;
|
|
118
|
-
}
|
|
119
|
-
throw new Error('Exa returned no results');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function ddgSearch(query, num, signal) {
|
|
123
|
-
const signals = [AbortSignal.timeout(SEARCH_TIMEOUT_MS)];
|
|
124
|
-
if (signal) signals.push(signal);
|
|
125
|
-
const res = await fetch('https://html.duckduckgo.com/html/', {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
signal: AbortSignal.any(signals),
|
|
128
|
-
headers: { 'User-Agent': UA, 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
129
|
-
body: `q=${encodeURIComponent(query)}`,
|
|
130
|
-
});
|
|
131
|
-
if (!res.ok) throw new Error(`DuckDuckGo returned HTTP ${res.status}`);
|
|
132
|
-
const html = await res.text();
|
|
133
|
-
const out = [];
|
|
134
|
-
const re = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>)?/gi;
|
|
135
|
-
let m;
|
|
136
|
-
while ((m = re.exec(html)) && out.length < num) {
|
|
137
|
-
let href = decodeEntities(m[1]);
|
|
138
|
-
const u = /[?&]uddg=([^&]+)/.exec(href);
|
|
139
|
-
if (u) { try { href = decodeURIComponent(u[1]); } catch {} }
|
|
140
|
-
const title = decodeEntities(m[2].replace(/<[^>]+>/g, '')).trim();
|
|
141
|
-
const snippet = decodeEntities((m[3] || '').replace(/<[^>]+>/g, '')).trim();
|
|
142
|
-
if (title && /^https?:/.test(href)) out.push(`Title: ${title}\nURL: ${href}${snippet ? `\n${snippet}` : ''}`);
|
|
143
|
-
}
|
|
144
|
-
if (!out.length) throw new Error('DuckDuckGo returned no results');
|
|
145
|
-
return out.join('\n\n');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** @returns {Promise<{ok:boolean, text?:string, error?:string, via?:string}>} */
|
|
149
|
-
async function webSearch(query, opts = {}) {
|
|
150
|
-
const num = Math.min(Math.max(Number(opts.num) || 8, 1), 20);
|
|
151
|
-
const errors = [];
|
|
152
|
-
try { return { ok: true, text: await exaSearch(query, num, opts.signal), via: 'exa' }; } catch (e) { errors.push(e.message); }
|
|
153
|
-
if (opts.signal?.aborted) return { ok: false, error: 'Search cancelled.' };
|
|
154
|
-
try { return { ok: true, text: await ddgSearch(query, num, opts.signal), via: 'duckduckgo' }; } catch (e) { errors.push(e.message); }
|
|
155
|
-
return { ok: false, error: `Web search failed (${errors.join('; ')}).` };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
module.exports = { fetchPage, webSearch, htmlToText };
|
|
1
|
+
/**
|
|
2
|
+
* Web access for the agent: fetch a page as readable text, and search the web.
|
|
3
|
+
*
|
|
4
|
+
* Search uses Exa's public MCP endpoint (the same one opencode's websearch tool
|
|
5
|
+
* talks to; no key needed) and falls back to DuckDuckGo's HTML results when
|
|
6
|
+
* Exa is unreachable. Fetching converts HTML to plain markdown-ish text so the
|
|
7
|
+
* model gets the content, not the markup.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36 Codeply';
|
|
11
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
const FETCH_TIMEOUT_MS = 30000;
|
|
13
|
+
const SEARCH_TIMEOUT_MS = 25000;
|
|
14
|
+
|
|
15
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', ndash: '-', mdash: '-', hellip: '...', rsquo: "'", lsquo: "'", rdquo: '"', ldquo: '"', copy: '(c)' };
|
|
16
|
+
|
|
17
|
+
function decodeEntities(s) {
|
|
18
|
+
return s
|
|
19
|
+
.replace(/&#(\d+);/g, (_, n) => { try { return String.fromCodePoint(Number(n)); } catch { return ''; } })
|
|
20
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, n) => { try { return String.fromCodePoint(parseInt(n, 16)); } catch { return ''; } })
|
|
21
|
+
.replace(/&([a-z]+);/gi, (m, n) => (ENTITIES[n.toLowerCase()] ?? m));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** HTML -> readable text: headings, links, lists, code and paragraphs survive; the rest goes. */
|
|
25
|
+
function htmlToText(html, baseUrl) {
|
|
26
|
+
let s = String(html);
|
|
27
|
+
s = s.replace(/<!--[\s\S]*?-->/g, '');
|
|
28
|
+
s = s.replace(/<(script|style|noscript|template|svg|iframe|head)\b[\s\S]*?<\/\1>/gi, '');
|
|
29
|
+
s = s.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => '\n```\n' + code.replace(/<[^>]+>/g, '') + '\n```\n');
|
|
30
|
+
s = s.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_, c) => '`' + c.replace(/<[^>]+>/g, '') + '`');
|
|
31
|
+
s = s.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/gi, (_, n, t) => `\n\n${'#'.repeat(Number(n))} ${t.replace(/<[^>]+>/g, '').trim()}\n\n`);
|
|
32
|
+
s = s.replace(/<a\b[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, t) => {
|
|
33
|
+
const label = t.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
34
|
+
if (!label) return '';
|
|
35
|
+
if (!href || href.startsWith('#') || /^(javascript|mailto):/i.test(href)) return label;
|
|
36
|
+
let abs = href;
|
|
37
|
+
try { abs = new URL(href, baseUrl).href; } catch {}
|
|
38
|
+
return `[${label}](${abs})`;
|
|
39
|
+
});
|
|
40
|
+
s = s.replace(/<li\b[^>]*>/gi, '\n- ');
|
|
41
|
+
s = s.replace(/<br\s*\/?>/gi, '\n');
|
|
42
|
+
s = s.replace(/<\/(p|div|section|article|header|footer|main|nav|ul|ol|table|tr|blockquote|figure)>/gi, '\n');
|
|
43
|
+
s = s.replace(/<\/t[dh]>/gi, ' | ');
|
|
44
|
+
s = s.replace(/<[^>]+>/g, '');
|
|
45
|
+
s = decodeEntities(s);
|
|
46
|
+
s = s.replace(/[ \t\f\v]+/g, ' ').replace(/ *\n */g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
47
|
+
return s.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} url
|
|
52
|
+
* @param {{format?:'text'|'html'|'raw', signal?:AbortSignal, maxChars?:number}} [opts]
|
|
53
|
+
* @returns {Promise<{ok:boolean, text?:string, error?:string, status?:number, contentType?:string, finalUrl?:string}>}
|
|
54
|
+
*/
|
|
55
|
+
async function fetchPage(url, opts = {}) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try { parsed = new URL(url); } catch { return { ok: false, error: `Not a valid URL: ${url}` }; }
|
|
58
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return { ok: false, error: 'web_fetch only supports http:// and https:// URLs.' };
|
|
59
|
+
const signals = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
|
|
60
|
+
if (opts.signal) signals.push(opts.signal);
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await fetch(parsed.href, {
|
|
64
|
+
redirect: 'follow',
|
|
65
|
+
signal: AbortSignal.any(signals),
|
|
66
|
+
headers: { 'User-Agent': UA, Accept: 'text/html,application/xhtml+xml,application/json,text/plain,text/markdown;q=0.9,*/*;q=0.5', 'Accept-Language': 'en-US,en;q=0.9' },
|
|
67
|
+
});
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { ok: false, error: `Could not reach ${parsed.href}: ${e.name === 'TimeoutError' ? 'timed out' : e.message}` };
|
|
70
|
+
}
|
|
71
|
+
if (!res.ok) return { ok: false, status: res.status, error: `${parsed.href} returned HTTP ${res.status}.` };
|
|
72
|
+
const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
73
|
+
const declared = Number(res.headers.get('content-length') || 0);
|
|
74
|
+
if (declared > MAX_BYTES) return { ok: false, error: `The page is ${(declared / 1e6).toFixed(1)}MB, over the ${MAX_BYTES / 1e6}MB limit.` };
|
|
75
|
+
if (/^(image|video|audio)\//.test(contentType) || /^application\/(zip|octet-stream|pdf|gzip)/.test(contentType)) {
|
|
76
|
+
return { ok: false, contentType, error: `${parsed.href} is ${contentType}, not text. Use fetch_image for images or run curl to download a file.` };
|
|
77
|
+
}
|
|
78
|
+
let buf;
|
|
79
|
+
try { buf = Buffer.from(await res.arrayBuffer()); } catch (e) { return { ok: false, error: `Failed reading ${parsed.href}: ${e.message}` }; }
|
|
80
|
+
if (buf.length > MAX_BYTES) return { ok: false, error: `The page is over the ${MAX_BYTES / 1e6}MB limit.` };
|
|
81
|
+
const body = buf.toString('utf8');
|
|
82
|
+
const format = opts.format || 'text';
|
|
83
|
+
const isHtml = contentType.includes('html') || (!contentType && /^\s*<(!doctype|html)/i.test(body));
|
|
84
|
+
let text = body;
|
|
85
|
+
if (isHtml && format === 'text') text = htmlToText(body, res.url || parsed.href);
|
|
86
|
+
else if (contentType.includes('json') && format !== 'raw') {
|
|
87
|
+
try { text = JSON.stringify(JSON.parse(body), null, 2); } catch {}
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, text, contentType, finalUrl: res.url || parsed.href, status: res.status };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function exaSearch(query, num, signal) {
|
|
95
|
+
const signals = [AbortSignal.timeout(SEARCH_TIMEOUT_MS)];
|
|
96
|
+
if (signal) signals.push(signal);
|
|
97
|
+
const res = await fetch('https://mcp.exa.ai/mcp', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
signal: AbortSignal.any(signals),
|
|
100
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
jsonrpc: '2.0', id: 1, method: 'tools/call',
|
|
103
|
+
params: { name: 'web_search_exa', arguments: { query, numResults: num, type: 'auto', livecrawl: 'fallback' } },
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) throw new Error(`Exa returned HTTP ${res.status}`);
|
|
107
|
+
const raw = await res.text();
|
|
108
|
+
// The reply is either plain JSON or an SSE stream of "data: {...}" lines.
|
|
109
|
+
const payloads = [];
|
|
110
|
+
if (raw.trimStart().startsWith('{')) payloads.push(raw);
|
|
111
|
+
else for (const line of raw.split('\n')) if (line.startsWith('data:')) payloads.push(line.slice(5).trim());
|
|
112
|
+
for (const p of payloads) {
|
|
113
|
+
let j;
|
|
114
|
+
try { j = JSON.parse(p); } catch { continue; }
|
|
115
|
+
if (j.error) throw new Error(j.error.message || 'Exa error');
|
|
116
|
+
const text = (j.result?.content || []).filter((c) => c.type === 'text').map((c) => c.text).join('\n').trim();
|
|
117
|
+
if (text) return text;
|
|
118
|
+
}
|
|
119
|
+
throw new Error('Exa returned no results');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function ddgSearch(query, num, signal) {
|
|
123
|
+
const signals = [AbortSignal.timeout(SEARCH_TIMEOUT_MS)];
|
|
124
|
+
if (signal) signals.push(signal);
|
|
125
|
+
const res = await fetch('https://html.duckduckgo.com/html/', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
signal: AbortSignal.any(signals),
|
|
128
|
+
headers: { 'User-Agent': UA, 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
129
|
+
body: `q=${encodeURIComponent(query)}`,
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) throw new Error(`DuckDuckGo returned HTTP ${res.status}`);
|
|
132
|
+
const html = await res.text();
|
|
133
|
+
const out = [];
|
|
134
|
+
const re = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>)?/gi;
|
|
135
|
+
let m;
|
|
136
|
+
while ((m = re.exec(html)) && out.length < num) {
|
|
137
|
+
let href = decodeEntities(m[1]);
|
|
138
|
+
const u = /[?&]uddg=([^&]+)/.exec(href);
|
|
139
|
+
if (u) { try { href = decodeURIComponent(u[1]); } catch {} }
|
|
140
|
+
const title = decodeEntities(m[2].replace(/<[^>]+>/g, '')).trim();
|
|
141
|
+
const snippet = decodeEntities((m[3] || '').replace(/<[^>]+>/g, '')).trim();
|
|
142
|
+
if (title && /^https?:/.test(href)) out.push(`Title: ${title}\nURL: ${href}${snippet ? `\n${snippet}` : ''}`);
|
|
143
|
+
}
|
|
144
|
+
if (!out.length) throw new Error('DuckDuckGo returned no results');
|
|
145
|
+
return out.join('\n\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @returns {Promise<{ok:boolean, text?:string, error?:string, via?:string}>} */
|
|
149
|
+
async function webSearch(query, opts = {}) {
|
|
150
|
+
const num = Math.min(Math.max(Number(opts.num) || 8, 1), 20);
|
|
151
|
+
const errors = [];
|
|
152
|
+
try { return { ok: true, text: await exaSearch(query, num, opts.signal), via: 'exa' }; } catch (e) { errors.push(e.message); }
|
|
153
|
+
if (opts.signal?.aborted) return { ok: false, error: 'Search cancelled.' };
|
|
154
|
+
try { return { ok: true, text: await ddgSearch(query, num, opts.signal), via: 'duckduckgo' }; } catch (e) { errors.push(e.message); }
|
|
155
|
+
return { ok: false, error: `Web search failed (${errors.join('; ')}).` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { fetchPage, webSearch, htmlToText };
|