codeply-cli 0.5.1 → 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 +82 -17
- package/lib/chatgpt.js +282 -0
- package/lib/cloud-browser.mjs +143 -143
- package/lib/cloud.mjs +1186 -1144
- 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/native-tools.mjs
CHANGED
|
@@ -32,8 +32,12 @@ export const TOOL_DOCS = {
|
|
|
32
32
|
list_skills: { d: 'Search the full skill library.', req: ['query'], p: { query: 'One or two words.' } },
|
|
33
33
|
fetch_image: { d: 'Download an image into the project.', req: ['url', 'path'], p: { url: 'Image URL.', path: 'Where to save it, e.g. assets/hero.jpg.' } },
|
|
34
34
|
browser_check: { d: 'Open a page in a real browser and get errors plus a screenshot.', req: ['url'], p: { url: 'file:///absolute/path or http://localhost:port.', wait: 'Extra milliseconds to wait before capturing.', viewport: 'desktop (default), tablet, mobile, or WIDTHxHEIGHT.' } },
|
|
35
|
-
gmail_send: { d: 'Send a real email now
|
|
35
|
+
gmail_send: { d: 'Send a real email now. Only when the user asked for it to go out. The user sees it on an editable card first and may change it or save it as a draft instead; the result says what really happened.', req: [], p: { to: 'Recipient address.', subject: 'Subject line.', body: 'Plain-text body.', draft: 'Optional: the id of a draft saved in Codeply (from drafts_list) to send; to/subject/body then come from it.' } },
|
|
36
36
|
gmail_search: { d: 'Search the connected Gmail account. Leave query empty to list the newest mail.', req: [], p: { query: 'Gmail search syntax, e.g. "is:unread" or "from:x@y.com". Empty = newest mail.' } },
|
|
37
|
+
gmail_draft: { d: 'Save an email as a draft without sending it: in Gmail\'s Drafts when Gmail is connected, otherwise in Codeply\'s own drafts.', req: [], p: { to: 'Recipient address (optional for a draft).', subject: 'Subject line.', body: 'Plain-text body.' } },
|
|
38
|
+
drafts_list: { d: 'List the drafts saved in Codeply (ids, subject, recipient). Send one with gmail_send and its id as draft.', req: [], p: {} },
|
|
39
|
+
calendar_list: { d: 'List events on the user\'s Google Calendar in a time range (default: today). Times are local.', req: [], p: { from: 'Start: today, tomorrow, 2026-10-03 or 2026-10-03T14:00. Default today.', to: 'End (a date means through that whole day). Default: end of the from day.', max: 'Most events to return (1-50, default 20).' } },
|
|
40
|
+
calendar_add: { d: 'Add an event to the user\'s Google Calendar (asks the user first).', req: ['title', 'start'], p: { title: 'Event title.', start: 'Local start: 2026-10-03T14:00, or 2026-10-03 for an all-day event.', end: 'Local end. Default: one hour after start (all-day: the same day).', description: 'Notes for the event.', location: 'Where it is.', reminders: 'Popup reminders, minutes before, comma-separated, e.g. "10,60".' } },
|
|
37
41
|
slack_post_message: { d: 'Post a real Slack message now. Only when the user asked for it.', req: ['channel', 'text'], p: { channel: 'Channel name or id.', text: 'Message text.' } },
|
|
38
42
|
vercel_deploy: { d: 'Deploy a folder to the connected Vercel account (live, real).', req: [], p: { path: 'Folder to deploy. Default ".".' } },
|
|
39
43
|
supabase_create_project: { d: 'Create a new Supabase project (billable, real).', req: ['name'], p: { name: 'Project name.' } },
|
|
@@ -44,7 +48,16 @@ export const TOOL_DOCS = {
|
|
|
44
48
|
supabase_api: { d: 'Call the Supabase Management API.', req: ['method', 'path'], p: { method: 'GET, POST, PATCH, PUT or DELETE.', path: 'Path starting with /v1/.', body: 'JSON body.' } },
|
|
45
49
|
supabase_sql: { d: 'Run SQL on a Supabase project database.', req: ['ref', 'query'], p: { ref: 'Project ref.', query: 'SQL.' } },
|
|
46
50
|
vercel_api: { d: 'Call the Vercel REST API.', req: ['method', 'path'], p: { method: 'HTTP method.', path: 'Path like /v9/projects.', body: 'JSON body.' } },
|
|
51
|
+
publish_check: { d: 'Publishing only: read the project for publishing (stack, database signals, connections, earlier publishes).', req: [], p: { path: 'Folder to publish. Default ".".' } },
|
|
52
|
+
publish_connect: { d: 'Publishing only: ask the user to connect Supabase, Vercel or GitHub in one click and wait until it is connected.', req: ['service'], p: { service: 'supabase, vercel or github.', reason: 'One sentence on why it is needed, in plain words.' } },
|
|
53
|
+
supabase_setup: { d: 'Publishing only: pick (asks the user) or create a Supabase project, then write its URL and anon key into the app where its stack reads them.', req: [], p: { project: 'Optional: a project ref or name, or "new".', name: 'Name for a new project.', region: 'Region for a new project. Default us-east-1.' } },
|
|
54
|
+
supabase_schema: { d: 'Publishing only: run the SQL the app needs on its Supabase project. The user sees and approves it; RLS is turned on for new tables.', req: [], p: { sql: 'The SQL (tables plus RLS policies).', path: 'Or a .sql file in the project.', ref: 'Project ref. Default: the one from supabase_setup.' } },
|
|
55
|
+
publish_deploy: { d: 'Publishing only: deploy to Vercel (asks about GitHub before the first deploy), set env vars, wait until it is live and return the live URL.', req: [], p: { path: 'Folder to publish. Default ".".', name: 'Vercel project name. Default: the folder name or the earlier project.' } },
|
|
56
|
+
publish_github: { d: 'Publishing only, when the user wants GitHub: create or reuse the repo, push, and link it to Vercel so pushes deploy on their own.', req: [], p: { path: 'Folder. Default ".".', name: 'Repo name for a new repo.', private: '"false" for a public repo. Default private.' } },
|
|
47
57
|
web_fetch: { d: 'Fetch a web page or API URL and get its content as readable text.', req: ['url'], p: { url: 'http:// or https:// URL.', format: '"text" (default, HTML converted to readable text), "html" or "raw".' } },
|
|
58
|
+
reach_me: { d: 'Reach the user on their phone: how=call rings their phone with your voice (only when it is urgent or they asked for a call), how=text sends a notification (anything else). Now, or later with at.', req: ['how', 'message'], p: { how: 'call or text.', message: 'What to tell them, short and specific.', at: 'Optional: when, like 17:30, 5pm, "in 20 minutes" or an ISO time. Empty means now.', bot: 'Optional: which of the user\'s bots it comes from, by name ("tell Shella to call me" means bot=Shella). Empty means you.' } },
|
|
59
|
+
watch_email: { d: 'Watch the inbox for an email from a sender and reach the user the moment one arrives (works in the background, also with Craft closed to the tray).', req: ['from', 'how'], p: { from: 'The email address, or a domain like example.com.', how: 'call or text, when it arrives.', note: 'Optional: what to say about it.', repeat: 'Optional: yes to alert on every email from them; default is once.' } },
|
|
60
|
+
image_search: { d: 'Search the whole web for real photos (like Google Images). Returns image URLs with sizes; download the one you pick with fetch_image.', req: ['query'], p: { query: 'What the photo should show.', num: 'Number of results (1-16, default 8).' } },
|
|
48
61
|
web_search: { d: 'Search the web for current information (docs, errors, versions, news).', req: ['query'], p: { query: 'The search query.', num: 'Number of results (1-20, default 8).' } },
|
|
49
62
|
apply_patch: { d: 'Change several files at once (add, update, delete, move) with one patch. Use it for multi-file changes; use edit_file for a single block.', req: ['patch'], p: { patch: 'The patch text: "*** Begin Patch", then "*** Add File: path" (lines start with +), "*** Update File: path" (optional "*** Move to: path", "@@ anchor line", then lines starting with " ", "-" or "+"), "*** Delete File: path", and "*** End Patch".' } },
|
|
50
63
|
lsp: { d: 'Code navigation for TypeScript/JavaScript: find the real definition or every reference of a symbol, hover for its type, outline a file, or search symbols.', req: ['operation'], p: { operation: 'definition, references, implementation, hover, documentSymbol or workspaceSymbol.', path: 'File the symbol is in (not needed for workspaceSymbol).', line: '1-based line of the symbol.', character: '1-based column of the symbol (or give symbol instead).', symbol: 'The name on that line, e.g. fetchUser.', query: 'For workspaceSymbol: the name to look for.' } },
|
|
@@ -109,6 +122,8 @@ export function toNativeMessages(messages, parse, mcpToNative = null) {
|
|
|
109
122
|
role: 'assistant',
|
|
110
123
|
content: prose || '',
|
|
111
124
|
tool_calls: withIds.map((c) => ({ id: c.id, type: 'function', function: nativeCall(c) })),
|
|
125
|
+
// DeepSeek's thinking mode rejects a tool call whose reasoning is missing.
|
|
126
|
+
...(m.reasoning_content !== undefined ? { reasoning_content: m.reasoning_content } : {}),
|
|
112
127
|
});
|
|
113
128
|
// Collect everything up to the next assistant message.
|
|
114
129
|
const answers = new Map();
|
package/lib/oauth-connectors.js
CHANGED
|
@@ -7,7 +7,23 @@
|
|
|
7
7
|
* owns spinning that server up/down; this file only builds URLs and talks to
|
|
8
8
|
* the providers' token/API endpoints. No secrets are ever logged.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
// REST API hosts for Vercel, Supabase and GitHub. The environment overrides
|
|
11
|
+
// exist only so the offline tests can point every call at fake local servers;
|
|
12
|
+
// the app itself never sets them.
|
|
13
|
+
const API = {
|
|
14
|
+
vercel: () => (process.env.CODEPLY_VERCEL_API || 'https://api.vercel.com').replace(/\/+$/, ''),
|
|
15
|
+
supabase: () => (process.env.CODEPLY_SUPABASE_API || 'https://api.supabase.com').replace(/\/+$/, ''),
|
|
16
|
+
github: () => (process.env.CODEPLY_GITHUB_API || 'https://api.github.com').replace(/\/+$/, ''),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// One Google connection covers Gmail (read, send, drafts) and Google
|
|
20
|
+
// Calendar events. A connection made before compose/calendar were added lacks
|
|
21
|
+
// them; the tools say "reconnect Gmail in Connect Apps" when Google refuses.
|
|
22
|
+
const GOOGLE_SCOPE = {
|
|
23
|
+
compose: 'https://www.googleapis.com/auth/gmail.compose',
|
|
24
|
+
calendar: 'https://www.googleapis.com/auth/calendar.events',
|
|
25
|
+
};
|
|
26
|
+
const GMAIL_SCOPES = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.send', GOOGLE_SCOPE.compose, GOOGLE_SCOPE.calendar].join(' ');
|
|
11
27
|
// channels:join is what lets slackPostMessage auto-join a public channel
|
|
12
28
|
// before posting to it - without it, chat:write alone only covers channels
|
|
13
29
|
// the app has already been manually invited into.
|
|
@@ -86,14 +102,14 @@ async function getGmailProfile(accessToken) {
|
|
|
86
102
|
* per-error message so the real cause shows, with a plain-words hint for the
|
|
87
103
|
* ones that need the user to act.
|
|
88
104
|
*/
|
|
89
|
-
function gmailError(data, status, what) {
|
|
105
|
+
function gmailError(data, status, what, scopeHint) {
|
|
90
106
|
const err = (data && data.error) || {};
|
|
91
107
|
const first = Array.isArray(err.errors) && err.errors[0] ? err.errors[0] : {};
|
|
92
108
|
const reason = first.reason || err.status || '';
|
|
93
109
|
const detail = [err.message, first.message].filter((m, i, a) => m && a.indexOf(m) === i).join(': ');
|
|
94
110
|
let hint = '';
|
|
95
111
|
if (status === 401) hint = ' The Gmail sign-in expired or was revoked. Reconnect Gmail in Connect Apps.';
|
|
96
|
-
else if (status === 403 && /scope|insufficient/i.test(detail + reason)) hint = ' Gmail was connected without read access. Reconnect Gmail in Connect Apps and allow all requested permissions.';
|
|
112
|
+
else if (status === 403 && /scope|insufficient/i.test(detail + reason)) hint = scopeHint || ' Gmail was connected without read access. Reconnect Gmail in Connect Apps and allow all requested permissions.';
|
|
97
113
|
else if (status === 403 && /accessNotConfigured|SERVICE_DISABLED|has not been used/i.test(detail + reason)) hint = ' The Gmail API is not enabled on the Google Cloud project behind this app.';
|
|
98
114
|
else if (/failedPrecondition|FAILED_PRECONDITION/i.test(reason)) hint = ' Google says this account has no usable Gmail mailbox for this app (Gmail turned off, or the account is not a test user of the app). Reconnect Gmail in Connect Apps with the right account.';
|
|
99
115
|
else if (status === 400) hint = ' Reconnecting Gmail in Connect Apps usually fixes this.';
|
|
@@ -111,8 +127,24 @@ function base64UrlEncode(str) {
|
|
|
111
127
|
return Buffer.from(str, 'utf8').toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
112
128
|
}
|
|
113
129
|
|
|
130
|
+
/**
|
|
131
|
+
* RFC 2822 message, base64url as Gmail wants it. A non-ASCII subject is RFC 2047 encoded.
|
|
132
|
+
* inReplyTo / references (Message-ID values) make it a reply in the same thread.
|
|
133
|
+
*/
|
|
134
|
+
function rawEmail({ to, subject, body, inReplyTo, references }) {
|
|
135
|
+
const s = String(subject || '');
|
|
136
|
+
const subj = /^[\x20-\x7e]*$/.test(s) ? s : `=?UTF-8?B?${Buffer.from(s, 'utf8').toString('base64')}?=`;
|
|
137
|
+
const oneLine = (v) => String(v || '').replace(/[\r\n]+/g, ' ').trim();
|
|
138
|
+
const head = [
|
|
139
|
+
to ? `To: ${oneLine(to)}` : '', `Subject: ${oneLine(subj)}`,
|
|
140
|
+
inReplyTo ? `In-Reply-To: ${oneLine(inReplyTo)}` : '', references ? `References: ${oneLine(references)}` : '',
|
|
141
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
142
|
+
].filter(Boolean).join('\r\n');
|
|
143
|
+
return base64UrlEncode(`${head}\r\n\r\n${body || ''}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
async function gmailSend(accessToken, { to, subject, body }) {
|
|
115
|
-
const raw =
|
|
147
|
+
const raw = rawEmail({ to, subject, body });
|
|
116
148
|
const res = await fetch('https://www.googleapis.com/gmail/v1/users/me/messages/send', {
|
|
117
149
|
method: 'POST',
|
|
118
150
|
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
@@ -123,6 +155,66 @@ async function gmailSend(accessToken, { to, subject, body }) {
|
|
|
123
155
|
return data; // { id, threadId, ... }
|
|
124
156
|
}
|
|
125
157
|
|
|
158
|
+
/** Saves a draft in the user's Gmail (needs gmail.compose). threadId + inReplyTo make it a reply in that thread. */
|
|
159
|
+
async function gmailCreateDraft(accessToken, { to, subject, body, threadId, inReplyTo, references }) {
|
|
160
|
+
const message = { raw: rawEmail({ to, subject, body, inReplyTo, references }) };
|
|
161
|
+
if (threadId) message.threadId = String(threadId);
|
|
162
|
+
const res = await fetch('https://www.googleapis.com/gmail/v1/users/me/drafts', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({ message }),
|
|
166
|
+
});
|
|
167
|
+
const data = await readJson(res);
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const e = gmailError(data, res.status, 'Gmail draft', ' Gmail was connected before drafts were allowed. Reconnect Gmail in Connect Apps to allow drafts.');
|
|
170
|
+
e.scopeMissing = res.status === 403 && /scope|insufficient/i.test(JSON.stringify(data));
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
return data; // { id, message: { id, threadId } }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Google Calendar (same Google connection as Gmail) ──────────────────────
|
|
177
|
+
const CALENDAR_EVENTS_URL = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
|
|
178
|
+
|
|
179
|
+
function calendarError(data, status, what) {
|
|
180
|
+
const err = (data && data.error) || {};
|
|
181
|
+
const first = Array.isArray(err.errors) && err.errors[0] ? err.errors[0] : {};
|
|
182
|
+
const reason = first.reason || err.status || '';
|
|
183
|
+
const detail = [err.message, first.message].filter((m, i, a) => m && a.indexOf(m) === i).join(': ');
|
|
184
|
+
const text = `${detail} ${reason} ${JSON.stringify(err.details || '')}`;
|
|
185
|
+
let hint = '';
|
|
186
|
+
if (status === 401) hint = ' The Google sign-in expired or was revoked. Reconnect Gmail in Connect Apps.';
|
|
187
|
+
else if (status === 403 && /accessNotConfigured|SERVICE_DISABLED|has not been used|is disabled/i.test(text)) hint = ' The Google Calendar API is not enabled on the Google Cloud project behind this app. Enable "Google Calendar API" in that project (Google Cloud console, APIs & Services, Library), wait a minute, then try again.';
|
|
188
|
+
else if (status === 403 && /scope|insufficient/i.test(text)) hint = ' Google was connected before Calendar access was added. Reconnect Gmail in Connect Apps (it covers Calendar too) and allow calendar access.';
|
|
189
|
+
else if (status === 400) hint = ' Check the start and end times.';
|
|
190
|
+
const e = new Error(`${what} failed (HTTP ${status}${reason ? `, ${reason}` : ''}): ${detail || 'no details from Google'}.${hint}`);
|
|
191
|
+
e.status = status;
|
|
192
|
+
return e;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Events on the primary calendar between timeMin and timeMax (RFC 3339), soonest first. */
|
|
196
|
+
async function calendarListEvents(accessToken, { timeMin, timeMax, maxResults = 20 }) {
|
|
197
|
+
const params = new URLSearchParams({ singleEvents: 'true', orderBy: 'startTime', maxResults: String(maxResults) });
|
|
198
|
+
if (timeMin) params.set('timeMin', timeMin);
|
|
199
|
+
if (timeMax) params.set('timeMax', timeMax);
|
|
200
|
+
const res = await fetch(`${CALENDAR_EVENTS_URL}?${params}`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
201
|
+
const data = await readJson(res);
|
|
202
|
+
if (!res.ok) throw calendarError(data, res.status, 'Calendar list');
|
|
203
|
+
return data.items || [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Creates an event on the primary calendar. `event` is a Calendar API event resource. */
|
|
207
|
+
async function calendarInsertEvent(accessToken, event) {
|
|
208
|
+
const res = await fetch(CALENDAR_EVENTS_URL, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify(event),
|
|
212
|
+
});
|
|
213
|
+
const data = await readJson(res);
|
|
214
|
+
if (!res.ok) throw calendarError(data, res.status, 'Calendar add');
|
|
215
|
+
return data; // { id, htmlLink, ... }
|
|
216
|
+
}
|
|
217
|
+
|
|
126
218
|
async function gmailSearch(accessToken, query, maxResults = 10) {
|
|
127
219
|
// "*" and "" mean "everything" to a person but are not Gmail syntax; an
|
|
128
220
|
// empty q simply lists the newest mail, which is what they meant.
|
|
@@ -242,7 +334,7 @@ function buildVercelAuthUrl(slug, state) {
|
|
|
242
334
|
}
|
|
243
335
|
|
|
244
336
|
async function exchangeVercelCode(clientId, clientSecret, code, redirectUri) {
|
|
245
|
-
const res = await fetch(
|
|
337
|
+
const res = await fetch(`${API.vercel()}/v2/oauth/access_token`, {
|
|
246
338
|
method: 'POST',
|
|
247
339
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
248
340
|
body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, code, redirect_uri: redirectUri }),
|
|
@@ -253,7 +345,7 @@ async function exchangeVercelCode(clientId, clientSecret, code, redirectUri) {
|
|
|
253
345
|
}
|
|
254
346
|
|
|
255
347
|
async function getVercelProfile(accessToken) {
|
|
256
|
-
const res = await fetch(
|
|
348
|
+
const res = await fetch(`${API.vercel()}/v2/user`, {
|
|
257
349
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
258
350
|
});
|
|
259
351
|
const body = await res.json();
|
|
@@ -276,7 +368,7 @@ function buildSupabaseAuthUrl(clientId, redirectUri) {
|
|
|
276
368
|
|
|
277
369
|
async function exchangeSupabaseCode(clientId, clientSecret, code, redirectUri) {
|
|
278
370
|
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
279
|
-
const res = await fetch(
|
|
371
|
+
const res = await fetch(`${API.supabase()}/v1/oauth/token`, {
|
|
280
372
|
method: 'POST',
|
|
281
373
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${basic}` },
|
|
282
374
|
body: new URLSearchParams({ code, grant_type: 'authorization_code', redirect_uri: redirectUri }),
|
|
@@ -299,7 +391,7 @@ async function vercelDeploy(accessToken, teamId, projectName, files) {
|
|
|
299
391
|
// guess the framework unattended.
|
|
300
392
|
const params = new URLSearchParams({ skipAutoDetectionConfirmation: '1' });
|
|
301
393
|
if (teamId) params.set('teamId', teamId);
|
|
302
|
-
const res = await fetch(
|
|
394
|
+
const res = await fetch(`${API.vercel()}/v13/deployments${params.toString() ? `?${params}` : ''}`, {
|
|
303
395
|
method: 'POST',
|
|
304
396
|
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
305
397
|
body: JSON.stringify({ name: projectName, target: 'production', files }),
|
|
@@ -313,7 +405,7 @@ async function vercelDeploy(accessToken, teamId, projectName, files) {
|
|
|
313
405
|
async function vercelSetEnvVars(accessToken, teamId, projectIdOrName, vars) {
|
|
314
406
|
const params = new URLSearchParams({ upsert: 'true' });
|
|
315
407
|
if (teamId) params.set('teamId', teamId);
|
|
316
|
-
const res = await fetch(
|
|
408
|
+
const res = await fetch(`${API.vercel()}/v10/projects/${encodeURIComponent(projectIdOrName)}/env?${params}`, {
|
|
317
409
|
method: 'POST',
|
|
318
410
|
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
319
411
|
body: JSON.stringify(vars.map((v) => ({
|
|
@@ -326,7 +418,7 @@ async function vercelSetEnvVars(accessToken, teamId, projectIdOrName, vars) {
|
|
|
326
418
|
}
|
|
327
419
|
|
|
328
420
|
async function supabaseListOrganizations(accessToken) {
|
|
329
|
-
const res = await fetch(
|
|
421
|
+
const res = await fetch(`${API.supabase()}/v1/organizations`, {
|
|
330
422
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
331
423
|
});
|
|
332
424
|
const body = await res.json();
|
|
@@ -343,8 +435,8 @@ const SUPABASE_PROVISION_TIMEOUT_MS = 3 * 60 * 1000;
|
|
|
343
435
|
* until it flips to `ACTIVE_HEALTHY` (or gives up after the timeout; the
|
|
344
436
|
* project still exists at that point, it's just not confirmed ready yet).
|
|
345
437
|
*/
|
|
346
|
-
async function supabaseCreateProject(accessToken, { name, organizationSlug, dbPass, region = 'us-east-1' }) {
|
|
347
|
-
const res = await fetch(
|
|
438
|
+
async function supabaseCreateProject(accessToken, { name, organizationSlug, dbPass, region = 'us-east-1', pollMs = SUPABASE_PROVISION_POLL_MS }) {
|
|
439
|
+
const res = await fetch(`${API.supabase()}/v1/projects`, {
|
|
348
440
|
method: 'POST',
|
|
349
441
|
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
350
442
|
// Despite older docs listing it as optional, the API now rejects project
|
|
@@ -357,8 +449,8 @@ async function supabaseCreateProject(accessToken, { name, organizationSlug, dbPa
|
|
|
357
449
|
const ref = project.ref;
|
|
358
450
|
const deadline = Date.now() + SUPABASE_PROVISION_TIMEOUT_MS;
|
|
359
451
|
while (project.status !== 'ACTIVE_HEALTHY' && Date.now() < deadline) {
|
|
360
|
-
await new Promise((r) => setTimeout(r,
|
|
361
|
-
const pr = await fetch(
|
|
452
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
453
|
+
const pr = await fetch(`${API.supabase()}/v1/projects/${ref}`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
362
454
|
project = await pr.json();
|
|
363
455
|
if (!pr.ok) throw new Error(project.message || `Supabase project status check failed (HTTP ${pr.status})`);
|
|
364
456
|
}
|
|
@@ -369,7 +461,7 @@ async function supabaseCreateProject(accessToken, { name, organizationSlug, dbPa
|
|
|
369
461
|
}
|
|
370
462
|
|
|
371
463
|
async function supabaseListProjects(accessToken) {
|
|
372
|
-
const res = await fetch(
|
|
464
|
+
const res = await fetch(`${API.supabase()}/v1/projects`, {
|
|
373
465
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
374
466
|
});
|
|
375
467
|
const body = await res.json();
|
|
@@ -379,7 +471,7 @@ async function supabaseListProjects(accessToken) {
|
|
|
379
471
|
|
|
380
472
|
/** Permanent, irreversible - Supabase does not soft-delete or restore a removed project. */
|
|
381
473
|
async function supabaseDeleteProject(accessToken, ref) {
|
|
382
|
-
const res = await fetch(
|
|
474
|
+
const res = await fetch(`${API.supabase()}/v1/projects/${ref}`, {
|
|
383
475
|
method: 'DELETE',
|
|
384
476
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
385
477
|
});
|
|
@@ -390,7 +482,7 @@ async function supabaseDeleteProject(accessToken, ref) {
|
|
|
390
482
|
|
|
391
483
|
/** anon/public key + project URL + DB connection string. The connection string is built locally (not fetched) since dbPass was chosen by the caller, not returned by the API. */
|
|
392
484
|
async function supabaseGetProjectKeys(accessToken, ref, dbPass) {
|
|
393
|
-
const res = await fetch(
|
|
485
|
+
const res = await fetch(`${API.supabase()}/v1/projects/${ref}/api-keys?reveal=true`, {
|
|
394
486
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
395
487
|
});
|
|
396
488
|
const keys = await res.json();
|
|
@@ -407,7 +499,7 @@ async function supabaseGetProjectKeys(accessToken, ref, dbPass) {
|
|
|
407
499
|
/** Supabase OAuth access tokens expire; the refresh token doesn't (until revoked). */
|
|
408
500
|
async function refreshSupabaseToken(clientId, clientSecret, refreshToken) {
|
|
409
501
|
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
410
|
-
const res = await fetch(
|
|
502
|
+
const res = await fetch(`${API.supabase()}/v1/oauth/token`, {
|
|
411
503
|
method: 'POST',
|
|
412
504
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${basic}` },
|
|
413
505
|
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
|
|
@@ -448,17 +540,17 @@ async function apiCall(base, accessToken, method, apiPath, body, query) {
|
|
|
448
540
|
}
|
|
449
541
|
|
|
450
542
|
function supabaseApi(accessToken, method, apiPath, body) {
|
|
451
|
-
return apiCall(
|
|
543
|
+
return apiCall(API.supabase(), accessToken, method, apiPath, body);
|
|
452
544
|
}
|
|
453
545
|
|
|
454
546
|
/** Runs SQL against a project's Postgres through the Management API. */
|
|
455
547
|
function supabaseQuery(accessToken, ref, query) {
|
|
456
|
-
return apiCall(
|
|
548
|
+
return apiCall(API.supabase(), accessToken, 'POST', `/v1/projects/${encodeURIComponent(ref)}/database/query`, { query });
|
|
457
549
|
}
|
|
458
550
|
|
|
459
551
|
/** teamId is appended automatically when the connection was installed to a team. */
|
|
460
552
|
function vercelApi(accessToken, teamId, method, apiPath, body) {
|
|
461
|
-
return apiCall(
|
|
553
|
+
return apiCall(API.vercel(), accessToken, method, apiPath, body, { teamId });
|
|
462
554
|
}
|
|
463
555
|
|
|
464
556
|
// GitHub's standard OAuth Apps flow (docs.github.com/apps/oauth-apps) - much
|
|
@@ -486,7 +578,7 @@ async function exchangeGithubCode(clientId, clientSecret, code, redirectUri) {
|
|
|
486
578
|
}
|
|
487
579
|
|
|
488
580
|
async function getGithubProfile(accessToken) {
|
|
489
|
-
const res = await fetch(
|
|
581
|
+
const res = await fetch(`${API.github()}/user`, {
|
|
490
582
|
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/vnd.github+json' },
|
|
491
583
|
});
|
|
492
584
|
const body = await res.json();
|
|
@@ -496,7 +588,7 @@ async function getGithubProfile(accessToken) {
|
|
|
496
588
|
|
|
497
589
|
/** Creates a new repo under the connected user's account. Defaults to private - safer for an arbitrary local folder than defaulting public. */
|
|
498
590
|
async function githubCreateRepo(accessToken, name, { private: isPrivate = true } = {}) {
|
|
499
|
-
const res = await fetch(
|
|
591
|
+
const res = await fetch(`${API.github()}/user/repos`, {
|
|
500
592
|
method: 'POST',
|
|
501
593
|
headers: {
|
|
502
594
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -511,8 +603,38 @@ async function githubCreateRepo(accessToken, name, { private: isPrivate = true }
|
|
|
511
603
|
return body; // { name, full_name, html_url, clone_url, default_branch, ... }
|
|
512
604
|
}
|
|
513
605
|
|
|
606
|
+
/**
|
|
607
|
+
* The token fallback in Connect Apps: the user pastes their own personal
|
|
608
|
+
* access token (Vercel account token, Supabase access token, GitHub token)
|
|
609
|
+
* when no OAuth app is configured. One read-only call proves it works and
|
|
610
|
+
* gives the label shown on the card. The token itself is never logged.
|
|
611
|
+
*/
|
|
612
|
+
async function checkAccessToken(name, token) {
|
|
613
|
+
const t = String(token || '').trim();
|
|
614
|
+
if (!t || /\s/.test(t)) throw new Error('That does not look like a token. Paste it again without spaces.');
|
|
615
|
+
if (name === 'vercel') {
|
|
616
|
+
const res = await fetch(`${API.vercel()}/v2/user`, { headers: { Authorization: `Bearer ${t}` } });
|
|
617
|
+
const body = await readJson(res);
|
|
618
|
+
if (!res.ok) throw new Error(res.status === 403 || res.status === 401 ? 'Vercel did not accept that token. Create a new one at vercel.com/account/settings/tokens.' : `Vercel check failed (HTTP ${res.status}).`);
|
|
619
|
+
return { accessToken: t, userName: body.user?.username || body.user?.email || '' };
|
|
620
|
+
}
|
|
621
|
+
if (name === 'supabase') {
|
|
622
|
+
const res = await fetch(`${API.supabase()}/v1/organizations`, { headers: { Authorization: `Bearer ${t}` } });
|
|
623
|
+
const body = await readJson(res);
|
|
624
|
+
if (!res.ok) throw new Error(res.status === 401 || res.status === 403 ? 'Supabase did not accept that token. Create a new one at supabase.com/dashboard/account/tokens.' : `Supabase check failed (HTTP ${res.status}).`);
|
|
625
|
+
return { accessToken: t, refreshToken: '', expiresAt: 0, email: Array.isArray(body) && body[0] ? body[0].name : '' };
|
|
626
|
+
}
|
|
627
|
+
if (name === 'github') {
|
|
628
|
+
const userName = await getGithubProfile(t).catch(() => { throw new Error('GitHub did not accept that token. Create one with the "repo" scope at github.com/settings/tokens.'); });
|
|
629
|
+
return { accessToken: t, userName };
|
|
630
|
+
}
|
|
631
|
+
throw new Error(`${name} has no token sign-in.`);
|
|
632
|
+
}
|
|
633
|
+
|
|
514
634
|
module.exports = {
|
|
515
|
-
|
|
635
|
+
API, apiCall, checkAccessToken,
|
|
636
|
+
buildGmailAuthUrl, exchangeGmailCode, refreshGmailToken, getGmailProfile, gmailSend, gmailSearch, gmailCreateDraft, gmailError, rawEmail,
|
|
637
|
+
calendarListEvents, calendarInsertEvent, calendarError, GOOGLE_SCOPE,
|
|
516
638
|
buildSlackAuthUrl, exchangeSlackCode, slackPostMessage, slackListChannels, slackJoinChannel,
|
|
517
639
|
buildVercelAuthUrl, exchangeVercelCode, getVercelProfile, vercelDeploy, vercelSetEnvVars,
|
|
518
640
|
buildSupabaseAuthUrl, exchangeSupabaseCode, refreshSupabaseToken, supabaseListOrganizations, supabaseCreateProject, supabaseGetProjectKeys,
|
package/lib/permissions.js
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standing permission rules, set once in a file instead of clicked per chat.
|
|
3
|
-
*
|
|
4
|
-
* ~/.codeply/permissions.json (every project)
|
|
5
|
-
* <project>/.codeply/permissions.json (this project, checked first)
|
|
6
|
-
*
|
|
7
|
-
* {
|
|
8
|
-
* "deny": ["run:git push*", "run:rm*", "write_file:*.env", "supabase_delete_project"],
|
|
9
|
-
* "allow": ["run:npm test", "run:npm run *", "run:git status", "edit_file:src/**"]
|
|
10
|
-
* }
|
|
11
|
-
*
|
|
12
|
-
* A rule is "<tool>" or "<tool>:<pattern>". For run the pattern matches the
|
|
13
|
-
* command's name (npm run dev, git checkout) and the full command line; for
|
|
14
|
-
* file tools it matches the path relative to the project. * matches within a
|
|
15
|
-
* path segment, ** across them, and a trailing " *" also matches the bare
|
|
16
|
-
* command. Deny beats allow; project rules beat global ones; anything no rule
|
|
17
|
-
* mentions is asked as usual. Evaluation is the same idea as opencode's
|
|
18
|
-
* permission/ rulesets.
|
|
19
|
-
*/
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const os = require('os');
|
|
22
|
-
const path = require('path');
|
|
23
|
-
|
|
24
|
-
function readRules(file) {
|
|
25
|
-
try {
|
|
26
|
-
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
27
|
-
const list = (k) => (Array.isArray(j[k]) ? j[k].map(String).filter(Boolean) : []);
|
|
28
|
-
return { allow: list('allow'), deny: list('deny') };
|
|
29
|
-
} catch { return { allow: [], deny: [] }; }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function wildcard(pattern) {
|
|
33
|
-
let re = '';
|
|
34
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
35
|
-
const c = pattern[i];
|
|
36
|
-
if (c === '*' && pattern[i + 1] === '*') { re += '.*'; i++; }
|
|
37
|
-
else if (c === '*') re += '[^/]*';
|
|
38
|
-
else if (c === '?') re += '.';
|
|
39
|
-
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
40
|
-
}
|
|
41
|
-
// "npm run *" should also cover plain "npm run".
|
|
42
|
-
re = re.replace(/ \[\^\/\]\*$/, '(?: .*)?').replace(/ \.\*$/, '(?: .*)?');
|
|
43
|
-
return new RegExp(`^${re}$`, process.platform === 'win32' ? 'i' : '');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Strings a request can be matched against for its tool. */
|
|
47
|
-
function subjects(req, cwd) {
|
|
48
|
-
if (req.tool === 'run') {
|
|
49
|
-
const cmd = String(req.detail || '').trim();
|
|
50
|
-
// Commands match by name AND as full text, so "run:npm run *" and
|
|
51
|
-
// "run:git push*" both work.
|
|
52
|
-
return { cmd, names: Array.isArray(req.patterns) ? req.patterns : [] };
|
|
53
|
-
}
|
|
54
|
-
if (req.path) {
|
|
55
|
-
const abs = path.isAbsolute(req.path) ? req.path : path.resolve(cwd || '.', req.path);
|
|
56
|
-
const rel = path.relative(cwd || '.', abs).replace(/\\/g, '/');
|
|
57
|
-
return { path: rel.startsWith('..') ? abs.replace(/\\/g, '/') : rel };
|
|
58
|
-
}
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function ruleMatches(rule, req, subj) {
|
|
63
|
-
const i = rule.indexOf(':');
|
|
64
|
-
const tool = (i === -1 ? rule : rule.slice(0, i)).trim();
|
|
65
|
-
const pattern = i === -1 ? null : rule.slice(i + 1).trim();
|
|
66
|
-
if (tool !== req.tool && tool !== '*') return false;
|
|
67
|
-
if (!pattern || pattern === '*') return true;
|
|
68
|
-
const re = wildcard(pattern);
|
|
69
|
-
if (req.tool === 'run') {
|
|
70
|
-
if (re.test(subj.cmd)) return true;
|
|
71
|
-
// For allow on a compound command every part must be covered (checked by
|
|
72
|
-
// the caller); here a single name matching is enough to count.
|
|
73
|
-
return subj.names.some((n) => re.test(n));
|
|
74
|
-
}
|
|
75
|
-
return subj.path ? re.test(subj.path) : false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {object} req the approval request ({tool, detail, patterns, path, danger})
|
|
80
|
-
* @returns {{decision: 'allow'|'deny'|null, rule?: string, file?: string}}
|
|
81
|
-
*/
|
|
82
|
-
function decide(req, cwd) {
|
|
83
|
-
const files = [cwd && path.join(cwd, '.codeply', 'permissions.json'), path.join(os.homedir(), '.codeply', 'permissions.json')].filter(Boolean);
|
|
84
|
-
const sets = files.map((f) => ({ file: f, ...readRules(f) }));
|
|
85
|
-
const subj = subjects(req, cwd);
|
|
86
|
-
for (const s of sets) {
|
|
87
|
-
const hit = s.deny.find((r) => ruleMatches(r, req, subj));
|
|
88
|
-
if (hit) return { decision: 'deny', rule: hit, file: s.file };
|
|
89
|
-
}
|
|
90
|
-
// Risky-looking commands (rm -rf, format, curl | sh...) always ask.
|
|
91
|
-
if (req.danger) return { decision: null };
|
|
92
|
-
for (const s of sets) {
|
|
93
|
-
if (req.tool === 'run') {
|
|
94
|
-
// Allow is judged per command, never on the whole line: "npm run *"
|
|
95
|
-
// must not let "npm run build && npm install" through. A line that
|
|
96
|
-
// can't be split into named commands (subshells, redirects) is never
|
|
97
|
-
// auto-allowed.
|
|
98
|
-
if (!subj.names.length) continue;
|
|
99
|
-
const parts = subj.names.length === 1 ? [{ cmd: subj.cmd, names: subj.names }] : subj.names.map((n) => ({ cmd: n, names: [n] }));
|
|
100
|
-
if (parts.every((p) => s.allow.some((r) => ruleMatches(r, req, p)))) return { decision: 'allow', rule: 'allow rules', file: s.file };
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const hit = s.allow.find((r) => ruleMatches(r, req, subj));
|
|
104
|
-
if (hit) return { decision: 'allow', rule: hit, file: s.file };
|
|
105
|
-
}
|
|
106
|
-
return { decision: null };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
module.exports = { decide, wildcard };
|
|
1
|
+
/**
|
|
2
|
+
* Standing permission rules, set once in a file instead of clicked per chat.
|
|
3
|
+
*
|
|
4
|
+
* ~/.codeply/permissions.json (every project)
|
|
5
|
+
* <project>/.codeply/permissions.json (this project, checked first)
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* "deny": ["run:git push*", "run:rm*", "write_file:*.env", "supabase_delete_project"],
|
|
9
|
+
* "allow": ["run:npm test", "run:npm run *", "run:git status", "edit_file:src/**"]
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* A rule is "<tool>" or "<tool>:<pattern>". For run the pattern matches the
|
|
13
|
+
* command's name (npm run dev, git checkout) and the full command line; for
|
|
14
|
+
* file tools it matches the path relative to the project. * matches within a
|
|
15
|
+
* path segment, ** across them, and a trailing " *" also matches the bare
|
|
16
|
+
* command. Deny beats allow; project rules beat global ones; anything no rule
|
|
17
|
+
* mentions is asked as usual. Evaluation is the same idea as opencode's
|
|
18
|
+
* permission/ rulesets.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
function readRules(file) {
|
|
25
|
+
try {
|
|
26
|
+
const j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
27
|
+
const list = (k) => (Array.isArray(j[k]) ? j[k].map(String).filter(Boolean) : []);
|
|
28
|
+
return { allow: list('allow'), deny: list('deny') };
|
|
29
|
+
} catch { return { allow: [], deny: [] }; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function wildcard(pattern) {
|
|
33
|
+
let re = '';
|
|
34
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
35
|
+
const c = pattern[i];
|
|
36
|
+
if (c === '*' && pattern[i + 1] === '*') { re += '.*'; i++; }
|
|
37
|
+
else if (c === '*') re += '[^/]*';
|
|
38
|
+
else if (c === '?') re += '.';
|
|
39
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
40
|
+
}
|
|
41
|
+
// "npm run *" should also cover plain "npm run".
|
|
42
|
+
re = re.replace(/ \[\^\/\]\*$/, '(?: .*)?').replace(/ \.\*$/, '(?: .*)?');
|
|
43
|
+
return new RegExp(`^${re}$`, process.platform === 'win32' ? 'i' : '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Strings a request can be matched against for its tool. */
|
|
47
|
+
function subjects(req, cwd) {
|
|
48
|
+
if (req.tool === 'run') {
|
|
49
|
+
const cmd = String(req.detail || '').trim();
|
|
50
|
+
// Commands match by name AND as full text, so "run:npm run *" and
|
|
51
|
+
// "run:git push*" both work.
|
|
52
|
+
return { cmd, names: Array.isArray(req.patterns) ? req.patterns : [] };
|
|
53
|
+
}
|
|
54
|
+
if (req.path) {
|
|
55
|
+
const abs = path.isAbsolute(req.path) ? req.path : path.resolve(cwd || '.', req.path);
|
|
56
|
+
const rel = path.relative(cwd || '.', abs).replace(/\\/g, '/');
|
|
57
|
+
return { path: rel.startsWith('..') ? abs.replace(/\\/g, '/') : rel };
|
|
58
|
+
}
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ruleMatches(rule, req, subj) {
|
|
63
|
+
const i = rule.indexOf(':');
|
|
64
|
+
const tool = (i === -1 ? rule : rule.slice(0, i)).trim();
|
|
65
|
+
const pattern = i === -1 ? null : rule.slice(i + 1).trim();
|
|
66
|
+
if (tool !== req.tool && tool !== '*') return false;
|
|
67
|
+
if (!pattern || pattern === '*') return true;
|
|
68
|
+
const re = wildcard(pattern);
|
|
69
|
+
if (req.tool === 'run') {
|
|
70
|
+
if (re.test(subj.cmd)) return true;
|
|
71
|
+
// For allow on a compound command every part must be covered (checked by
|
|
72
|
+
// the caller); here a single name matching is enough to count.
|
|
73
|
+
return subj.names.some((n) => re.test(n));
|
|
74
|
+
}
|
|
75
|
+
return subj.path ? re.test(subj.path) : false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {object} req the approval request ({tool, detail, patterns, path, danger})
|
|
80
|
+
* @returns {{decision: 'allow'|'deny'|null, rule?: string, file?: string}}
|
|
81
|
+
*/
|
|
82
|
+
function decide(req, cwd) {
|
|
83
|
+
const files = [cwd && path.join(cwd, '.codeply', 'permissions.json'), path.join(os.homedir(), '.codeply', 'permissions.json')].filter(Boolean);
|
|
84
|
+
const sets = files.map((f) => ({ file: f, ...readRules(f) }));
|
|
85
|
+
const subj = subjects(req, cwd);
|
|
86
|
+
for (const s of sets) {
|
|
87
|
+
const hit = s.deny.find((r) => ruleMatches(r, req, subj));
|
|
88
|
+
if (hit) return { decision: 'deny', rule: hit, file: s.file };
|
|
89
|
+
}
|
|
90
|
+
// Risky-looking commands (rm -rf, format, curl | sh...) always ask.
|
|
91
|
+
if (req.danger) return { decision: null };
|
|
92
|
+
for (const s of sets) {
|
|
93
|
+
if (req.tool === 'run') {
|
|
94
|
+
// Allow is judged per command, never on the whole line: "npm run *"
|
|
95
|
+
// must not let "npm run build && npm install" through. A line that
|
|
96
|
+
// can't be split into named commands (subshells, redirects) is never
|
|
97
|
+
// auto-allowed.
|
|
98
|
+
if (!subj.names.length) continue;
|
|
99
|
+
const parts = subj.names.length === 1 ? [{ cmd: subj.cmd, names: subj.names }] : subj.names.map((n) => ({ cmd: n, names: [n] }));
|
|
100
|
+
if (parts.every((p) => s.allow.some((r) => ruleMatches(r, req, p)))) return { decision: 'allow', rule: 'allow rules', file: s.file };
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const hit = s.allow.find((r) => ruleMatches(r, req, subj));
|
|
104
|
+
if (hit) return { decision: 'allow', rule: hit, file: s.file };
|
|
105
|
+
}
|
|
106
|
+
return { decision: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { decide, wildcard };
|