codeply-cli 0.5.0 → 0.5.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/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +65 -63
- package/bin/codeply.js +1 -1
- package/lib/agent.mjs +133 -24
- package/lib/ai.js +268 -9
- 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/bots.js +493 -16
- package/lib/chatgpt.js +282 -0
- package/lib/cloud-browser.mjs +143 -143
- package/lib/cloud.mjs +1186 -1105
- package/lib/commands.js +103 -103
- package/lib/config.js +83 -3
- 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 -311
- package/lib/history.js +75 -75
- package/lib/mail-watch.js +504 -0
- package/lib/mcp.js +258 -258
- package/lib/model-sync.js +114 -0
- package/lib/native-tools.mjs +16 -1
- package/lib/oauth-connectors.js +146 -24
- package/lib/permissions.js +109 -109
- package/lib/plugins.js +353 -353
- package/lib/publish.js +916 -0
- package/lib/reach.js +67 -0
- package/lib/server.mjs +457 -457
- package/lib/session-db.js +220 -220
- package/lib/share.js +168 -168
- package/lib/skills.js +2 -0
- package/lib/snapshot.js +146 -146
- package/lib/telemetry.js +102 -0
- package/lib/terminal-hints.js +154 -154
- package/lib/tools.mjs +316 -14
- package/lib/typecheck-worker.js +147 -147
- package/lib/web-tools.js +209 -158
- package/package.json +49 -49
- 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/publish-website/SKILL.md +76 -0
- 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/mcp.js
CHANGED
|
@@ -1,258 +1,258 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A small Model Context Protocol client: enough to connect to MCP servers,
|
|
3
|
-
* list their tools and call them. No SDK dependency; MCP is JSON-RPC 2.0
|
|
4
|
-
* over either a child process's stdio (one JSON message per line) or HTTP
|
|
5
|
-
* ("streamable HTTP": POST a request, get JSON or an SSE stream back).
|
|
6
|
-
*
|
|
7
|
-
* Servers are configured in the same shape Claude Desktop / Cursor use, so an
|
|
8
|
-
* existing config can be pasted in:
|
|
9
|
-
*
|
|
10
|
-
* ~/.codeply/mcp.json (every project)
|
|
11
|
-
* <project>/.codeply/mcp.json (this project; same name wins)
|
|
12
|
-
*
|
|
13
|
-
* { "mcpServers": {
|
|
14
|
-
* "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "..." } },
|
|
15
|
-
* "docs": { "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer ..." } },
|
|
16
|
-
* "old": { "command": "...", "disabled": true } } }
|
|
17
|
-
*
|
|
18
|
-
* Connections are kept for the life of the process and reused across turns.
|
|
19
|
-
*/
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const os = require('os');
|
|
22
|
-
const path = require('path');
|
|
23
|
-
const { spawn } = require('child_process');
|
|
24
|
-
const plugins = require('./plugins.js');
|
|
25
|
-
|
|
26
|
-
const PROTOCOL_VERSION = '2025-06-18';
|
|
27
|
-
const CONNECT_TIMEOUT_MS = 20000;
|
|
28
|
-
const CALL_TIMEOUT_MS = 120000;
|
|
29
|
-
const CLIENT_INFO = { name: 'codeply', version: '1.0' };
|
|
30
|
-
|
|
31
|
-
function readConfig(file) {
|
|
32
|
-
try {
|
|
33
|
-
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
34
|
-
return j.mcpServers || j.servers || {};
|
|
35
|
-
} catch { return {}; }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Merged server config for a project: project entries override global ones. */
|
|
39
|
-
function loadServers(cwd) {
|
|
40
|
-
const merged = { ...plugins.mcpServers(cwd), ...readConfig(path.join(os.homedir(), '.codeply', 'mcp.json')) };
|
|
41
|
-
if (cwd) Object.assign(merged, readConfig(path.join(cwd, '.codeply', 'mcp.json')));
|
|
42
|
-
return Object.fromEntries(Object.entries(merged).filter(([, s]) => s && !s.disabled && (s.command || s.url)));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Transports ─────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
class StdioConnection {
|
|
48
|
-
constructor(name, spec, cwd) {
|
|
49
|
-
this.name = name;
|
|
50
|
-
this.pending = new Map();
|
|
51
|
-
this.nextId = 1;
|
|
52
|
-
this.stderr = '';
|
|
53
|
-
// npx/npm/uvx are .cmd shims on Windows and only start through a shell.
|
|
54
|
-
// A full path to a program (which may contain spaces) is started
|
|
55
|
-
// directly. Through the shell, arguments are quoted here, since Node
|
|
56
|
-
// just joins them with spaces.
|
|
57
|
-
const cmd = String(spec.command);
|
|
58
|
-
const args = (spec.args || []).map(String);
|
|
59
|
-
const viaShell = process.platform === 'win32' && !/[\\/]/.test(cmd) && !/\.exe$/i.test(cmd);
|
|
60
|
-
const quote = (a) => (/[\s"&|<>^()]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
|
|
61
|
-
this.proc = spawn(viaShell ? [cmd, ...args].map(quote).join(' ') : cmd, viaShell ? [] : args, {
|
|
62
|
-
cwd: spec.cwd || cwd || process.cwd(),
|
|
63
|
-
env: { ...process.env, ...(spec.env || {}) },
|
|
64
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
|
-
windowsHide: true,
|
|
66
|
-
shell: viaShell,
|
|
67
|
-
});
|
|
68
|
-
let buf = '';
|
|
69
|
-
this.proc.stdout.on('data', (d) => {
|
|
70
|
-
buf += d.toString('utf8');
|
|
71
|
-
let nl;
|
|
72
|
-
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
73
|
-
const line = buf.slice(0, nl).trim();
|
|
74
|
-
buf = buf.slice(nl + 1);
|
|
75
|
-
if (!line) continue;
|
|
76
|
-
let msg;
|
|
77
|
-
try { msg = JSON.parse(line); } catch { continue; } // servers sometimes log to stdout
|
|
78
|
-
this.onMessage(msg);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
this.proc.stderr.on('data', (d) => { this.stderr = (this.stderr + d.toString('utf8')).slice(-4000); });
|
|
82
|
-
this.proc.on('error', (e) => this.failAll(`could not start: ${e.message}`));
|
|
83
|
-
this.proc.on('exit', (code) => { this.closed = true; this.failAll(`exited (code ${code})${this.stderr ? `: ${this.stderr.trim().split('\n').slice(-3).join(' ')}` : ''}`); });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
onMessage(msg) {
|
|
87
|
-
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
88
|
-
const p = this.pending.get(msg.id);
|
|
89
|
-
this.pending.delete(msg.id);
|
|
90
|
-
if (msg.error) p.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
91
|
-
else p.resolve(msg.result);
|
|
92
|
-
} else if (msg.method && msg.id !== undefined) {
|
|
93
|
-
// A request from the server (sampling, roots...): we offer none of those.
|
|
94
|
-
this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'Not supported by this client' } });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
write(obj) {
|
|
99
|
-
if (!this.closed) this.proc.stdin.write(JSON.stringify(obj) + '\n');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
request(method, params, timeoutMs) {
|
|
103
|
-
return new Promise((resolve, reject) => {
|
|
104
|
-
if (this.closed) return reject(new Error(`${this.name} is not running`));
|
|
105
|
-
const id = this.nextId++;
|
|
106
|
-
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`${method} timed out after ${timeoutMs / 1000}s`)); }, timeoutMs);
|
|
107
|
-
this.pending.set(id, { resolve: (v) => { clearTimeout(timer); resolve(v); }, reject: (e) => { clearTimeout(timer); reject(e); } });
|
|
108
|
-
this.write({ jsonrpc: '2.0', id, method, params });
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
notify(method, params) { this.write({ jsonrpc: '2.0', method, ...(params ? { params } : {}) }); }
|
|
113
|
-
|
|
114
|
-
failAll(why) {
|
|
115
|
-
for (const [, p] of this.pending) p.reject(new Error(`${this.name} ${why}`));
|
|
116
|
-
this.pending.clear();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
close() { try { this.proc.kill(); } catch {} }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
class HttpConnection {
|
|
123
|
-
constructor(name, spec) {
|
|
124
|
-
this.name = name;
|
|
125
|
-
this.url = spec.url;
|
|
126
|
-
this.headers = spec.headers || {};
|
|
127
|
-
this.sessionId = null;
|
|
128
|
-
this.nextId = 1;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async post(body, timeoutMs) {
|
|
132
|
-
const res = await fetch(this.url, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
Accept: 'application/json, text/event-stream',
|
|
137
|
-
'MCP-Protocol-Version': PROTOCOL_VERSION,
|
|
138
|
-
...(this.sessionId ? { 'Mcp-Session-Id': this.sessionId } : {}),
|
|
139
|
-
...this.headers,
|
|
140
|
-
},
|
|
141
|
-
body: JSON.stringify(body),
|
|
142
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
143
|
-
});
|
|
144
|
-
const sid = res.headers.get('mcp-session-id');
|
|
145
|
-
if (sid) this.sessionId = sid;
|
|
146
|
-
return res;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async request(method, params, timeoutMs) {
|
|
150
|
-
const id = this.nextId++;
|
|
151
|
-
const res = await this.post({ jsonrpc: '2.0', id, method, params }, timeoutMs);
|
|
152
|
-
if (!res.ok) throw new Error(`${method}: HTTP ${res.status}${res.status === 401 ? ' (needs authorization - add a token under "headers")' : ''}`);
|
|
153
|
-
const type = res.headers.get('content-type') || '';
|
|
154
|
-
let msg = null;
|
|
155
|
-
if (type.includes('text/event-stream')) {
|
|
156
|
-
// Read events until the one answering our id.
|
|
157
|
-
const text = await res.text();
|
|
158
|
-
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
159
|
-
const data = block.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('\n');
|
|
160
|
-
if (!data) continue;
|
|
161
|
-
try { const m = JSON.parse(data); if (m.id === id) { msg = m; break; } } catch {}
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
msg = await res.json().catch(() => null);
|
|
165
|
-
}
|
|
166
|
-
if (!msg) throw new Error(`${method}: no response`);
|
|
167
|
-
if (msg.error) throw new Error(msg.error.message || JSON.stringify(msg.error));
|
|
168
|
-
return msg.result;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async notify(method, params) {
|
|
172
|
-
try { await this.post({ jsonrpc: '2.0', method, ...(params ? { params } : {}) }, 10000); } catch {}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
close() {}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ─── Servers ────────────────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
const servers = new Map(); // key -> { name, conn, tools, error, specKey }
|
|
181
|
-
|
|
182
|
-
function specKey(spec) { return JSON.stringify(spec); }
|
|
183
|
-
|
|
184
|
-
async function connect(name, spec, cwd) {
|
|
185
|
-
const conn = spec.url ? new HttpConnection(name, spec) : new StdioConnection(name, spec, cwd);
|
|
186
|
-
try {
|
|
187
|
-
const init = await conn.request('initialize', { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: CLIENT_INFO }, CONNECT_TIMEOUT_MS);
|
|
188
|
-
await conn.notify('notifications/initialized');
|
|
189
|
-
const tools = [];
|
|
190
|
-
let cursor;
|
|
191
|
-
do {
|
|
192
|
-
const page = await conn.request('tools/list', cursor ? { cursor } : {}, CONNECT_TIMEOUT_MS);
|
|
193
|
-
tools.push(...(page.tools || []));
|
|
194
|
-
cursor = page.nextCursor;
|
|
195
|
-
} while (cursor && tools.length < 500);
|
|
196
|
-
return { name, conn, tools, instructions: init?.instructions || '', error: null };
|
|
197
|
-
} catch (e) {
|
|
198
|
-
conn.close();
|
|
199
|
-
return { name, conn: null, tools: [], instructions: '', error: e.message };
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Connect to every configured server for this project (reusing live
|
|
205
|
-
* connections) and return what they offer. Never throws.
|
|
206
|
-
* @returns {Promise<Array<{name, tools, error, instructions}>>}
|
|
207
|
-
*/
|
|
208
|
-
async function listServers(cwd) {
|
|
209
|
-
const wanted = loadServers(cwd);
|
|
210
|
-
const out = [];
|
|
211
|
-
await Promise.all(Object.entries(wanted).map(async ([name, spec]) => {
|
|
212
|
-
const key = `${cwd}::${name}`;
|
|
213
|
-
let s = servers.get(key);
|
|
214
|
-
const stale = s && (s.specKey !== specKey(spec) || (s.conn && s.conn.closed) || s.error);
|
|
215
|
-
if (!s || stale) {
|
|
216
|
-
if (s?.conn) s.conn.close();
|
|
217
|
-
s = { ...(await connect(name, spec, cwd)), specKey: specKey(spec) };
|
|
218
|
-
servers.set(key, s);
|
|
219
|
-
}
|
|
220
|
-
out.push({ name, tools: s.tools, error: s.error, instructions: s.instructions });
|
|
221
|
-
}));
|
|
222
|
-
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Tool results as plain text for the model, images kept aside as data URLs. */
|
|
226
|
-
function flattenResult(result) {
|
|
227
|
-
const parts = Array.isArray(result?.content) ? result.content : [];
|
|
228
|
-
const text = [];
|
|
229
|
-
const images = [];
|
|
230
|
-
for (const p of parts) {
|
|
231
|
-
if (p.type === 'text') text.push(p.text);
|
|
232
|
-
else if (p.type === 'image' && p.data) images.push(`data:${p.mimeType || 'image/png'};base64,${p.data}`);
|
|
233
|
-
else if (p.type === 'resource') text.push(p.resource?.text || `[resource ${p.resource?.uri || ''}]`);
|
|
234
|
-
else text.push(`[${p.type} content]`);
|
|
235
|
-
}
|
|
236
|
-
if (!parts.length && result?.structuredContent) text.push(JSON.stringify(result.structuredContent, null, 1));
|
|
237
|
-
return { text: text.join('\n') || '(empty result)', images, isError: !!result?.isError };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async function callTool(cwd, serverName, toolName, args) {
|
|
241
|
-
const s = servers.get(`${cwd}::${serverName}`);
|
|
242
|
-
if (!s || !s.conn) throw new Error(`MCP server "${serverName}" is not connected${s?.error ? ` (${s.error})` : ''}.`);
|
|
243
|
-
const result = await s.conn.request('tools/call', { name: toolName, arguments: args || {} }, CALL_TIMEOUT_MS);
|
|
244
|
-
return flattenResult(result);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function findTool(cwd, serverName, toolName) {
|
|
248
|
-
const s = servers.get(`${cwd}::${serverName}`);
|
|
249
|
-
return s ? s.tools.find((t) => t.name === toolName) || null : null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function closeAll() {
|
|
253
|
-
for (const [, s] of servers) if (s.conn) s.conn.close();
|
|
254
|
-
servers.clear();
|
|
255
|
-
}
|
|
256
|
-
process.on('exit', closeAll);
|
|
257
|
-
|
|
258
|
-
module.exports = { listServers, callTool, findTool, loadServers, closeAll, flattenResult };
|
|
1
|
+
/**
|
|
2
|
+
* A small Model Context Protocol client: enough to connect to MCP servers,
|
|
3
|
+
* list their tools and call them. No SDK dependency; MCP is JSON-RPC 2.0
|
|
4
|
+
* over either a child process's stdio (one JSON message per line) or HTTP
|
|
5
|
+
* ("streamable HTTP": POST a request, get JSON or an SSE stream back).
|
|
6
|
+
*
|
|
7
|
+
* Servers are configured in the same shape Claude Desktop / Cursor use, so an
|
|
8
|
+
* existing config can be pasted in:
|
|
9
|
+
*
|
|
10
|
+
* ~/.codeply/mcp.json (every project)
|
|
11
|
+
* <project>/.codeply/mcp.json (this project; same name wins)
|
|
12
|
+
*
|
|
13
|
+
* { "mcpServers": {
|
|
14
|
+
* "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "..." } },
|
|
15
|
+
* "docs": { "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer ..." } },
|
|
16
|
+
* "old": { "command": "...", "disabled": true } } }
|
|
17
|
+
*
|
|
18
|
+
* Connections are kept for the life of the process and reused across turns.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { spawn } = require('child_process');
|
|
24
|
+
const plugins = require('./plugins.js');
|
|
25
|
+
|
|
26
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
27
|
+
const CONNECT_TIMEOUT_MS = 20000;
|
|
28
|
+
const CALL_TIMEOUT_MS = 120000;
|
|
29
|
+
const CLIENT_INFO = { name: 'codeply', version: '1.0' };
|
|
30
|
+
|
|
31
|
+
function readConfig(file) {
|
|
32
|
+
try {
|
|
33
|
+
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
return j.mcpServers || j.servers || {};
|
|
35
|
+
} catch { return {}; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Merged server config for a project: project entries override global ones. */
|
|
39
|
+
function loadServers(cwd) {
|
|
40
|
+
const merged = { ...plugins.mcpServers(cwd), ...readConfig(path.join(os.homedir(), '.codeply', 'mcp.json')) };
|
|
41
|
+
if (cwd) Object.assign(merged, readConfig(path.join(cwd, '.codeply', 'mcp.json')));
|
|
42
|
+
return Object.fromEntries(Object.entries(merged).filter(([, s]) => s && !s.disabled && (s.command || s.url)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Transports ─────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
class StdioConnection {
|
|
48
|
+
constructor(name, spec, cwd) {
|
|
49
|
+
this.name = name;
|
|
50
|
+
this.pending = new Map();
|
|
51
|
+
this.nextId = 1;
|
|
52
|
+
this.stderr = '';
|
|
53
|
+
// npx/npm/uvx are .cmd shims on Windows and only start through a shell.
|
|
54
|
+
// A full path to a program (which may contain spaces) is started
|
|
55
|
+
// directly. Through the shell, arguments are quoted here, since Node
|
|
56
|
+
// just joins them with spaces.
|
|
57
|
+
const cmd = String(spec.command);
|
|
58
|
+
const args = (spec.args || []).map(String);
|
|
59
|
+
const viaShell = process.platform === 'win32' && !/[\\/]/.test(cmd) && !/\.exe$/i.test(cmd);
|
|
60
|
+
const quote = (a) => (/[\s"&|<>^()]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
|
|
61
|
+
this.proc = spawn(viaShell ? [cmd, ...args].map(quote).join(' ') : cmd, viaShell ? [] : args, {
|
|
62
|
+
cwd: spec.cwd || cwd || process.cwd(),
|
|
63
|
+
env: { ...process.env, ...(spec.env || {}) },
|
|
64
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
|
+
windowsHide: true,
|
|
66
|
+
shell: viaShell,
|
|
67
|
+
});
|
|
68
|
+
let buf = '';
|
|
69
|
+
this.proc.stdout.on('data', (d) => {
|
|
70
|
+
buf += d.toString('utf8');
|
|
71
|
+
let nl;
|
|
72
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
73
|
+
const line = buf.slice(0, nl).trim();
|
|
74
|
+
buf = buf.slice(nl + 1);
|
|
75
|
+
if (!line) continue;
|
|
76
|
+
let msg;
|
|
77
|
+
try { msg = JSON.parse(line); } catch { continue; } // servers sometimes log to stdout
|
|
78
|
+
this.onMessage(msg);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.proc.stderr.on('data', (d) => { this.stderr = (this.stderr + d.toString('utf8')).slice(-4000); });
|
|
82
|
+
this.proc.on('error', (e) => this.failAll(`could not start: ${e.message}`));
|
|
83
|
+
this.proc.on('exit', (code) => { this.closed = true; this.failAll(`exited (code ${code})${this.stderr ? `: ${this.stderr.trim().split('\n').slice(-3).join(' ')}` : ''}`); });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onMessage(msg) {
|
|
87
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
88
|
+
const p = this.pending.get(msg.id);
|
|
89
|
+
this.pending.delete(msg.id);
|
|
90
|
+
if (msg.error) p.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
91
|
+
else p.resolve(msg.result);
|
|
92
|
+
} else if (msg.method && msg.id !== undefined) {
|
|
93
|
+
// A request from the server (sampling, roots...): we offer none of those.
|
|
94
|
+
this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'Not supported by this client' } });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
write(obj) {
|
|
99
|
+
if (!this.closed) this.proc.stdin.write(JSON.stringify(obj) + '\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
request(method, params, timeoutMs) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
if (this.closed) return reject(new Error(`${this.name} is not running`));
|
|
105
|
+
const id = this.nextId++;
|
|
106
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`${method} timed out after ${timeoutMs / 1000}s`)); }, timeoutMs);
|
|
107
|
+
this.pending.set(id, { resolve: (v) => { clearTimeout(timer); resolve(v); }, reject: (e) => { clearTimeout(timer); reject(e); } });
|
|
108
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
notify(method, params) { this.write({ jsonrpc: '2.0', method, ...(params ? { params } : {}) }); }
|
|
113
|
+
|
|
114
|
+
failAll(why) {
|
|
115
|
+
for (const [, p] of this.pending) p.reject(new Error(`${this.name} ${why}`));
|
|
116
|
+
this.pending.clear();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
close() { try { this.proc.kill(); } catch {} }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class HttpConnection {
|
|
123
|
+
constructor(name, spec) {
|
|
124
|
+
this.name = name;
|
|
125
|
+
this.url = spec.url;
|
|
126
|
+
this.headers = spec.headers || {};
|
|
127
|
+
this.sessionId = null;
|
|
128
|
+
this.nextId = 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async post(body, timeoutMs) {
|
|
132
|
+
const res = await fetch(this.url, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
Accept: 'application/json, text/event-stream',
|
|
137
|
+
'MCP-Protocol-Version': PROTOCOL_VERSION,
|
|
138
|
+
...(this.sessionId ? { 'Mcp-Session-Id': this.sessionId } : {}),
|
|
139
|
+
...this.headers,
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify(body),
|
|
142
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
143
|
+
});
|
|
144
|
+
const sid = res.headers.get('mcp-session-id');
|
|
145
|
+
if (sid) this.sessionId = sid;
|
|
146
|
+
return res;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async request(method, params, timeoutMs) {
|
|
150
|
+
const id = this.nextId++;
|
|
151
|
+
const res = await this.post({ jsonrpc: '2.0', id, method, params }, timeoutMs);
|
|
152
|
+
if (!res.ok) throw new Error(`${method}: HTTP ${res.status}${res.status === 401 ? ' (needs authorization - add a token under "headers")' : ''}`);
|
|
153
|
+
const type = res.headers.get('content-type') || '';
|
|
154
|
+
let msg = null;
|
|
155
|
+
if (type.includes('text/event-stream')) {
|
|
156
|
+
// Read events until the one answering our id.
|
|
157
|
+
const text = await res.text();
|
|
158
|
+
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
159
|
+
const data = block.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('\n');
|
|
160
|
+
if (!data) continue;
|
|
161
|
+
try { const m = JSON.parse(data); if (m.id === id) { msg = m; break; } } catch {}
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
msg = await res.json().catch(() => null);
|
|
165
|
+
}
|
|
166
|
+
if (!msg) throw new Error(`${method}: no response`);
|
|
167
|
+
if (msg.error) throw new Error(msg.error.message || JSON.stringify(msg.error));
|
|
168
|
+
return msg.result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async notify(method, params) {
|
|
172
|
+
try { await this.post({ jsonrpc: '2.0', method, ...(params ? { params } : {}) }, 10000); } catch {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
close() {}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Servers ────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
const servers = new Map(); // key -> { name, conn, tools, error, specKey }
|
|
181
|
+
|
|
182
|
+
function specKey(spec) { return JSON.stringify(spec); }
|
|
183
|
+
|
|
184
|
+
async function connect(name, spec, cwd) {
|
|
185
|
+
const conn = spec.url ? new HttpConnection(name, spec) : new StdioConnection(name, spec, cwd);
|
|
186
|
+
try {
|
|
187
|
+
const init = await conn.request('initialize', { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: CLIENT_INFO }, CONNECT_TIMEOUT_MS);
|
|
188
|
+
await conn.notify('notifications/initialized');
|
|
189
|
+
const tools = [];
|
|
190
|
+
let cursor;
|
|
191
|
+
do {
|
|
192
|
+
const page = await conn.request('tools/list', cursor ? { cursor } : {}, CONNECT_TIMEOUT_MS);
|
|
193
|
+
tools.push(...(page.tools || []));
|
|
194
|
+
cursor = page.nextCursor;
|
|
195
|
+
} while (cursor && tools.length < 500);
|
|
196
|
+
return { name, conn, tools, instructions: init?.instructions || '', error: null };
|
|
197
|
+
} catch (e) {
|
|
198
|
+
conn.close();
|
|
199
|
+
return { name, conn: null, tools: [], instructions: '', error: e.message };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Connect to every configured server for this project (reusing live
|
|
205
|
+
* connections) and return what they offer. Never throws.
|
|
206
|
+
* @returns {Promise<Array<{name, tools, error, instructions}>>}
|
|
207
|
+
*/
|
|
208
|
+
async function listServers(cwd) {
|
|
209
|
+
const wanted = loadServers(cwd);
|
|
210
|
+
const out = [];
|
|
211
|
+
await Promise.all(Object.entries(wanted).map(async ([name, spec]) => {
|
|
212
|
+
const key = `${cwd}::${name}`;
|
|
213
|
+
let s = servers.get(key);
|
|
214
|
+
const stale = s && (s.specKey !== specKey(spec) || (s.conn && s.conn.closed) || s.error);
|
|
215
|
+
if (!s || stale) {
|
|
216
|
+
if (s?.conn) s.conn.close();
|
|
217
|
+
s = { ...(await connect(name, spec, cwd)), specKey: specKey(spec) };
|
|
218
|
+
servers.set(key, s);
|
|
219
|
+
}
|
|
220
|
+
out.push({ name, tools: s.tools, error: s.error, instructions: s.instructions });
|
|
221
|
+
}));
|
|
222
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Tool results as plain text for the model, images kept aside as data URLs. */
|
|
226
|
+
function flattenResult(result) {
|
|
227
|
+
const parts = Array.isArray(result?.content) ? result.content : [];
|
|
228
|
+
const text = [];
|
|
229
|
+
const images = [];
|
|
230
|
+
for (const p of parts) {
|
|
231
|
+
if (p.type === 'text') text.push(p.text);
|
|
232
|
+
else if (p.type === 'image' && p.data) images.push(`data:${p.mimeType || 'image/png'};base64,${p.data}`);
|
|
233
|
+
else if (p.type === 'resource') text.push(p.resource?.text || `[resource ${p.resource?.uri || ''}]`);
|
|
234
|
+
else text.push(`[${p.type} content]`);
|
|
235
|
+
}
|
|
236
|
+
if (!parts.length && result?.structuredContent) text.push(JSON.stringify(result.structuredContent, null, 1));
|
|
237
|
+
return { text: text.join('\n') || '(empty result)', images, isError: !!result?.isError };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function callTool(cwd, serverName, toolName, args) {
|
|
241
|
+
const s = servers.get(`${cwd}::${serverName}`);
|
|
242
|
+
if (!s || !s.conn) throw new Error(`MCP server "${serverName}" is not connected${s?.error ? ` (${s.error})` : ''}.`);
|
|
243
|
+
const result = await s.conn.request('tools/call', { name: toolName, arguments: args || {} }, CALL_TIMEOUT_MS);
|
|
244
|
+
return flattenResult(result);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function findTool(cwd, serverName, toolName) {
|
|
248
|
+
const s = servers.get(`${cwd}::${serverName}`);
|
|
249
|
+
return s ? s.tools.find((t) => t.name === toolName) || null : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function closeAll() {
|
|
253
|
+
for (const [, s] of servers) if (s.conn) s.conn.close();
|
|
254
|
+
servers.clear();
|
|
255
|
+
}
|
|
256
|
+
process.on('exit', closeAll);
|
|
257
|
+
|
|
258
|
+
module.exports = { listServers, callTool, findTool, loadServers, closeAll, flattenResult };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codeply - "Use on my phone": syncs a model the user added (with their own
|
|
3
|
+
* API key) to their Codeply account, so the phone app can use it while this
|
|
4
|
+
* PC is off.
|
|
5
|
+
*
|
|
6
|
+
* The key goes to the `user-models` edge function once, over TLS with the
|
|
7
|
+
* user's sign-in token. The function seals it (envelope encryption, see
|
|
8
|
+
* Codeply-App/supabase/functions/_shared/model-keys.ts) and never sends it
|
|
9
|
+
* back: listing returns only "****abcd". The phone then chats through the
|
|
10
|
+
* `byok-proxy` function, which decrypts it there for each request.
|
|
11
|
+
*
|
|
12
|
+
* Not synced, by design:
|
|
13
|
+
* chatgpt the ChatGPT sign-in stays on this PC (chatgpt.js).
|
|
14
|
+
* ollama Ollama runs on this PC (or a host only it can reach).
|
|
15
|
+
* no key / plain http / a private address: the cloud cannot use those.
|
|
16
|
+
*
|
|
17
|
+
* Pure logic with an injectable fetch and token source, so it is tested
|
|
18
|
+
* against a fake server in scripts/engine-test.mjs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_URL = 'https://zswkhfkfseclgadhvobg.supabase.co/functions/v1/user-models';
|
|
22
|
+
|
|
23
|
+
const PRIVATE_HOST = /^(localhost|.*\.localhost|.*\.local|.*\.internal|.*\.lan|.*\.home\.arpa|0\.0\.0\.0|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|169\.254\.\d+\.\d+|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d+\.\d+|\[.*\])$/i;
|
|
24
|
+
|
|
25
|
+
/** Why a model cannot be used on the phone without this PC, or null when it can. */
|
|
26
|
+
function phoneBlocker(m) {
|
|
27
|
+
if (!m) return 'Unknown model.';
|
|
28
|
+
if (m.kind === 'chatgpt') return 'ChatGPT plan models stay on this PC: your ChatGPT sign-in is never uploaded. The phone can still use them while this PC is on.';
|
|
29
|
+
if (m.kind === 'ollama') return 'Ollama runs on this PC, so the phone uses it through this PC while it is on.';
|
|
30
|
+
if (!m.apiKey) return 'Only models with an API key can be used on the phone without this PC.';
|
|
31
|
+
let u;
|
|
32
|
+
try { u = new URL(m.baseUrl); } catch { return 'The base URL is not a valid address.'; }
|
|
33
|
+
if (u.protocol !== 'https:') return 'The phone can only use https addresses. This one keeps working through this PC.';
|
|
34
|
+
if (u.username || u.password) return 'Remove the user name or password from the base URL first.';
|
|
35
|
+
if (u.port && u.port !== '443') return 'The phone can only use the standard https port. This one keeps working through this PC.';
|
|
36
|
+
const host = u.hostname.replace(/\.$/, '');
|
|
37
|
+
if (PRIVATE_HOST.test(host) || (!host.includes('.') && !host.includes(':'))) {
|
|
38
|
+
return 'That address is on your own network, so it only works through this PC.';
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What the phone toggle should show for a model, given who is signed in now. */
|
|
44
|
+
function phoneStatus(m, userId) {
|
|
45
|
+
const blocker = phoneBlocker(m);
|
|
46
|
+
if (blocker) return { eligible: false, on: false, synced: false, reason: blocker };
|
|
47
|
+
const on = m.phone === true;
|
|
48
|
+
const synced = on && !!m.phoneSyncedAt && !m.phoneError && !!userId && m.phoneUser === userId;
|
|
49
|
+
return { eligible: true, on, synced, reason: on && m.phoneError ? m.phoneError : '' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* opts.getAccessToken: async () => token or null (signed out)
|
|
54
|
+
* opts.fetch: fetch implementation (defaults to the global one)
|
|
55
|
+
* opts.url / opts.anonKey: the function endpoint and the project's public anon key
|
|
56
|
+
*/
|
|
57
|
+
function createModelSync({ getAccessToken, fetch: fetchImpl = globalThis.fetch, url = DEFAULT_URL, anonKey = '', timeoutMs = 20000 } = {}) {
|
|
58
|
+
async function call(action, payload) {
|
|
59
|
+
let token = null;
|
|
60
|
+
try { token = await getAccessToken(); } catch {}
|
|
61
|
+
if (!token) return { ok: false, signedOut: true, error: 'Sign in to Codeply to use this model on your phone.' };
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await fetchImpl(url, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { Authorization: `Bearer ${token}`, ...(anonKey ? { apikey: anonKey } : {}), 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({ action, ...payload }),
|
|
68
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
69
|
+
});
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { ok: false, offline: true, error: e && e.name === 'TimeoutError' ? 'Codeply took too long to answer.' : "Can't reach Codeply. Check your internet connection." };
|
|
72
|
+
}
|
|
73
|
+
const data = await res.json().catch(() => ({}));
|
|
74
|
+
if (res.ok && data.success) return { ok: true, ...data };
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: res.status,
|
|
78
|
+
retry: !!data.retry,
|
|
79
|
+
needsKey: /send the api key/i.test(data.error || ''),
|
|
80
|
+
error: data.error || `Codeply could not save it (${res.status}).`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Metadata of every synced model, keys masked. */
|
|
85
|
+
async function list() {
|
|
86
|
+
return call('list', {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sends a model. withKey also sends its API key (first sync, a new key, a
|
|
91
|
+
* new address); otherwise only the name and model id are updated. When the
|
|
92
|
+
* server turns out not to have the key (removed elsewhere), it retries once
|
|
93
|
+
* with the key, and once more on a save conflict.
|
|
94
|
+
*/
|
|
95
|
+
async function push(m, { withKey = false } = {}) {
|
|
96
|
+
const blocker = phoneBlocker(m);
|
|
97
|
+
if (blocker) return { ok: false, blocked: true, error: blocker };
|
|
98
|
+
const send = (k) => call('upsert', {
|
|
99
|
+
model: { clientId: m.id, name: m.name, kind: 'openai', baseUrl: m.baseUrl, model: m.model, ...(k ? { apiKey: m.apiKey } : {}) },
|
|
100
|
+
});
|
|
101
|
+
let r = await send(withKey);
|
|
102
|
+
if (!r.ok && r.needsKey && !withKey) r = await send(true);
|
|
103
|
+
if (!r.ok && r.retry) r = await send(withKey || r.needsKey);
|
|
104
|
+
return r;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function remove(clientId) {
|
|
108
|
+
return call('delete', { clientId });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { list, push, remove };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { phoneBlocker, phoneStatus, createModelSync, DEFAULT_URL };
|